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:
Archer
2023-08-27 23:12:06 +08:00
committed by GitHub
parent 2556c19a9a
commit 5fcdf28c5c
32 changed files with 886 additions and 67 deletions

View File

@@ -5,6 +5,7 @@ import { RequestPaging } from '../types/index';
import type { OutLinkSchema } from '@/types/mongoSchema';
import type { ShareChatEditType } from '@/types/app';
import type { Props as UpdateHistoryProps } from '@/pages/api/chat/history/updateChatHistory';
import { AdminUpdateFeedbackParams } from './request/chat';
/**
* 获取初始化聊天内容
@@ -64,3 +65,9 @@ export const getShareChatList = (appId: string) =>
* delete a shareChat
*/
export const delShareChatById = (id: string) => DELETE(`/chat/shareChat/delete?id=${id}`);
export const userUpdateChatFeedback = (data: { chatItemId: string; userFeedback?: string }) =>
POST('/chat/feedback/userUpdate', data);
export const adminUpdateChatFeedback = (data: AdminUpdateFeedbackParams) =>
POST('/chat/feedback/adminUpdate', data);

View File

@@ -66,6 +66,14 @@ export const getKbDataItemById = (dataId: string) =>
export const postKbDataFromList = (data: PushDataProps) =>
POST<PushDateResponse>(`/openapi/kb/pushData`, data);
/**
* insert one data to dataset
*/
export const insertData2Kb = (data: {
kbId: string;
data: { a: string; q: string; source?: string };
}) => POST<string>(`/plugins/kb/data/insertData`, data);
/**
* 更新一条数据
*/

6
client/src/api/request/chat.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
export type AdminUpdateFeedbackParams = {
chatItemId: string;
kbId: string;
dataId: string;
content: string;
};

View File

