balibabu
commited on
Commit
·
8e222fd
1
Parent(s):
2edbd4b
feat: add file icon and add message popover content (#77)
Browse files* feat: add message popover content
* feat: add file icon
This view is limited to 50 files because it contains too many changes.
See raw diff
- web/src/assets/svg/file-icon/aep.svg +10 -0
- web/src/assets/svg/file-icon/ai.svg +10 -0
- web/src/assets/svg/file-icon/avi.svg +10 -0
- web/src/assets/svg/file-icon/css.svg +10 -0
- web/src/assets/svg/file-icon/csv.svg +10 -0
- web/src/assets/svg/file-icon/dmg.svg +10 -0
- web/src/assets/svg/file-icon/doc.svg +10 -0
- web/src/assets/svg/file-icon/docx.svg +10 -0
- web/src/assets/svg/file-icon/eps.svg +10 -0
- web/src/assets/svg/file-icon/exe.svg +10 -0
- web/src/assets/svg/file-icon/fig.svg +10 -0
- web/src/assets/svg/file-icon/gif.svg +10 -0
- web/src/assets/svg/file-icon/html.svg +10 -0
- web/src/assets/svg/file-icon/indd.svg +10 -0
- web/src/assets/svg/file-icon/java.svg +10 -0
- web/src/assets/svg/file-icon/jpeg.svg +10 -0
- web/src/assets/svg/file-icon/jpg.svg +10 -0
- web/src/assets/svg/file-icon/js.svg +10 -0
- web/src/assets/svg/file-icon/json.svg +10 -0
- web/src/assets/svg/file-icon/mkv.svg +10 -0
- web/src/assets/svg/file-icon/mp3.svg +10 -0
- web/src/assets/svg/file-icon/mp4.svg +10 -0
- web/src/assets/svg/file-icon/mpeg.svg +10 -0
- web/src/assets/svg/file-icon/pdf.svg +10 -0
- web/src/assets/svg/file-icon/png.svg +10 -0
- web/src/assets/svg/file-icon/ppt.svg +10 -0
- web/src/assets/svg/file-icon/pptx.svg +10 -0
- web/src/assets/svg/file-icon/psd.svg +10 -0
- web/src/assets/svg/file-icon/rss.svg +10 -0
- web/src/assets/svg/file-icon/sql.svg +10 -0
- web/src/assets/svg/file-icon/svg.svg +10 -0
- web/src/assets/svg/file-icon/tiff.svg +10 -0
- web/src/assets/svg/file-icon/txt.svg +10 -0
- web/src/assets/svg/file-icon/wav.svg +10 -0
- web/src/assets/svg/file-icon/webp.svg +0 -0
- web/src/assets/svg/file-icon/xls.svg +10 -0
- web/src/assets/svg/file-icon/xlsx.svg +10 -0
- web/src/assets/svg/file-icon/xml.svg +10 -0
- web/src/components/new-document-link.tsx +20 -0
- web/src/components/svg-icon.tsx +34 -0
- web/src/constants/common.ts +40 -0
- web/src/hooks/commonHooks.ts +36 -0
- web/src/interfaces/database/chat.ts +1 -1
- web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx +0 -6
- web/src/pages/add-knowledge/components/knowledge-testing/testing-result/select-files.tsx +3 -7
- web/src/pages/add-knowledge/index.tsx +5 -5
- web/src/pages/chat/chat-container/index.less +8 -0
- web/src/pages/chat/chat-container/index.tsx +107 -24
- web/src/pages/chat/hooks.ts +37 -1
- web/src/utils/domUtils.ts +3 -0
web/src/assets/svg/file-icon/aep.svg
ADDED
|
web/src/assets/svg/file-icon/ai.svg
ADDED
|
web/src/assets/svg/file-icon/avi.svg
ADDED
|
web/src/assets/svg/file-icon/css.svg
ADDED
|
web/src/assets/svg/file-icon/csv.svg
ADDED
|
web/src/assets/svg/file-icon/dmg.svg
ADDED
|
web/src/assets/svg/file-icon/doc.svg
ADDED
|
web/src/assets/svg/file-icon/docx.svg
ADDED
|
web/src/assets/svg/file-icon/eps.svg
ADDED
|
web/src/assets/svg/file-icon/exe.svg
ADDED
|
web/src/assets/svg/file-icon/fig.svg
ADDED
|
web/src/assets/svg/file-icon/gif.svg
ADDED
|
web/src/assets/svg/file-icon/html.svg
ADDED
|
web/src/assets/svg/file-icon/indd.svg
ADDED
|
web/src/assets/svg/file-icon/java.svg
ADDED
|
web/src/assets/svg/file-icon/jpeg.svg
ADDED
|
web/src/assets/svg/file-icon/jpg.svg
ADDED
|
web/src/assets/svg/file-icon/js.svg
ADDED
|
web/src/assets/svg/file-icon/json.svg
ADDED
|
web/src/assets/svg/file-icon/mkv.svg
ADDED
|
web/src/assets/svg/file-icon/mp3.svg
ADDED
|
web/src/assets/svg/file-icon/mp4.svg
ADDED
|
web/src/assets/svg/file-icon/mpeg.svg
ADDED
|
web/src/assets/svg/file-icon/pdf.svg
ADDED
|
web/src/assets/svg/file-icon/png.svg
ADDED
|
web/src/assets/svg/file-icon/ppt.svg
ADDED
|
web/src/assets/svg/file-icon/pptx.svg
ADDED
|
web/src/assets/svg/file-icon/psd.svg
ADDED
|
web/src/assets/svg/file-icon/rss.svg
ADDED
|
web/src/assets/svg/file-icon/sql.svg
ADDED
|
web/src/assets/svg/file-icon/svg.svg
ADDED
|
web/src/assets/svg/file-icon/tiff.svg
ADDED
|
web/src/assets/svg/file-icon/txt.svg
ADDED
|
web/src/assets/svg/file-icon/wav.svg
ADDED
|
web/src/assets/svg/file-icon/webp.svg
ADDED
|
web/src/assets/svg/file-icon/xls.svg
ADDED
|
web/src/assets/svg/file-icon/xlsx.svg
ADDED
|
web/src/assets/svg/file-icon/xml.svg
ADDED
|
web/src/components/new-document-link.tsx
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { api_host } from '@/utils/api';
|
2 |
+
import React from 'react';
|
3 |
+
|
4 |
+
interface IProps extends React.PropsWithChildren {
|
5 |
+
documentId: string;
|
6 |
+
}
|
7 |
+
|
8 |
+
const NewDocumentLink = ({ children, documentId }: IProps) => {
|
9 |
+
return (
|
10 |
+
<a
|
11 |
+
target="_blank"
|
12 |
+
href={`${api_host}/document/get/${documentId}`}
|
13 |
+
rel="noreferrer"
|
14 |
+
>
|
15 |
+
{children}
|
16 |
+
</a>
|
17 |
+
);
|
18 |
+
};
|
19 |
+
|
20 |
+
export default NewDocumentLink;
|
web/src/components/svg-icon.tsx
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import {
|
2 |
+
UseDynamicSVGImportOptions,
|
3 |
+
useDynamicSVGImport,
|
4 |
+
} from '@/hooks/commonHooks';
|
5 |
+
|
6 |
+
interface IconProps extends React.SVGProps<SVGSVGElement> {
|
7 |
+
name: string;
|
8 |
+
onCompleted?: UseDynamicSVGImportOptions['onCompleted'];
|
9 |
+
onError?: UseDynamicSVGImportOptions['onError'];
|
10 |
+
}
|
11 |
+
|
12 |
+
const SvgIcon: React.FC<IconProps> = ({
|
13 |
+
name,
|
14 |
+
onCompleted,
|
15 |
+
onError,
|
16 |
+
...rest
|
17 |
+
}): React.ReactNode | null => {
|
18 |
+
const { error, loading, SvgIcon } = useDynamicSVGImport(name, {
|
19 |
+
onCompleted,
|
20 |
+
onError,
|
21 |
+
});
|
22 |
+
if (error) {
|
23 |
+
return error.message;
|
24 |
+
}
|
25 |
+
if (loading) {
|
26 |
+
return 'Loading...';
|
27 |
+
}
|
28 |
+
if (SvgIcon) {
|
29 |
+
return <SvgIcon {...rest} />;
|
30 |
+
}
|
31 |
+
return null;
|
32 |
+
};
|
33 |
+
|
34 |
+
export default SvgIcon;
|
web/src/constants/common.ts
CHANGED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const fileIconMap = {
|
2 |
+
aep: 'aep.svg',
|
3 |
+
ai: 'ai.svg',
|
4 |
+
avi: 'avi.svg',
|
5 |
+
css: 'css.svg',
|
6 |
+
csv: 'csv.svg',
|
7 |
+
dmg: 'dmg.svg',
|
8 |
+
doc: 'doc.svg',
|
9 |
+
docx: 'docx.svg',
|
10 |
+
eps: 'eps.svg',
|
11 |
+
exe: 'exe.svg',
|
12 |
+
fig: 'fig.svg',
|
13 |
+
gif: 'gif.svg',
|
14 |
+
html: 'html.svg',
|
15 |
+
indd: 'indd.svg',
|
16 |
+
java: 'java.svg',
|
17 |
+
jpeg: 'jpeg.svg',
|
18 |
+
jpg: 'jpg.svg',
|
19 |
+
js: 'js.svg',
|
20 |
+
json: 'json.svg',
|
21 |
+
mkv: 'mkv.svg',
|
22 |
+
mp3: 'mp3.svg',
|
23 |
+
mp4: 'mp4.svg',
|
24 |
+
mpeg: 'mpeg.svg',
|
25 |
+
pdf: 'pdf.svg',
|
26 |
+
png: 'png.svg',
|
27 |
+
ppt: 'ppt.svg',
|
28 |
+
pptx: 'pptx.svg',
|
29 |
+
psd: 'psd.svg',
|
30 |
+
rss: 'rss.svg',
|
31 |
+
sql: 'sql.svg',
|
32 |
+
svg: 'svg.svg',
|
33 |
+
tiff: 'tiff.svg',
|
34 |
+
txt: 'txt.svg',
|
35 |
+
wav: 'wav.svg',
|
36 |
+
webp: 'webp.svg',
|
37 |
+
xls: 'xls.svg',
|
38 |
+
xlsx: 'xlsx.svg',
|
39 |
+
xml: 'xml.svg',
|
40 |
+
};
|
web/src/hooks/commonHooks.ts
CHANGED
@@ -32,3 +32,39 @@ export const useDeepCompareEffect = (
|
|
32 |
};
|
33 |
}, []);
|
34 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
};
|
33 |
}, []);
|
34 |
};
|
35 |
+
|
36 |
+
export interface UseDynamicSVGImportOptions {
|
37 |
+
onCompleted?: (
|
38 |
+
name: string,
|
39 |
+
SvgIcon: React.FC<React.SVGProps<SVGSVGElement>> | undefined,
|
40 |
+
) => void;
|
41 |
+
onError?: (err: Error) => void;
|
42 |
+
}
|
43 |
+
|
44 |
+
export function useDynamicSVGImport(
|
45 |
+
name: string,
|
46 |
+
options: UseDynamicSVGImportOptions = {},
|
47 |
+
) {
|
48 |
+
const ImportedIconRef = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
|
49 |
+
const [loading, setLoading] = useState(false);
|
50 |
+
const [error, setError] = useState<Error>();
|
51 |
+
|
52 |
+
const { onCompleted, onError } = options;
|
53 |
+
useEffect(() => {
|
54 |
+
setLoading(true);
|
55 |
+
const importIcon = async (): Promise<void> => {
|
56 |
+
try {
|
57 |
+
ImportedIconRef.current = (await import(name)).ReactComponent;
|
58 |
+
onCompleted?.(name, ImportedIconRef.current);
|
59 |
+
} catch (err: any) {
|
60 |
+
onError?.(err);
|
61 |
+
setError(err);
|
62 |
+
} finally {
|
63 |
+
setLoading(false);
|
64 |
+
}
|
65 |
+
};
|
66 |
+
importIcon();
|
67 |
+
}, [name, onCompleted, onError]);
|
68 |
+
|
69 |
+
return { error, loading, SvgIcon: ImportedIconRef.current };
|
70 |
+
}
|
web/src/interfaces/database/chat.ts
CHANGED
@@ -71,7 +71,7 @@ export interface IReference {
|
|
71 |
total: number;
|
72 |
}
|
73 |
|
74 |
-
interface Docagg {
|
75 |
count: number;
|
76 |
doc_id: string;
|
77 |
doc_name: string;
|
|
|
71 |
total: number;
|
72 |
}
|
73 |
|
74 |
+
export interface Docagg {
|
75 |
count: number;
|
76 |
doc_id: string;
|
77 |
doc_name: string;
|
web/src/pages/add-knowledge/components/knowledge-chunk/components/chunk-card/index.tsx
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
import Image from '@/components/image';
|
2 |
import { IChunk } from '@/interfaces/database/knowledge';
|
3 |
-
import { api_host } from '@/utils/api';
|
4 |
import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd';
|
5 |
import { useState } from 'react';
|
6 |
import styles from './index.less';
|
@@ -48,11 +47,6 @@ const ChunkCard = ({
|
|
48 |
<Image id={item.img_id} className={styles.imagePreview}></Image>
|
49 |
}
|
50 |
>
|
51 |
-
<img
|
52 |
-
src={`${api_host}/document/image/${item.img_id}`}
|
53 |
-
alt=""
|
54 |
-
className={styles.image}
|
55 |
-
/>
|
56 |
<Image id={item.img_id} className={styles.image}></Image>
|
57 |
</Popover>
|
58 |
)}
|
|
|
1 |
import Image from '@/components/image';
|
2 |
import { IChunk } from '@/interfaces/database/knowledge';
|
|
|
3 |
import { Card, Checkbox, CheckboxProps, Flex, Popover, Switch } from 'antd';
|
4 |
import { useState } from 'react';
|
5 |
import styles from './index.less';
|
|
|
47 |
<Image id={item.img_id} className={styles.imagePreview}></Image>
|
48 |
}
|
49 |
>
|
|
|
|
|
|
|
|
|
|
|
50 |
<Image id={item.img_id} className={styles.image}></Image>
|
51 |
</Popover>
|
52 |
)}
|
web/src/pages/add-knowledge/components/knowledge-testing/testing-result/select-files.tsx
CHANGED
@@ -1,6 +1,6 @@
|
|
1 |
import { ReactComponent as NavigationPointerIcon } from '@/assets/svg/navigation-pointer.svg';
|
|
|
2 |
import { ITestingDocument } from '@/interfaces/database/knowledge';
|
3 |
-
import { api_host } from '@/utils/api';
|
4 |
import { Table, TableProps } from 'antd';
|
5 |
import { useDispatch, useSelector } from 'umi';
|
6 |
|
@@ -34,13 +34,9 @@ const SelectFiles = ({ handleTesting }: IProps) => {
|
|
34 |
key: 'view',
|
35 |
width: 50,
|
36 |
render: (_, { doc_id }) => (
|
37 |
-
<
|
38 |
-
href={`${api_host}/document/get/${doc_id}`}
|
39 |
-
target="_blank"
|
40 |
-
rel="noreferrer"
|
41 |
-
>
|
42 |
<NavigationPointerIcon />
|
43 |
-
</
|
44 |
),
|
45 |
},
|
46 |
];
|
|
|
1 |
import { ReactComponent as NavigationPointerIcon } from '@/assets/svg/navigation-pointer.svg';
|
2 |
+
import NewDocumentLink from '@/components/new-document-link';
|
3 |
import { ITestingDocument } from '@/interfaces/database/knowledge';
|
|
|
4 |
import { Table, TableProps } from 'antd';
|
5 |
import { useDispatch, useSelector } from 'umi';
|
6 |
|
|
|
34 |
key: 'view',
|
35 |
width: 50,
|
36 |
render: (_, { doc_id }) => (
|
37 |
+
<NewDocumentLink documentId={doc_id}>
|
|
|
|
|
|
|
|
|
38 |
<NavigationPointerIcon />
|
39 |
+
</NewDocumentLink>
|
40 |
),
|
41 |
},
|
42 |
];
|
web/src/pages/add-knowledge/index.tsx
CHANGED
@@ -2,7 +2,7 @@ import { useKnowledgeBaseId } from '@/hooks/knowledgeHook';
|
|
2 |
import { useSecondPathName, useThirdPathName } from '@/hooks/routeHook';
|
3 |
import { Breadcrumb } from 'antd';
|
4 |
import { ItemType } from 'antd/es/breadcrumb/Breadcrumb';
|
5 |
-
import { useEffect, useMemo } from 'react';
|
6 |
import { Link, Outlet, useDispatch, useLocation, useNavigate } from 'umi';
|
7 |
import Siderbar from './components/knowledge-sidebar';
|
8 |
import {
|
@@ -25,9 +25,9 @@ const KnowledgeAdding = () => {
|
|
25 |
const datasetActiveKey: KnowledgeDatasetRouteKey =
|
26 |
useThirdPathName() as KnowledgeDatasetRouteKey;
|
27 |
|
28 |
-
const gotoList = () => {
|
29 |
navigate('/knowledge');
|
30 |
-
};
|
31 |
|
32 |
const breadcrumbItems: ItemType[] = useMemo(() => {
|
33 |
const items: ItemType[] = [
|
@@ -54,7 +54,7 @@ const KnowledgeAdding = () => {
|
|
54 |
}
|
55 |
|
56 |
return items;
|
57 |
-
}, [activeKey, datasetActiveKey]);
|
58 |
|
59 |
useEffect(() => {
|
60 |
const search: string = location.search.slice(1);
|
@@ -71,7 +71,7 @@ const KnowledgeAdding = () => {
|
|
71 |
...map,
|
72 |
},
|
73 |
});
|
74 |
-
}, [location]);
|
75 |
|
76 |
return (
|
77 |
<>
|
|
|
2 |
import { useSecondPathName, useThirdPathName } from '@/hooks/routeHook';
|
3 |
import { Breadcrumb } from 'antd';
|
4 |
import { ItemType } from 'antd/es/breadcrumb/Breadcrumb';
|
5 |
+
import { useCallback, useEffect, useMemo } from 'react';
|
6 |
import { Link, Outlet, useDispatch, useLocation, useNavigate } from 'umi';
|
7 |
import Siderbar from './components/knowledge-sidebar';
|
8 |
import {
|
|
|
25 |
const datasetActiveKey: KnowledgeDatasetRouteKey =
|
26 |
useThirdPathName() as KnowledgeDatasetRouteKey;
|
27 |
|
28 |
+
const gotoList = useCallback(() => {
|
29 |
navigate('/knowledge');
|
30 |
+
}, [navigate]);
|
31 |
|
32 |
const breadcrumbItems: ItemType[] = useMemo(() => {
|
33 |
const items: ItemType[] = [
|
|
|
54 |
}
|
55 |
|
56 |
return items;
|
57 |
+
}, [activeKey, datasetActiveKey, gotoList, knowledgeBaseId]);
|
58 |
|
59 |
useEffect(() => {
|
60 |
const search: string = location.search.slice(1);
|
|
|
71 |
...map,
|
72 |
},
|
73 |
});
|
74 |
+
}, [location, dispatch]);
|
75 |
|
76 |
return (
|
77 |
<>
|
web/src/pages/chat/chat-container/index.less
CHANGED
@@ -39,3 +39,11 @@
|
|
39 |
.messageItemRight {
|
40 |
text-align: right;
|
41 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
.messageItemRight {
|
40 |
text-align: right;
|
41 |
}
|
42 |
+
|
43 |
+
.referencePopoverWrapper {
|
44 |
+
width: 50vw;
|
45 |
+
}
|
46 |
+
|
47 |
+
.referenceChunkImage {
|
48 |
+
width: 10vw;
|
49 |
+
}
|
web/src/pages/chat/chat-container/index.tsx
CHANGED
@@ -3,18 +3,38 @@ import { MessageType } from '@/constants/chat';
|
|
3 |
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
4 |
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
5 |
import { IReference, Message } from '@/interfaces/database/chat';
|
6 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
import classNames from 'classnames';
|
8 |
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
9 |
import reactStringReplace from 'react-string-replace';
|
10 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
11 |
import { IClientConversation } from '../interface';
|
12 |
|
|
|
|
|
13 |
import { InfoCircleOutlined } from '@ant-design/icons';
|
14 |
import Markdown from 'react-markdown';
|
15 |
import { visitParents } from 'unist-util-visit-parents';
|
16 |
import styles from './index.less';
|
17 |
|
|
|
|
|
|
|
|
|
18 |
const rehypeWrapReference = () => {
|
19 |
return function wrapTextTransform(tree: any) {
|
20 |
visitParents(tree, 'text', (node, ancestors) => {
|
@@ -28,32 +48,69 @@ const rehypeWrapReference = () => {
|
|
28 |
};
|
29 |
};
|
30 |
|
31 |
-
const MessageItem = ({
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
const userInfo = useSelectUserInfo();
|
33 |
|
34 |
-
const
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
42 |
);
|
43 |
|
44 |
const renderReference = useCallback(
|
45 |
(text: string) => {
|
46 |
-
return reactStringReplace(text,
|
|
|
47 |
return (
|
48 |
-
<Popover content={
|
49 |
<InfoCircleOutlined key={i} className={styles.referenceIcon} />
|
50 |
</Popover>
|
51 |
);
|
52 |
});
|
53 |
},
|
54 |
-
[
|
55 |
);
|
56 |
|
|
|
|
|
|
|
|
|
57 |
return (
|
58 |
<div
|
59 |
className={classNames(styles.messageItem, {
|
@@ -86,9 +143,7 @@ const MessageItem = ({ item }: { item: Message; references: IReference[] }) => {
|
|
86 |
<AssistantIcon></AssistantIcon>
|
87 |
)}
|
88 |
<Flex vertical gap={8} flex={1}>
|
89 |
-
<b>
|
90 |
-
{item.role === MessageType.Assistant ? 'Resume Assistant' : 'You'}
|
91 |
-
</b>
|
92 |
<div className={styles.messageText}>
|
93 |
<Markdown
|
94 |
rehypePlugins={[rehypeWrapReference]}
|
@@ -102,6 +157,22 @@ const MessageItem = ({ item }: { item: Message; references: IReference[] }) => {
|
|
102 |
{item.content}
|
103 |
</Markdown>
|
104 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
105 |
</Flex>
|
106 |
</div>
|
107 |
</section>
|
@@ -117,6 +188,8 @@ const ChatContainer = () => {
|
|
117 |
'completeConversation',
|
118 |
'getConversation',
|
119 |
]);
|
|
|
|
|
120 |
|
121 |
const handlePressEnter = () => {
|
122 |
setValue('');
|
@@ -131,14 +204,24 @@ const ChatContainer = () => {
|
|
131 |
<Flex flex={1} className={styles.chatContainer} vertical>
|
132 |
<Flex flex={1} vertical className={styles.messageContainer}>
|
133 |
<div>
|
134 |
-
{conversation?.message?.map((message) =>
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
</div>
|
|
|
142 |
</Flex>
|
143 |
<Input
|
144 |
size="large"
|
|
|
3 |
import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
|
4 |
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
5 |
import { IReference, Message } from '@/interfaces/database/chat';
|
6 |
+
import {
|
7 |
+
Avatar,
|
8 |
+
Button,
|
9 |
+
Flex,
|
10 |
+
Input,
|
11 |
+
List,
|
12 |
+
Popover,
|
13 |
+
Space,
|
14 |
+
Typography,
|
15 |
+
} from 'antd';
|
16 |
import classNames from 'classnames';
|
17 |
import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
|
18 |
import reactStringReplace from 'react-string-replace';
|
19 |
+
import {
|
20 |
+
useFetchConversation,
|
21 |
+
useGetFileIcon,
|
22 |
+
useScrollToBottom,
|
23 |
+
useSendMessage,
|
24 |
+
} from '../hooks';
|
25 |
import { IClientConversation } from '../interface';
|
26 |
|
27 |
+
import Image from '@/components/image';
|
28 |
+
import NewDocumentLink from '@/components/new-document-link';
|
29 |
import { InfoCircleOutlined } from '@ant-design/icons';
|
30 |
import Markdown from 'react-markdown';
|
31 |
import { visitParents } from 'unist-util-visit-parents';
|
32 |
import styles from './index.less';
|
33 |
|
34 |
+
const reg = /(#{2}\d+\${2})/g;
|
35 |
+
|
36 |
+
const getChunkIndex = (match: string) => Number(match.slice(2, 3));
|
37 |
+
|
38 |
const rehypeWrapReference = () => {
|
39 |
return function wrapTextTransform(tree: any) {
|
40 |
visitParents(tree, 'text', (node, ancestors) => {
|
|
|
48 |
};
|
49 |
};
|
50 |
|
51 |
+
const MessageItem = ({
|
52 |
+
item,
|
53 |
+
reference,
|
54 |
+
}: {
|
55 |
+
item: Message;
|
56 |
+
reference: IReference;
|
57 |
+
}) => {
|
58 |
const userInfo = useSelectUserInfo();
|
59 |
|
60 |
+
const isAssistant = item.role === MessageType.Assistant;
|
61 |
+
|
62 |
+
const getFileIcon = useGetFileIcon();
|
63 |
+
|
64 |
+
const getPopoverContent = useCallback(
|
65 |
+
(chunkIndex: number) => {
|
66 |
+
const chunks = reference?.chunks ?? [];
|
67 |
+
const chunkItem = chunks[chunkIndex];
|
68 |
+
const document = reference?.doc_aggs.find(
|
69 |
+
(x) => x?.doc_id === chunkItem?.doc_id,
|
70 |
+
);
|
71 |
+
const documentId = document?.doc_id;
|
72 |
+
return (
|
73 |
+
<Flex
|
74 |
+
key={chunkItem?.chunk_id}
|
75 |
+
gap={10}
|
76 |
+
className={styles.referencePopoverWrapper}
|
77 |
+
>
|
78 |
+
<Image
|
79 |
+
id={chunkItem?.img_id}
|
80 |
+
className={styles.referenceChunkImage}
|
81 |
+
></Image>
|
82 |
+
<Space direction={'vertical'}>
|
83 |
+
<div>{chunkItem?.content_with_weight}</div>
|
84 |
+
{documentId && (
|
85 |
+
<NewDocumentLink documentId={documentId}>
|
86 |
+
{document?.doc_name}
|
87 |
+
</NewDocumentLink>
|
88 |
+
)}
|
89 |
+
</Space>
|
90 |
+
</Flex>
|
91 |
+
);
|
92 |
+
},
|
93 |
+
[reference],
|
94 |
);
|
95 |
|
96 |
const renderReference = useCallback(
|
97 |
(text: string) => {
|
98 |
+
return reactStringReplace(text, reg, (match, i) => {
|
99 |
+
const chunkIndex = getChunkIndex(match);
|
100 |
return (
|
101 |
+
<Popover content={getPopoverContent(chunkIndex)}>
|
102 |
<InfoCircleOutlined key={i} className={styles.referenceIcon} />
|
103 |
</Popover>
|
104 |
);
|
105 |
});
|
106 |
},
|
107 |
+
[getPopoverContent],
|
108 |
);
|
109 |
|
110 |
+
const referenceDocumentList = useMemo(() => {
|
111 |
+
return reference?.doc_aggs ?? [];
|
112 |
+
}, [reference?.doc_aggs]);
|
113 |
+
|
114 |
return (
|
115 |
<div
|
116 |
className={classNames(styles.messageItem, {
|
|
|
143 |
<AssistantIcon></AssistantIcon>
|
144 |
)}
|
145 |
<Flex vertical gap={8} flex={1}>
|
146 |
+
<b>{isAssistant ? 'Resume Assistant' : 'You'}</b>
|
|
|
|
|
147 |
<div className={styles.messageText}>
|
148 |
<Markdown
|
149 |
rehypePlugins={[rehypeWrapReference]}
|
|
|
157 |
{item.content}
|
158 |
</Markdown>
|
159 |
</div>
|
160 |
+
{isAssistant && referenceDocumentList.length > 0 && (
|
161 |
+
<List
|
162 |
+
bordered
|
163 |
+
dataSource={referenceDocumentList}
|
164 |
+
renderItem={(item) => (
|
165 |
+
<List.Item>
|
166 |
+
<Typography.Text mark>
|
167 |
+
{/* <SvgIcon name={getFileIcon(item.doc_name)}></SvgIcon> */}
|
168 |
+
</Typography.Text>
|
169 |
+
<NewDocumentLink documentId={item.doc_id}>
|
170 |
+
{item.doc_name}
|
171 |
+
</NewDocumentLink>
|
172 |
+
</List.Item>
|
173 |
+
)}
|
174 |
+
/>
|
175 |
+
)}
|
176 |
</Flex>
|
177 |
</div>
|
178 |
</section>
|
|
|
188 |
'completeConversation',
|
189 |
'getConversation',
|
190 |
]);
|
191 |
+
const ref = useScrollToBottom();
|
192 |
+
useGetFileIcon();
|
193 |
|
194 |
const handlePressEnter = () => {
|
195 |
setValue('');
|
|
|
204 |
<Flex flex={1} className={styles.chatContainer} vertical>
|
205 |
<Flex flex={1} vertical className={styles.messageContainer}>
|
206 |
<div>
|
207 |
+
{conversation?.message?.map((message) => {
|
208 |
+
const assistantMessages = conversation?.message
|
209 |
+
?.filter((x) => x.role === MessageType.Assistant)
|
210 |
+
.slice(1);
|
211 |
+
const referenceIndex = assistantMessages.findIndex(
|
212 |
+
(x) => x.id === message.id,
|
213 |
+
);
|
214 |
+
const reference = conversation.reference[referenceIndex];
|
215 |
+
return (
|
216 |
+
<MessageItem
|
217 |
+
key={message.id}
|
218 |
+
item={message}
|
219 |
+
reference={reference}
|
220 |
+
></MessageItem>
|
221 |
+
);
|
222 |
+
})}
|
223 |
</div>
|
224 |
+
<div ref={ref} />
|
225 |
</Flex>
|
226 |
<Input
|
227 |
size="large"
|
web/src/pages/chat/hooks.ts
CHANGED
@@ -1,8 +1,10 @@
|
|
1 |
import showDeleteConfirm from '@/components/deleting-confirm';
|
2 |
import { MessageType } from '@/constants/chat';
|
|
|
3 |
import { IConversation, IDialog } from '@/interfaces/database/chat';
|
|
|
4 |
import omit from 'lodash/omit';
|
5 |
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
6 |
import { useDispatch, useSearchParams, useSelector } from 'umi';
|
7 |
import { v4 as uuid } from 'uuid';
|
8 |
import { ChatSearchParams, EmptyConversationId } from './constants';
|
@@ -441,4 +443,38 @@ export const useSendMessage = () => {
|
|
441 |
return { sendMessage: handleSendMessage };
|
442 |
};
|
443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
444 |
//#endregion
|
|
|
1 |
import showDeleteConfirm from '@/components/deleting-confirm';
|
2 |
import { MessageType } from '@/constants/chat';
|
3 |
+
import { fileIconMap } from '@/constants/common';
|
4 |
import { IConversation, IDialog } from '@/interfaces/database/chat';
|
5 |
+
import { getFileExtension } from '@/utils';
|
6 |
import omit from 'lodash/omit';
|
7 |
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
8 |
import { useDispatch, useSearchParams, useSelector } from 'umi';
|
9 |
import { v4 as uuid } from 'uuid';
|
10 |
import { ChatSearchParams, EmptyConversationId } from './constants';
|
|
|
443 |
return { sendMessage: handleSendMessage };
|
444 |
};
|
445 |
|
446 |
+
export const useScrollToBottom = () => {
|
447 |
+
const ref = useRef<HTMLDivElement>(null);
|
448 |
+
let chatModel: ChatModelState = useSelector((state: any) => state.chatModel);
|
449 |
+
const { currentConversation } = chatModel;
|
450 |
+
|
451 |
+
const scrollToBottom = useCallback(() => {
|
452 |
+
if (currentConversation.id) {
|
453 |
+
ref.current?.scrollIntoView({ behavior: 'instant' });
|
454 |
+
}
|
455 |
+
}, [currentConversation]);
|
456 |
+
|
457 |
+
useEffect(() => {
|
458 |
+
scrollToBottom();
|
459 |
+
}, [scrollToBottom]);
|
460 |
+
|
461 |
+
return ref;
|
462 |
+
};
|
463 |
+
|
464 |
+
export const useGetFileIcon = () => {
|
465 |
+
// const req = require.context('@/assets/svg/file-icon');
|
466 |
+
// const ret = req.keys().map(req);
|
467 |
+
// console.info(ret);
|
468 |
+
// useEffect(() => {}, []);
|
469 |
+
|
470 |
+
const getFileIcon = (filename: string) => {
|
471 |
+
const ext: string = getFileExtension(filename);
|
472 |
+
const iconPath = fileIconMap[ext as keyof typeof fileIconMap];
|
473 |
+
// const x = require(`@/assets/svg/file-icon/${iconPath}`);
|
474 |
+
return `@/assets/svg/file-icon/${iconPath}`;
|
475 |
+
};
|
476 |
+
|
477 |
+
return getFileIcon;
|
478 |
+
};
|
479 |
+
|
480 |
//#endregion
|
web/src/utils/domUtils.ts
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
export const scrollToBottom = (element: HTMLElement) => {
|
2 |
+
element.scrollTo(0, element.scrollHeight);
|
3 |
+
};
|