user feedback and admin mark (#228)
* fix: csv empty data * feat: user feedback and mark answer * version intro * perf: chat logs sort
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { Chat, ChatItem, connectToDatabase } from '@/service/mongo';
|
||||
import { Chat, connectToDatabase } from '@/service/mongo';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
import type { PagingData } from '@/types';
|
||||
import { AppLogsListItemType } from '@/types/app';
|
||||
@@ -30,25 +30,48 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const [data, total] = await Promise.all([
|
||||
Chat.aggregate([
|
||||
{ $match: where },
|
||||
{ $sort: { updateTime: -1 } },
|
||||
{ $skip: (pageNum - 1) * pageSize },
|
||||
{ $limit: pageSize },
|
||||
{
|
||||
$lookup: {
|
||||
from: 'chatitems',
|
||||
localField: 'chatId',
|
||||
foreignField: 'chatId',
|
||||
as: 'messageCount'
|
||||
as: 'chatitems'
|
||||
}
|
||||
},
|
||||
{
|
||||
$addFields: {
|
||||
feedbackCount: {
|
||||
$size: {
|
||||
$filter: {
|
||||
input: '$chatitems',
|
||||
as: 'item',
|
||||
cond: { $ifNull: ['$$item.userFeedback', false] }
|
||||
}
|
||||
}
|
||||
},
|
||||
markCount: {
|
||||
$size: {
|
||||
$filter: {
|
||||
input: '$chatitems',
|
||||
as: 'item',
|
||||
cond: { $ifNull: ['$$item.adminFeedback', false] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ $sort: { feedbackCount: -1, updateTime: -1 } },
|
||||
{ $skip: (pageNum - 1) * pageSize },
|
||||
{ $limit: pageSize },
|
||||
{
|
||||
$project: {
|
||||
id: '$chatId',
|
||||
title: 1,
|
||||
source: 1,
|
||||
time: '$updateTime',
|
||||
messageCount: { $size: '$messageCount' },
|
||||
callbackCount: { $literal: 0 }
|
||||
messageCount: { $size: '$chatitems' },
|
||||
feedbackCount: 1,
|
||||
markCount: 1
|
||||
}
|
||||
}
|
||||
]),
|
||||
|
||||
40
client/src/pages/api/chat/feedback/adminUpdate.ts
Normal file
40
client/src/pages/api/chat/feedback/adminUpdate.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ChatItem } from '@/service/mongo';
|
||||
import { AdminUpdateFeedbackParams } from '@/api/request/chat';
|
||||
import { authUser } from '@/service/utils/auth';
|
||||
|
||||
/* 初始化我的聊天框,需要身份验证 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { chatItemId, kbId, dataId, content = undefined } = req.body as AdminUpdateFeedbackParams;
|
||||
|
||||
if (!chatItemId || !kbId || !dataId || !content) {
|
||||
throw new Error('missing parameter');
|
||||
}
|
||||
|
||||
const { userId } = await authUser({ req, authToken: true });
|
||||
|
||||
await ChatItem.findOneAndUpdate(
|
||||
{
|
||||
userId,
|
||||
dataId: chatItemId
|
||||
},
|
||||
{
|
||||
adminFeedback: {
|
||||
kbId,
|
||||
dataId,
|
||||
content
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
34
client/src/pages/api/chat/feedback/userUpdate.ts
Normal file
34
client/src/pages/api/chat/feedback/userUpdate.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, ChatItem } from '@/service/mongo';
|
||||
|
||||
/* 初始化我的聊天框,需要身份验证 */
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
const { chatItemId, userFeedback = undefined } = req.body as {
|
||||
chatItemId: string;
|
||||
userFeedback?: string;
|
||||
};
|
||||
|
||||
if (!chatItemId) {
|
||||
throw new Error('chatItemId is required');
|
||||
}
|
||||
|
||||
await ChatItem.findOneAndUpdate(
|
||||
{
|
||||
dataId: chatItemId
|
||||
},
|
||||
{
|
||||
...(userFeedback ? { userFeedback } : { $unset: { userFeedback: '' } })
|
||||
}
|
||||
);
|
||||
|
||||
jsonRes(res);
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
chatId,
|
||||
userId
|
||||
},
|
||||
`dataId obj value ${TaskResponseKeyEnum.responseData}`
|
||||
`dataId obj value adminFeedback userFeedback ${TaskResponseKeyEnum.responseData}`
|
||||
)
|
||||
.sort({ _id: -1 })
|
||||
.limit(30)
|
||||
|
||||
@@ -25,7 +25,10 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex
|
||||
await connectToDatabase();
|
||||
|
||||
// auth user and get kb
|
||||
const [{ userId }, kb] = await Promise.all([authUser({ req }), KB.findById(kbId, 'model')]);
|
||||
const [{ userId }, kb] = await Promise.all([
|
||||
authUser({ req }),
|
||||
KB.findById(kbId, 'vectorModel')
|
||||
]);
|
||||
|
||||
if (!kb) {
|
||||
throw new Error("Can't find database");
|
||||
|
||||
88
client/src/pages/api/plugins/kb/data/insertData.ts
Normal file
88
client/src/pages/api/plugins/kb/data/insertData.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
import { jsonRes } from '@/service/response';
|
||||
import { connectToDatabase, KB } from '@/service/mongo';
|
||||
import { authKb, authUser } from '@/service/utils/auth';
|
||||
import { withNextCors } from '@/service/utils/tools';
|
||||
import { PgTrainingTableName } from '@/constants/plugin';
|
||||
import { insertKbItem, PgClient } from '@/service/pg';
|
||||
import { modelToolMap } from '@/utils/plugin';
|
||||
import { getVectorModel } from '@/service/utils/data';
|
||||
import { getVector } from '@/pages/api/openapi/plugin/vector';
|
||||
|
||||
export type Props = {
|
||||
kbId: string;
|
||||
data: { a: string; q: string; source?: string };
|
||||
};
|
||||
|
||||
export default withNextCors(async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
|
||||
try {
|
||||
await connectToDatabase();
|
||||
|
||||
const { kbId, data = { q: '', a: '' } } = req.body as Props;
|
||||
|
||||
if (!kbId || !data?.q) {
|
||||
throw new Error('缺少参数');
|
||||
}
|
||||
|
||||
// 凭证校验
|
||||
const { userId } = await authUser({ req });
|
||||
|
||||
// auth kb
|
||||
const kb = await authKb({ kbId, userId });
|
||||
|
||||
const q = data?.q?.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
const a = data?.a?.replace(/\\n/g, '\n').trim().replace(/'/g, '"');
|
||||
|
||||
// token check
|
||||
const token = modelToolMap.countTokens({
|
||||
model: 'gpt-3.5-turbo',
|
||||
messages: [{ obj: 'System', value: q }]
|
||||
});
|
||||
|
||||
if (token > getVectorModel(kb.vectorModel).maxToken) {
|
||||
throw new Error('Over Tokens');
|
||||
}
|
||||
|
||||
const { rows: existsRows } = await PgClient.query(`
|
||||
SELECT COUNT(*) > 0 AS exists
|
||||
FROM ${PgTrainingTableName}
|
||||
WHERE md5(q)=md5('${q}') AND md5(a)=md5('${a}') AND user_id='${userId}' AND kb_id='${kbId}'
|
||||
`);
|
||||
const exists = existsRows[0]?.exists || false;
|
||||
|
||||
if (exists) {
|
||||
throw new Error('已经存在完全一致的数据');
|
||||
}
|
||||
|
||||
const { vectors } = await getVector({
|
||||
model: kb.vectorModel,
|
||||
input: [q],
|
||||
userId
|
||||
});
|
||||
|
||||
const response = await insertKbItem({
|
||||
userId,
|
||||
kbId,
|
||||
data: [
|
||||
{
|
||||
q,
|
||||
a,
|
||||
source: data.source,
|
||||
vector: vectors[0]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
const id = response?.rows?.[0]?.id || '';
|
||||
|
||||
jsonRes(res, {
|
||||
data: id
|
||||
});
|
||||
} catch (err) {
|
||||
jsonRes(res, {
|
||||
code: 500,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
Th,
|
||||
Td,
|
||||
Tbody,
|
||||
useTheme
|
||||
useTheme,
|
||||
useDisclosure,
|
||||
ModalBody
|
||||
} from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -24,11 +26,18 @@ import ChatBox, { type ComponentRef } from '@/components/ChatBox';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getInitChatSiteInfo } from '@/api/chat';
|
||||
import Tag from '@/components/Tag';
|
||||
import MyModal from '@/components/MyModal';
|
||||
|
||||
const Logs = ({ appId }: { appId: string }) => {
|
||||
const { t } = useTranslation();
|
||||
const { isPc } = useGlobalStore();
|
||||
|
||||
const {
|
||||
isOpen: isOpenMarkDesc,
|
||||
onOpen: onOpenMarkDesc,
|
||||
onClose: onCloseMarkDesc
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
data: logs,
|
||||
isLoading,
|
||||
@@ -54,7 +63,16 @@ const Logs = ({ appId }: { appId: string }) => {
|
||||
{t('app.Chat logs')}
|
||||
</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{t('app.Chat Logs Tips')}
|
||||
{t('app.Chat Logs Tips')},{' '}
|
||||
<Box
|
||||
as={'span'}
|
||||
mr={2}
|
||||
textDecoration={'underline'}
|
||||
cursor={'pointer'}
|
||||
onClick={onOpenMarkDesc}
|
||||
>
|
||||
{t('chat.Read Mark Description')}
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
@@ -69,6 +87,8 @@ const Logs = ({ appId }: { appId: string }) => {
|
||||
<Th>{t('app.Logs Time')}</Th>
|
||||
<Th>{t('app.Logs Title')}</Th>
|
||||
<Th>{t('app.Logs Message Total')}</Th>
|
||||
<Th>{t('app.Feedback Count')}</Th>
|
||||
<Th>{t('app.Mark Count')}</Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
@@ -86,6 +106,27 @@ const Logs = ({ appId }: { appId: string }) => {
|
||||
{item.title}
|
||||
</Td>
|
||||
<Td>{item.messageCount}</Td>
|
||||
<Td w={'100px'}>
|
||||
{!!item?.feedbackCount ? (
|
||||
<Box display={'inline-block'}>
|
||||
<Flex
|
||||
bg={'#FFF2EC'}
|
||||
color={'#C96330'}
|
||||
px={3}
|
||||
py={1}
|
||||
alignItems={'center'}
|
||||
borderRadius={'lg'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
<MyIcon mr={1} name={'badLight'} color={'#C96330'} w={'14px'} />
|
||||
{item.feedbackCount}
|
||||
</Flex>
|
||||
</Box>
|
||||
) : (
|
||||
<>-</>
|
||||
)}
|
||||
</Td>
|
||||
<Td>{item.markCount}</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
@@ -109,6 +150,13 @@ const Logs = ({ appId }: { appId: string }) => {
|
||||
onClose={() => setDetailLogsId(undefined)}
|
||||
/>
|
||||
)}
|
||||
<MyModal
|
||||
isOpen={isOpenMarkDesc}
|
||||
onClose={onCloseMarkDesc}
|
||||
title={t('chat.Mark Description Title')}
|
||||
>
|
||||
<ModalBody whiteSpace={'pre-wrap'}>{t('chat.Mark Description')}</ModalBody>
|
||||
</MyModal>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
@@ -222,6 +270,7 @@ function DetailLogsModal({
|
||||
<Box pt={2} flex={'1 0 0'}>
|
||||
<ChatBox
|
||||
ref={ChatBoxRef}
|
||||
isLogs
|
||||
chatId={chatId}
|
||||
appAvatar={chat?.app.avatar}
|
||||
userAvatar={HUMAN_ICON}
|
||||
|
||||
@@ -38,7 +38,10 @@ const CsvImport = ({ kbId }: { kbId: string }) => {
|
||||
|
||||
const { mutate: onclickUpload, isLoading: uploading } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const chunks = files.map((file) => file.chunks).flat();
|
||||
const chunks = files
|
||||
.map((file) => file.chunks)
|
||||
.flat()
|
||||
.filter((item) => item?.q);
|
||||
|
||||
const filterChunks = chunks.filter((item) => item.q.length < maxToken);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Textarea, Button } from '@chakra-ui/react';
|
||||
import { Box, Textarea, Button, Flex } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
@@ -7,6 +7,8 @@ import { getErrText } from '@/utils/tools';
|
||||
import { postKbDataFromList } from '@/api/plugins/kb';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
|
||||
type ManualFormType = { q: string; a: string };
|
||||
|
||||
@@ -70,7 +72,12 @@ const ManualImport = ({ kbId }: { kbId: string }) => {
|
||||
<Box p={[4, 8]} h={'100%'} overflow={'overlay'}>
|
||||
<Box display={'flex'} flexDirection={['column', 'row']}>
|
||||
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['50%', '100%']} position={'relative'}>
|
||||
<Box h={'30px'}>{'匹配的知识点'}</Box>
|
||||
<Flex>
|
||||
<Box h={'30px'}>{'匹配的知识点'}</Box>
|
||||
<MyTooltip label={'被向量化的部分,通常是问题,也可以是一段陈述描述'}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
placeholder={`匹配的知识点。这部分内容会被搜索,请把控内容的质量。最多 ${maxToken} 字。`}
|
||||
maxLength={maxToken}
|
||||
@@ -87,7 +94,14 @@ const ManualImport = ({ kbId }: { kbId: string }) => {
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex={1} h={['50%', '100%']}>
|
||||
<Box h={'30px'}>补充知识</Box>
|
||||
<Flex>
|
||||
<Box h={'30px'}>{'补充知识'}</Box>
|
||||
<MyTooltip
|
||||
label={'匹配的知识点被命中后,这部分内容会随匹配知识点一起注入模型,引导模型回答'}
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
placeholder={
|
||||
'补充知识。这部分内容不会被搜索,但会作为"匹配的知识点"的内容补充,你可以讲一些细节的内容填写在这里。总和最多 3000 字。'
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { Box, Flex, Button, Textarea, IconButton } from '@chakra-ui/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { postKbDataFromList, putKbDataById, delOneKbDataByDataId } from '@/api/plugins/kb';
|
||||
import { insertData2Kb, putKbDataById, delOneKbDataByDataId } from '@/api/plugins/kb';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { TrainingModeEnum } from '@/constants/plugin';
|
||||
import { getErrText } from '@/utils/tools';
|
||||
import { vectorModelList } from '@/store/static';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MyModal from '@/components/MyModal';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export type FormData = { dataId?: string; a: string; q: string };
|
||||
export type FormData = { dataId?: string; a: string; q: string; source?: string };
|
||||
|
||||
const InputDataModal = ({
|
||||
onClose,
|
||||
@@ -30,16 +32,20 @@ const InputDataModal = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { kbDetail, getKbDetail } = useUserStore();
|
||||
|
||||
const { register, handleSubmit, reset } = useForm<FormData>({
|
||||
defaultValues
|
||||
});
|
||||
|
||||
const maxToken = kbDetail.vectorModel?.maxToken || 2000;
|
||||
|
||||
/**
|
||||
* 确认导入新数据
|
||||
*/
|
||||
const sureImportData = useCallback(
|
||||
async (e: FormData) => {
|
||||
if (e.a.length + e.q.length >= 3000) {
|
||||
if (e.q.length >= maxToken) {
|
||||
toast({
|
||||
title: '总长度超长了',
|
||||
status: 'warning'
|
||||
@@ -50,31 +56,24 @@ const InputDataModal = ({
|
||||
|
||||
try {
|
||||
const data = {
|
||||
dataId: '',
|
||||
a: e.a,
|
||||
q: e.q,
|
||||
source: '手动录入'
|
||||
};
|
||||
const { insertLen } = await postKbDataFromList({
|
||||
data.dataId = await insertData2Kb({
|
||||
kbId,
|
||||
mode: TrainingModeEnum.index,
|
||||
data: [data]
|
||||
data
|
||||
});
|
||||
|
||||
if (insertLen === 0) {
|
||||
toast({
|
||||
title: '已存在完全一致的数据',
|
||||
status: 'warning'
|
||||
});
|
||||
} else {
|
||||
toast({
|
||||
title: '导入数据成功,需要一段时间训练',
|
||||
status: 'success'
|
||||
});
|
||||
reset({
|
||||
a: '',
|
||||
q: ''
|
||||
});
|
||||
}
|
||||
toast({
|
||||
title: '导入数据成功,需要一段时间训练',
|
||||
status: 'success'
|
||||
});
|
||||
reset({
|
||||
a: '',
|
||||
q: ''
|
||||
});
|
||||
|
||||
onSuccess(data);
|
||||
} catch (err: any) {
|
||||
@@ -85,7 +84,7 @@ const InputDataModal = ({
|
||||
}
|
||||
setLoading(false);
|
||||
},
|
||||
[kbId, onSuccess, reset, toast]
|
||||
[kbId, maxToken, onSuccess, reset, toast]
|
||||
);
|
||||
|
||||
const updateData = useCallback(
|
||||
@@ -121,6 +120,11 @@ const InputDataModal = ({
|
||||
[defaultValues.a, defaultValues.q, kbId, onClose, onSuccess, toast]
|
||||
);
|
||||
|
||||
useQuery(['getKbDetail'], () => {
|
||||
if (kbDetail._id === kbId) return null;
|
||||
return getKbDetail(kbId);
|
||||
});
|
||||
|
||||
return (
|
||||
<MyModal
|
||||
isOpen={true}
|
||||
@@ -142,10 +146,15 @@ const InputDataModal = ({
|
||||
pb={2}
|
||||
>
|
||||
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['50%', '100%']}>
|
||||
<Box h={'30px'}>{'匹配的知识点'}</Box>
|
||||
<Flex>
|
||||
<Box h={'30px'}>{'匹配的知识点'}</Box>
|
||||
<MyTooltip label={'被向量化的部分,通常是问题,也可以是一段陈述描述'}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
placeholder={'匹配的知识点。这部分内容会被搜索,请把控内容的质量。总和最多 3000 字。'}
|
||||
maxLength={3000}
|
||||
placeholder={`匹配的知识点。这部分内容会被搜索,请把控内容的质量,最多 ${maxToken} 字。`}
|
||||
maxLength={maxToken}
|
||||
resize={'none'}
|
||||
h={'calc(100% - 30px)'}
|
||||
{...register(`q`, {
|
||||
@@ -154,7 +163,14 @@ const InputDataModal = ({
|
||||
/>
|
||||
</Box>
|
||||
<Box flex={1} h={['50%', '100%']}>
|
||||
<Box h={'30px'}>补充知识</Box>
|
||||
<Flex>
|
||||
<Box h={'30px'}>{'补充知识'}</Box>
|
||||
<MyTooltip
|
||||
label={'匹配的知识点被命中后,这部分内容会随匹配知识点一起注入模型,引导模型回答'}
|
||||
>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Textarea
|
||||
placeholder={
|
||||
'补充知识。这部分内容不会被搜索,但会作为"匹配的知识点"的内容补充,你可以讲一些细节的内容填写在这里。总和最多 3000 字。'
|
||||
|
||||
Reference in New Issue
Block a user