@@ -0,0 +1,56 @@
import React, { useRef } from 'react';
import { ModalBody, Textarea, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '../MyModal';
import { useRequest } from '@/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { userUpdateChatFeedback } from '@/api/chat';
const FeedbackModal = ({
chatItemId,
onSuccess,
onClose
}: {
chatItemId: string;
onSuccess: (e: string) => void;
onClose: () => void;
}) => {
const ref = useRef<HTMLTextAreaElement>(null);
const { t } = useTranslation();
const { mutate, isLoading } = useRequest({
mutationFn: async () => {
const val = ref.current?.value || 'N/A';
return userUpdateChatFeedback({
chatItemId,
userFeedback: val
});
},
onSuccess() {
onSuccess(ref.current?.value || 'N/A');
},
successToast: t('chat.Feedback Success'),
errorToast: t('chat.Feedback Failed')
});
return (
<MyModal isOpen={true} onClose={onClose} title={t('chat.Feedback Modal')}>
<ModalBody>
<Textarea
ref={ref}
rows={10}
placeholder={t('chat.Feedback Modal Tip') || 'chat.Feedback Modal Tip'}
/>
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={2} onClick={onClose}>
{t('Cancel')}
</Button>
<Button isLoading={isLoading} onClick={mutate}>
{t('chat.Feedback Submit')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default FeedbackModal;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { ModalBody, ModalFooter, Button } from '@chakra-ui/react';
import MyModal from '../MyModal';
import { useRequest } from '@/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { userUpdateChatFeedback } from '@/api/chat';
const ReadFeedbackModal = ({
chatItemId,
content,
isMarked,
onMark,
onSuccess,
onClose
}: {
chatItemId: string;
content: string;
isMarked: boolean;
onMark: () => void;
onSuccess: () => void;
onClose: () => void;
}) => {
const { t } = useTranslation();
const { mutate, isLoading } = useRequest({
mutationFn: async () => {
return userUpdateChatFeedback({
chatItemId,
userFeedback: undefined
});
},
onSuccess() {
onSuccess();
},
errorToast: t('chat.Feedback Update Failed')
});
return (
<MyModal isOpen={true} onClose={onClose} title={t('chat.Feedback Modal')}>
<ModalBody>{content}</ModalBody>
<ModalFooter>
{!isMarked && (
<Button variant={'base'} mr={2} onClick={onMark}>
{t('chat.Feedback Mark')}
</Button>
)}
<Button isLoading={isLoading} onClick={mutate}>
{t('chat.Feedback Close')}
</Button>
</ModalFooter>
</MyModal>
);
};
export default React.memo(ReadFeedbackModal);

View File

@@ -0,0 +1,117 @@
import React, { useRef, useState } from 'react';
import {
ModalBody,
useTheme,
ModalFooter,
Button,
ModalHeader,
Box,
Card,
Flex
} from '@chakra-ui/react';
import MyModal from '../MyModal';
import { useTranslation } from 'next-i18next';
import { useQuery } from '@tanstack/react-query';
import { useUserStore } from '@/store/user';
import { useToast } from '@/hooks/useToast';
import Avatar from '../Avatar';
import MyIcon from '@/components/Icon';
import { useGlobalStore } from '@/store/global';
const SelectDataset = ({
onSuccess,
onClose
}: {
onSuccess: (kbId: string) => void;
onClose: () => void;
}) => {
const { t } = useTranslation();
const theme = useTheme();
const { isPc } = useGlobalStore();
const { toast } = useToast();
const { myKbList, loadKbList } = useUserStore();
const [selectedId, setSelectedId] = useState<string>();
useQuery(['loadKbList'], loadKbList);
return (
<MyModal isOpen={true} onClose={onClose} w={'100%'} maxW={['90vw', '900px']} isCentered={!isPc}>
<Flex flexDirection={'column'} h={['90vh', 'auto']}>
<ModalHeader>
<Box>{t('chat.Select Mark Kb')}</Box>
<Box fontSize={'sm'} color={'myGray.500'} fontWeight={'normal'}>
{t('chat.Select Mark Kb Desc')}
</Box>
</ModalHeader>
<ModalBody
flex={['1 0 0', '0 0 auto']}
maxH={'80vh'}
overflowY={'auto'}
display={'grid'}
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
gridGap={3}
userSelect={'none'}
>
{myKbList.map((item) =>
(() => {
const selected = selectedId === item._id;
return (
<Card
key={item._id}
p={3}
border={theme.borders.base}
boxShadow={'sm'}
h={'80px'}
cursor={'pointer'}
_hover={{
boxShadow: 'md'
}}
{...(selected
? {
bg: 'myBlue.300'
}
: {})}
onClick={() => {
setSelectedId(item._id);
}}
>
<Flex alignItems={'center'} h={'38px'}>
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
{item.name}
</Box>
</Flex>
<Flex justifyContent={'flex-end'} alignItems={'center'} fontSize={'sm'}>
<MyIcon mr={1} name="kbTest" w={'12px'} />
<Box color={'myGray.500'}>{item.vectorModel.name}</Box>
</Flex>
</Card>
);
})()
)}
</ModalBody>
<ModalFooter>
<Button variant={'base'} mr={2} onClick={onClose}>
{t('Cancel')}
</Button>
<Button
onClick={() => {
if (!selectedId) {
return toast({
status: 'warning',
title: t('Select value is empty')
});
}
onSuccess(selectedId);
}}
>
{t('Confirm')}
</Button>
</ModalFooter>
</Flex>
</MyModal>
);
};
export default SelectDataset;

View File

@@ -25,7 +25,6 @@ import {
} from '@/utils/tools';
import { Box, Card, Flex, Input, Textarea, Button, useTheme, BoxProps } from '@chakra-ui/react';
import { feConfigs } from '@/store/static';
import { Types } from 'mongoose';
import { EventNameEnum } from '../Markdown/constant';
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
@@ -41,6 +40,7 @@ import { useGlobalStore } from '@/store/global';
import { TaskResponseKeyEnum, getDefaultChatVariables } from '@/constants/chat';
import { useTranslation } from 'react-i18next';
import { customAlphabet } from 'nanoid';
import { userUpdateChatFeedback, adminUpdateChatFeedback } from '@/api/chat';
import MyIcon from '@/components/Icon';
import Avatar from '@/components/Avatar';
@@ -49,6 +49,10 @@ import MySelect from '@/components/Select';
import MyTooltip from '../MyTooltip';
import dynamic from 'next/dynamic';
const ResponseTags = dynamic(() => import('./ResponseTags'));
const FeedbackModal = dynamic(() => import('./FeedbackModal'));
const ReadFeedbackModal = dynamic(() => import('./ReadFeedbackModal'));
const SelectDataset = dynamic(() => import('./SelectDataset'));
const InputDataModal = dynamic(() => import('@/pages/kb/detail/components/InputDataModal'));
import styles from './index.module.scss';
@@ -124,6 +128,7 @@ const ChatAvatar = ({ src, type }: { src?: string; type: 'Human' | 'AI' }) => {
const ChatBox = (
{
isLogs = false,
showEmptyIntro = false,
chatId,
appAvatar,
@@ -134,6 +139,7 @@ const ChatBox = (
onStartChat,
onDelMessage
}: {
isLogs?: boolean;
showEmptyIntro?: boolean;
chatId?: string;
appAvatar?: string;
@@ -162,6 +168,19 @@ const ChatBox = (
const [refresh, setRefresh] = useState(false);
const [variables, setVariables] = useState<Record<string, any>>({});
const [chatHistory, setChatHistory] = useState<ChatSiteItemType[]>([]);
const [feedbackId, setFeedbackId] = useState<string>();
const [readFeedbackData, setReadFeedbackData] = useState<{
chatItemId: string;
content: string;
isMarked: boolean;
}>();
const [adminMarkData, setAdminMarkData] = useState<{
kbId?: string;
chatItemId: string;
dataId?: string;
q: string;
a: string;
}>();
const isChatting = useMemo(
() =>
@@ -409,7 +428,7 @@ const ChatBox = (
const controlContainerStyle = {
className: 'control',
color: 'myGray.400',
display: ['flex', 'none'],
display: isLogs ? 'flex' : ['flex', 'none'],
pl: 1,
mt: 2
};
@@ -643,7 +662,7 @@ const ChatBox = (
/>
</MyTooltip>
)}
{hasVoiceApi && (
{!isLogs && hasVoiceApi && (
<MyTooltip label={'语音播报'}>
<MyIcon
{...controlIconStyle}
@@ -653,7 +672,93 @@ const ChatBox = (
/>
</MyTooltip>
)}
{/* admin mark icon */}
{isLogs && (
<MyTooltip label={t('chat.Mark')}>
<MyIcon
{...controlIconStyle}
name={'markLight'}
_hover={{ color: '#67c13b' }}
onClick={() => {
if (!item.dataId) return;
if (item.adminFeedback) {
setAdminMarkData({
chatItemId: item.dataId,
kbId: item.adminFeedback.kbId,
dataId: item.adminFeedback.dataId,
q: chatHistory[index - 1]?.value || '',
a: item.adminFeedback.content
});
} else {
setAdminMarkData({
chatItemId: item.dataId,
q: chatHistory[index - 1]?.value || '',
a: item.value
});
}
}}
/>
</MyTooltip>
)}
{/* user feed back icon */}
{item.dataId &&
(isLogs ? (
<MyTooltip label={t('chat.Read User Feedback')}>
<MyIcon
display={item.userFeedback ? 'block' : 'none'}
{...controlIconStyle}
color={'white'}
bg={'#FC9663'}
fontWeight={'bold'}
name={'badLight'}
onClick={() =>
setReadFeedbackData({
chatItemId: item.dataId || '',
content: item.userFeedback || '',
isMarked: !!item.adminFeedback
})
}
/>
</MyTooltip>
) : (
<MyTooltip
label={
item.userFeedback
? `取消反馈。\n您当前反馈内容为:\n${item.userFeedback}`
: '反馈'
}
>
<MyIcon
{...controlIconStyle}
{...(!!item.userFeedback
? {
color: 'white',
bg: '#FC9663',
fontWeight: 'bold',
onClick: () => {
if (!item.dataId) return;
setChatHistory((state) =>
state.map((chatItem) =>
chatItem.dataId === item.dataId
? { ...chatItem, userFeedback: undefined }
: chatItem
)
);
try {
userUpdateChatFeedback({ chatItemId: item.dataId });
} catch (error) {}
}
}
: {
_hover: { color: '#FB7C3C' },
onClick: () => setFeedbackId(item.dataId)
})}
name={'badLight'}
/>
</MyTooltip>
))}
</Flex>
{/* chatting status */}
{statusBoxData && index === chatHistory.length - 1 && (
<Flex
ml={3}
@@ -688,6 +793,19 @@ const ChatBox = (
contentId={item.dataId}
responseData={item.responseData}
/>
{/* admin mark content */}
{isLogs && item.adminFeedback && (
<Box>
<Flex alignItems={'center'} py={2}>
<MyIcon name={'markLight'} w={'14px'} color={'myGray.900'} />
<Box ml={2} color={'myGray.500'}>
{t('chat.Admin Mark Content')}
</Box>
<Box h={'1px'} bg={'myGray.300'} flex={'1'} />
</Flex>
<Box>{item.adminFeedback.content}</Box>
</Box>
)}
</Card>
</Box>
</>
@@ -782,6 +900,115 @@ const ChatBox = (
</Box>
</Box>
) : null}
{/* user feedback modal */}
{!!feedbackId && (
<FeedbackModal
chatItemId={feedbackId}
onClose={() => setFeedbackId(undefined)}
onSuccess={(content: string) => {
setChatHistory((state) =>
state.map((item) =>
item.dataId === feedbackId ? { ...item, userFeedback: content } : item
)
);
setFeedbackId(undefined);
}}
/>
)}
{/* admin read feedback modal */}
{!!readFeedbackData && (
<ReadFeedbackModal
{...readFeedbackData}
onClose={() => setReadFeedbackData(undefined)}
onMark={() => {
const index = chatHistory.findIndex(
(item) => item.dataId === readFeedbackData.chatItemId
);
if (index === -1) return setReadFeedbackData(undefined);
setAdminMarkData({
chatItemId: readFeedbackData.chatItemId,
q: chatHistory[index - 1]?.value || '',
a: chatHistory[index]?.value || ''
});
}}
onSuccess={() => {
setChatHistory((state) =>
state.map((chatItem) =>
chatItem.dataId === readFeedbackData.chatItemId
? { ...chatItem, userFeedback: undefined }
: chatItem
)
);
setReadFeedbackData(undefined);
}}
/>
)}
{/* select one dataset to insert markData */}
{adminMarkData && !adminMarkData.kbId && (
<SelectDataset
onClose={() => setAdminMarkData(undefined)}
// @ts-ignore
onSuccess={(kbId) => setAdminMarkData((state) => ({ ...state, kbId }))}
/>
)}
{/* edit markData modal */}
{adminMarkData && adminMarkData.kbId && (
<InputDataModal
onClose={() => setAdminMarkData(undefined)}
onSuccess={async (data) => {
if (!adminMarkData.kbId || !data.dataId) {
return setAdminMarkData(undefined);
}
const adminFeedback = {
kbId: adminMarkData.kbId,
dataId: data.dataId,
content: data.a
};
// update dom
setChatHistory((state) =>
state.map((chatItem) =>
chatItem.dataId === adminMarkData.chatItemId
? {
...chatItem,
adminFeedback
}
: chatItem
)
);
// request to update adminFeedback
try {
adminUpdateChatFeedback({
chatItemId: adminMarkData.chatItemId,
...adminFeedback
});
if (readFeedbackData) {
userUpdateChatFeedback({
chatItemId: readFeedbackData.chatItemId,
userFeedback: undefined
});
setChatHistory((state) =>
state.map((chatItem) =>
chatItem.dataId === readFeedbackData.chatItemId
? { ...chatItem, userFeedback: undefined }
: chatItem
)
);
setReadFeedbackData(undefined);
}
} catch (error) {}
setAdminMarkData(undefined);
}}
kbId={adminMarkData.kbId}
defaultValues={{
dataId: adminMarkData.dataId,
q: adminMarkData.q,
a: adminMarkData.a
}}
/>
)}
</Flex>
);
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1693123058703" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="16328" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M875.65 912h-380a36 36 0 0 1 0-72h380a36 36 0 1 1 0 72zM812.26 284.82L285.39 811.69l-88.11 15.1L212 738l526.72-526.72 73.54 73.54m90.51-11.31L750 120.77a16 16 0 0 0-22.62 0L152.5 695.68a34.11 34.11 0 0 0-9.5 18.56l-25.95 156.23a32 32 0 0 0 37 36.78l155.38-26.62a34.2 34.2 0 0 0 18.38-9.52l575-575a16 16 0 0 0 0-22.63z" p-id="16329"></path></svg>

After

Width:  |  Height:  |  Size: 677 B

View File

@@ -77,7 +77,9 @@ const map = {
playFill: require('./icons/fill/play.svg').default,
courseLight: require('./icons/light/course.svg').default,
promotionLight: require('./icons/light/promotion.svg').default,
logsLight: require('./icons/light/logs.svg').default
logsLight: require('./icons/light/logs.svg').default,
badLight: require('./icons/light/bad.svg').default,
markLight: require('./icons/light/mark.svg').default
};
export type IconName = keyof typeof map;

View File

@@ -11,6 +11,6 @@ export const defaultKbDetail: KbItemType = {
name: 'Embedding-2',
price: 0.2,
defaultToken: 500,
maxToken: 8000
maxToken: 3000
}
};

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

@@ -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 字。'

View File

@@ -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 字。'

View File

@@ -41,7 +41,11 @@ const ChatItemSchema = new Schema({
type: String
},
adminFeedback: {
type: String
type: {
kbId: String,
dataId: String,
content: String
}
},
[TaskResponseKeyEnum.responseData]: {
type: [

View File

@@ -211,8 +211,10 @@ function filterQuote({
const quotePrompt =
filterQuoteQA.length > 0
? `"""${filterQuoteQA
.map((item) => (item.a ? `[${item.q}\n${item.a}]` : `${item.q}`))
.join('\n\n')}"""`
.map((item) =>
item.a ? `{instruction:"${item.q}",output:"${item.a}"}` : `{instruction:"${item.q}"}`
)
.join('\n')}"""`
: '';
return {
@@ -240,11 +242,11 @@ function getChatMessages({
return limitPrompt;
}
const defaultPrompt =
'三引号引用的内容是你的补充知识,它们是最新的,根据引用内容来回答我的问题。';
'三引号是我提供给你的专属知识它们拥有最高优先级。instruction 是相关介绍output 是预期回答,使用引用内容来回答我下面的问题。';
if (limitPrompt) {
return `${defaultPrompt}${limitPrompt}`;
}
return `${defaultPrompt}你仅回答引用包含的内容,如果问题不包含在引用中,你直接回复: "你的问题没有在知识库中体现"`;
return `${defaultPrompt}\n回答内容限制你仅回答三引号中提及的内容下面我提出的问题与引用内容无关时你可以直接回复: "你的问题没有在知识库中体现"`;
})();
const messages: ChatItemType[] = [

View File

@@ -142,7 +142,9 @@ class Pg {
}
const fields = props.values[0].map((item) => item.key).join(',');
const sql = `INSERT INTO ${table} (${fields}) VALUES ${this.getInsertValStr(props.values)} `;
const sql = `INSERT INTO ${table} (${fields}) VALUES ${this.getInsertValStr(
props.values
)} RETURNING id`;
const pg = await connectPg();
return pg.query(sql);
}

View File

@@ -2,7 +2,15 @@ export const getChatModel = (model?: string) => {
return global.chatModels.find((item) => item.model === model);
};
export const getVectorModel = (model?: string) => {
return global.vectorModels.find((item) => item.model === model) || global.vectorModels[0];
return (
global.vectorModels.find((item) => item.model === model) || {
model: 'UnKnow',
name: 'UnKnow',
defaultToken: 500,
price: 0,
maxToken: 3000
}
);
};
export const getModel = (model?: string) => {

View File

@@ -114,5 +114,6 @@ export type AppLogsListItemType = {
time: Date;
title: string;
messageCount: number;
callbackCount: number;
feedbackCount: number;
markCount: number;
};

View File

@@ -2,6 +2,7 @@ import { ChatRoleEnum } from '@/constants/chat';
import type { InitChatResponse, InitShareChatResponse } from '@/api/response/chat';
import { TaskResponseKeyEnum } from '@/constants/chat';
import { ClassifyQuestionAgentItemType } from './app';
import { ChatItemSchema } from './mongoSchema';
export type ExportChatType = 'md' | 'pdf' | 'html';
@@ -9,6 +10,8 @@ export type ChatItemType = {
dataId?: string;
obj: `${ChatRoleEnum}`;
value: string;
userFeedback?: string;
adminFeedback?: ChatItemSchema['adminFeedback'];
[TaskResponseKeyEnum.responseData]?: ChatHistoryItemResType[];
};

View File

@@ -103,7 +103,11 @@ export interface ChatItemSchema extends ChatItemType {
appId: string;
time: Date;
userFeedback?: string;
adminFeedback?: string;
adminFeedback?: {
kbId: string;
dataId: string;
content: string;
};
}
export type BillListItemType = {
@@ -137,7 +141,7 @@ export interface OpenApiSchema {
userId: string;
createTime: Date;
lastUsedTime?: Date;
apiKey: String;
apiKey: string;
}
export interface PromotionRecordSchema {