feat: quote change

This commit is contained in:
archer
2023-05-23 18:35:45 +08:00
parent 944e876aaa
commit b8f08eb33e
21 changed files with 439 additions and 215 deletions

View File

@@ -5,7 +5,7 @@ import { RequestPaging } from '../types/index';
import type { ShareChatSchema } from '@/types/mongoSchema';
import type { ShareChatEditType } from '@/types/model';
import { Obj2Query } from '@/utils/tools';
import { Response as LastChatResultResponseType } from '@/pages/api/openapi/chat/lastChatResult';
import { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
/**
* 获取初始化聊天内容
@@ -25,10 +25,20 @@ export const getChatHistory = (data: RequestPaging) =>
export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${id}`);
/**
* get latest chat result by chatId
* get history quotes
*/
export const getChatResult = (chatId: string) =>
GET<LastChatResultResponseType>('/openapi/chat/lastChatResult', { chatId });
export const getHistoryQuote = (params: { chatId: string; historyId: string }) =>
GET<(QuoteItemType & { _id: string })[]>(`/chat/getHistoryQuote`, params);
/**
* update history quote status
*/
export const updateHistoryQuote = (params: {
chatId: string;
historyId: string;
quoteId: string;
}) => GET(`/chat/updateHistoryQuote`, params);
/**
* 删除一句对话
*/

View File

@@ -1,4 +1,4 @@
import { NEW_CHATID_HEADER } from '@/constants/chat';
import { NEW_CHATID_HEADER, QUOTE_LEN_HEADER } from '@/constants/chat';
interface StreamFetchProps {
url: string;
@@ -7,52 +7,57 @@ interface StreamFetchProps {
abortSignal: AbortController;
}
export const streamFetch = ({ url, data, onMessage, abortSignal }: StreamFetchProps) =>
new Promise<{ responseText: string; newChatId: string }>(async (resolve, reject) => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
signal: abortSignal.signal
});
const reader = res.body?.getReader();
if (!reader) return;
new Promise<{ responseText: string; newChatId: string; quoteLen: number }>(
async (resolve, reject) => {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
signal: abortSignal.signal
});
const reader = res.body?.getReader();
if (!reader) return;
const decoder = new TextDecoder();
const decoder = new TextDecoder();
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
const newChatId = decodeURIComponent(res.headers.get(NEW_CHATID_HEADER) || '');
const quoteLen = res.headers.get(QUOTE_LEN_HEADER)
? Number(res.headers.get(QUOTE_LEN_HEADER))
: 0;
let responseText = '';
let responseText = '';
const read = async () => {
try {
const { done, value } = await reader?.read();
if (done) {
if (res.status === 200) {
resolve({ responseText, newChatId });
} else {
const parseError = JSON.parse(responseText);
reject(parseError?.message || '请求异常');
const read = async () => {
try {
const { done, value } = await reader?.read();
if (done) {
if (res.status === 200) {
resolve({ responseText, newChatId, quoteLen });
} else {
const parseError = JSON.parse(responseText);
reject(parseError?.message || '请求异常');
}
return;
}
return;
const text = decoder.decode(value);
responseText += text;
onMessage(text);
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({ responseText, newChatId, quoteLen: 0 });
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
const text = decoder.decode(value);
responseText += text;
onMessage(text);
read();
} catch (err: any) {
if (err?.message === 'The user aborted a request.') {
return resolve({ responseText, newChatId });
}
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
};
read();
} catch (err: any) {
console.log(err, '====');
reject(typeof err === 'string' ? err : err?.message || '请求异常');
};
read();
} catch (err: any) {
console.log(err, '====');
reject(typeof err === 'string' ? err : err?.message || '请求异常');
}
}
});
);

View File

@@ -40,10 +40,8 @@ export const getTrainingData = (kbId: string) =>
embeddingQueue: number;
}>(`/plugins/kb/data/getTrainingData?kbId=${kbId}`);
/**
* 获取 web 页面内容
*/
export const getWebContent = (url: string) => POST<string>(`/model/data/fetchingUrlData`, { url });
export const getKbDataItemById = (dataId: string) =>
GET(`/plugins/kb/data/getDataById`, { dataId });
/**
* 直接push数据

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="1684826302600" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2244" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M904 512h-56c-4.4 0-8 3.6-8 8v320H184V184h320c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V520c0-4.4-3.6-8-8-8z" p-id="2245"></path><path d="M355.9 534.9L354 653.8c-0.1 8.9 7.1 16.2 16 16.2h0.4l118-2.9c2-0.1 4-0.9 5.4-2.3l415.9-415c3.1-3.1 3.1-8.2 0-11.3L785.4 114.3c-1.6-1.6-3.6-2.3-5.7-2.3s-4.1 0.8-5.7 2.3l-415.8 415c-1.4 1.5-2.3 3.5-2.3 5.6z m63.5 23.6L779.7 199l45.2 45.1-360.5 359.7-45.7 1.1 0.7-46.4z" p-id="2246"></path></svg>

After

Width:  |  Height:  |  Size: 810 B

View File

@@ -28,7 +28,8 @@ const map = {
git: require('./icons/git.svg').default,
kb: require('./icons/kb.svg').default,
appStore: require('./icons/appStore.svg').default,
menu: require('./icons/menu.svg').default
menu: require('./icons/menu.svg').default,
edit: require('./icons/edit.svg').default
};
export type IconName = keyof typeof map;

View File

@@ -1,4 +1,5 @@
export const NEW_CHATID_HEADER = 'response-new-chat-id';
export const QUOTE_LEN_HEADER = 'response-quote-len';
export enum ChatRoleEnum {
System = 'System',

View File

@@ -122,15 +122,15 @@ export const ModelVectorSearchModeMap: Record<
> = {
[ModelVectorSearchModeEnum.hightSimilarity]: {
text: '高相似度, 无匹配时拒绝回复',
similarity: 0.2
similarity: 0.18
},
[ModelVectorSearchModeEnum.noContext]: {
text: '高相似度,无匹配时直接回复',
similarity: 0.2
similarity: 0.18
},
[ModelVectorSearchModeEnum.lowSimilarity]: {
text: '低相似度匹配',
similarity: 0.8
similarity: 0.7
}
};

View File

@@ -8,7 +8,7 @@ import { ChatModelMap, ModelVectorSearchModeMap } from '@/constants/model';
import { pushChatBill } from '@/service/events/pushBill';
import { resStreamResponse } from '@/service/utils/chat';
import { appKbSearch } from '../openapi/kb/appKbSearch';
import { ChatRoleEnum } from '@/constants/chat';
import { ChatRoleEnum, QUOTE_LEN_HEADER } from '@/constants/chat';
import { BillTypeEnum } from '@/constants/user';
import { sensitiveCheck } from '@/service/api/text';
import { NEW_CHATID_HEADER } from '@/constants/chat';
@@ -85,7 +85,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
// get conversationId. create a newId if it is null
const conversationId = chatId || String(new Types.ObjectId());
!chatId && res?.setHeader(NEW_CHATID_HEADER, conversationId);
!chatId && res.setHeader(NEW_CHATID_HEADER, conversationId);
res.setHeader(QUOTE_LEN_HEADER, quote.length);
// search result is empty
if (code === 201) {

View File

@@ -0,0 +1,49 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { Types } from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId, historyId } = req.query as { chatId: string; historyId: string };
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
if (!chatId || !historyId) {
throw new Error('params is error');
}
const history = await Chat.aggregate([
{
$match: {
_id: new Types.ObjectId(chatId),
userId: new Types.ObjectId(userId)
}
},
{
$unwind: '$content'
},
{
$match: {
'content._id': new Types.ObjectId(historyId)
}
},
{
$project: {
quote: '$content.quote'
}
}
]);
jsonRes(res, {
data: history[0]?.quote || []
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -73,7 +73,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
_id: '$content._id',
obj: '$content.obj',
value: '$content.value',
quote: '$content.quote'
quoteLen: { $size: '$content.quote' }
}
}
]);

View File

@@ -57,7 +57,10 @@ export async function saveChat({
_id: item._id ? new mongoose.Types.ObjectId(item._id) : undefined,
obj: item.obj,
value: item.value,
quote: item.quote
quote: item.quote?.map((item) => ({
...item,
isEdit: false
}))
}));
// 没有 chatId, 创建一个对话

View File

@@ -0,0 +1,51 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase, Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { Types } from 'mongoose';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
let { chatId, historyId, quoteId } = req.query as {
chatId: string;
historyId: string;
quoteId: string;
};
await connectToDatabase();
const { userId } = await authUser({ req, authToken: true });
if (!chatId || !historyId || !quoteId) {
throw new Error('params is error');
}
await Chat.updateOne(
{
_id: new Types.ObjectId(chatId),
userId: new Types.ObjectId(userId),
'content._id': new Types.ObjectId(historyId)
},
{
$set: {
'content.$.quote.$[quoteElem].isEdit': true
}
},
{
arrayFilters: [
{
'quoteElem.id': quoteId
}
]
}
);
jsonRes(res, {
data: ''
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -1,38 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { Chat } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { QuoteItemType } from '../kb/appKbSearch';
type Props = {
chatId: string;
};
export type Response = {
quote: QuoteItemType[];
};
/* 聊天内容存存储 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { chatId } = req.query as Props;
if (!chatId) {
throw new Error('缺少参数');
}
const { userId } = await authUser({ req });
const chatItem = await Chat.findOne({ _id: chatId, userId }, { content: { $slice: -1 } });
jsonRes<Response>(res, {
data: {
quote: chatItem?.content[0]?.quote || []
}
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -13,7 +13,7 @@ import { openaiEmbedding } from '../plugin/openaiEmbedding';
import { ModelDataStatusEnum } from '@/constants/model';
import { modelToolMap } from '@/utils/plugin';
export type QuoteItemType = { id: string; q: string; a: string };
export type QuoteItemType = { id: string; q: string; a: string; isEdit: boolean };
type Props = {
prompts: ChatItemSimpleType[];
similarity: number;
@@ -97,7 +97,7 @@ export async function appKbSearch({
// search kb
const searchRes = await Promise.all(
promptVectors.map((promptVector) =>
PgClient.select<{ id: string; q: string; a: string }>('modelData', {
PgClient.select<QuoteItemType>('modelData', {
fields: ['id', 'q', 'a'],
where: [
['status', ModelDataStatusEnum.ready],

View File

@@ -1,13 +1,9 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import type { NextApiRequest, NextApiResponse } from 'next';
import { type Tiktoken } from '@dqbd/tiktoken';
import { jsonRes } from '@/service/response';
import { authUser } from '@/service/utils/auth';
import Graphemer from 'graphemer';
import type { ChatItemSimpleType } from '@/types/chat';
import { ChatCompletionRequestMessage } from 'openai';
import { getOpenAiEncMap } from '@/utils/plugin/openai';
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
import { countOpenAIToken } from '@/utils/plugin/openai';
type ModelType = 'gpt-3.5-turbo' | 'gpt-4' | 'gpt-4-32k';
@@ -52,62 +48,13 @@ export function gpt_chatItemTokenSlice({
model: ModelType;
maxToken: number;
}) {
const textDecoder = new TextDecoder();
const graphemer = new Graphemer();
function getChatGPTEncodingText(messages: ChatCompletionRequestMessage[], model: ModelType) {
const isGpt3 = model === 'gpt-3.5-turbo';
const msgSep = isGpt3 ? '\n' : '';
const roleSep = isGpt3 ? '\n' : '<|im_sep|>';
return [
messages
.map(({ name = '', role, content }) => {
return `<|im_start|>${name || role}${roleSep}${content}<|im_end|>`;
})
.join(msgSep),
`<|im_start|>assistant${roleSep}`
].join(msgSep);
}
function text2TokensLen(encoder: Tiktoken, inputText: string) {
const encoding = encoder.encode(inputText, 'all');
const segments: { text: string; tokens: { id: number; idx: number }[] }[] = [];
let byteAcc: number[] = [];
let tokenAcc: { id: number; idx: number }[] = [];
let inputGraphemes = graphemer.splitGraphemes(inputText);
for (let idx = 0; idx < encoding.length; idx++) {
const token = encoding[idx]!;
byteAcc.push(...encoder.decode_single_token_bytes(token));
tokenAcc.push({ id: token, idx });
const segmentText = textDecoder.decode(new Uint8Array(byteAcc));
const graphemes = graphemer.splitGraphemes(segmentText);
if (graphemes.every((item, idx) => inputGraphemes[idx] === item)) {
segments.push({ text: segmentText, tokens: tokenAcc });
byteAcc = [];
tokenAcc = [];
inputGraphemes = inputGraphemes.slice(graphemes.length);
}
}
return segments.reduce((memo, i) => memo + i.tokens.length, 0) ?? 0;
}
const OpenAiEncMap = getOpenAiEncMap();
const enc = OpenAiEncMap[model];
let result: ChatItemSimpleType[] = [];
for (let i = 0; i < messages.length; i++) {
const msgs = [...result, messages[i]];
const tokens = text2TokensLen(
enc,
getChatGPTEncodingText(adaptChatItem_openAI({ messages }), model)
);
const tokens = countOpenAIToken({ messages: msgs, model });
if (tokens < maxToken) {
result = msgs;
} else {

View File

@@ -0,0 +1,39 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@/service/response';
import { connectToDatabase } from '@/service/mongo';
import { authUser } from '@/service/utils/auth';
import { PgClient } from '@/service/pg';
import type { PgKBDataItemType } from '@/types/pg';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
let { dataId } = req.query as {
dataId: string;
};
if (!dataId) {
throw new Error('缺少参数');
}
// 凭证校验
const { userId } = await authUser({ req, authToken: true });
await connectToDatabase();
const where: any = [['user_id', userId], 'AND', ['id', dataId]];
const searchRes = await PgClient.select<PgKBDataItemType>('modelData', {
fields: ['id', 'q', 'a', 'status'],
where,
limit: 1
});
jsonRes(res, {
data: searchRes.rows[0]
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}

View File

@@ -0,0 +1,175 @@
import React, { useCallback, useState } from 'react';
import {
Modal,
ModalOverlay,
ModalContent,
ModalBody,
ModalCloseButton,
ModalHeader,
Box,
useTheme
} from '@chakra-ui/react';
import { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
import MyIcon from '@/components/Icon';
import InputDataModal from '@/pages/kb/components/InputDataModal';
import { getKbDataItemById } from '@/api/plugins/kb';
import { useLoading } from '@/hooks/useLoading';
import { useQuery } from '@tanstack/react-query';
import { getHistoryQuote, updateHistoryQuote } from '@/api/chat';
import { useToast } from '@/hooks/useToast';
import { getErrText } from '@/utils/tools';
const QuoteModal = ({
historyId,
chatId,
onClose
}: {
historyId: string;
chatId: string;
onClose: () => void;
}) => {
const theme = useTheme();
const { toast } = useToast();
const { setIsLoading, Loading } = useLoading();
const [editDataItem, setEditDataItem] = useState<{
dataId: string;
a: string;
q: string;
}>();
const {
data: quote = [],
refetch,
isLoading
} = useQuery(['getHistoryQuote'], () => getHistoryQuote({ historyId, chatId }));
/**
* click edit, get new kbDataItem
*/
const onclickEdit = useCallback(
async (item: QuoteItemType) => {
try {
setIsLoading(true);
const data = (await getKbDataItemById(item.id)) as QuoteItemType;
if (!data) {
throw new Error('该数据已被删除');
}
setEditDataItem({
dataId: data.id,
q: data.q,
a: data.a
});
} catch (err) {
toast({
status: 'warning',
title: getErrText(err)
});
}
setIsLoading(false);
},
[setIsLoading, toast]
);
/**
* update kbData, update mongo status and reload quotes
*/
const updateQuoteStatus = useCallback(
async (quoteId: string) => {
setIsLoading(true);
try {
await updateHistoryQuote({
chatId,
historyId,
quoteId
});
// reload quote
refetch();
} catch (err) {
toast({
status: 'warning',
title: getErrText(err)
});
}
setIsLoading(false);
},
[chatId, historyId, refetch, setIsLoading, toast]
);
return (
<>
<Modal isOpen={true} onClose={onClose}>
<ModalOverlay />
<ModalContent
position={'relative'}
maxW={'min(90vw, 700px)'}
h={'80vh'}
overflow={'overlay'}
>
<ModalHeader>
({quote.length})
<Box fontSize={'sm'} fontWeight={'normal'}>
注意: 修改知识库内容成功后
</Box>
</ModalHeader>
<ModalCloseButton />
<ModalBody pt={0} whiteSpace={'pre-wrap'} textAlign={'justify'} fontSize={'sm'}>
{quote.map((item) => (
<Box
key={item.id}
flex={'1 0 0'}
p={2}
borderRadius={'sm'}
border={theme.borders.base}
_notLast={{ mb: 2 }}
position={'relative'}
_hover={{ '& .edit': { display: 'flex' } }}
>
{item.isEdit && <Box color={'myGray.600'}>()</Box>}
<Box>{item.q}</Box>
<Box>{item.a}</Box>
<Box
className="edit"
display={'none'}
position={'absolute'}
right={0}
top={0}
bottom={0}
w={'40px'}
bg={'rgba(255,255,255,0.9)'}
alignItems={'center'}
justifyContent={'center'}
boxShadow={'-10px 0 10px rgba(255,255,255,1)'}
>
<MyIcon
name={'edit'}
w={'18px'}
h={'18px'}
cursor={'pointer'}
color={'myGray.600'}
_hover={{
color: 'myBlue.700'
}}
onClick={() => onclickEdit(item)}
/>
</Box>
</Box>
))}
</ModalBody>
<Loading loading={isLoading} fixed={false} />
</ModalContent>
</Modal>
{editDataItem && (
<InputDataModal
onClose={() => setEditDataItem(undefined)}
onSuccess={() => updateQuoteStatus(editDataItem.dataId)}
kbId=""
defaultValues={editDataItem}
/>
)}
</>
);
};
export default QuoteModal;

View File

@@ -1,11 +1,6 @@
import React, { useCallback, useState, useRef, useMemo, useEffect, MouseEvent } from 'react';
import { useRouter } from 'next/router';
import {
getInitChatSiteInfo,
delChatRecordByIndex,
getChatResult,
delChatHistoryById
} from '@/api/chat';
import { getInitChatSiteInfo, delChatRecordByIndex, delChatHistoryById } from '@/api/chat';
import type { ChatItemType, ChatSiteItemType, ExportChatType } from '@/types/chat';
import {
Textarea,
@@ -22,6 +17,7 @@ import {
ModalContent,
ModalBody,
ModalCloseButton,
ModalHeader,
useDisclosure,
Drawer,
DrawerOverlay,
@@ -29,8 +25,7 @@ import {
Card,
Tooltip,
useOutsideClick,
useTheme,
ModalHeader
useTheme
} from '@chakra-ui/react';
import { useToast } from '@/hooks/useToast';
import { useGlobalStore } from '@/store/global';
@@ -48,12 +43,12 @@ import { useLoading } from '@/hooks/useLoading';
import { fileDownload } from '@/utils/file';
import { htmlTemplate } from '@/constants/common';
import { useUserStore } from '@/store/user';
import type { QuoteItemType } from '@/pages/api/openapi/kb/appKbSearch';
import Loading from '@/components/Loading';
import Markdown from '@/components/Markdown';
import SideBar from '@/components/SideBar';
import Avatar from '@/components/Avatar';
import Empty from './components/Empty';
import QuoteModal from './components/QuoteModal';
const PhoneSliderBar = dynamic(() => import('./components/PhoneSliderBar'), {
ssr: false
@@ -80,7 +75,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
const controller = useRef(new AbortController());
const isLeavePage = useRef(false);
const [showQuote, setShowQuote] = useState<QuoteItemType[]>([]);
const [showHistoryQuote, setShowHistoryQuote] = useState<string>();
const [messageContextMenuData, setMessageContextMenuData] = useState<{
// message messageContextMenuData
left: number;
@@ -182,7 +177,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
}));
// 流请求,获取数据
const { newChatId } = await streamFetch({
const { newChatId, quoteLen } = await streamFetch({
url: '/api/chat/chat',
data: {
prompt,
@@ -217,9 +212,6 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
abortSignal.signal.aborted && (await delay(600));
// get chat result
const { quote } = await getChatResult(chatId || newChatId);
// 设置聊天内容为完成状态
setChatData((state) => ({
...state,
@@ -229,7 +221,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
return {
...item,
status: 'finish',
quote
quoteLen
};
})
}));
@@ -735,7 +727,7 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
isChatting={isChatting && index === chatData.history.length - 1}
formatLink
/>
{item.quote && item.quote.length > 0 && (
{!!item.quoteLen && (
<Button
size={'xs'}
mt={2}
@@ -743,9 +735,9 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
colorScheme={'gray'}
variant={'outline'}
w={'90px'}
onClick={() => setShowQuote(item.quote || [])}
onClick={() => setShowHistoryQuote(item._id)}
>
{item.quoteLen}
</Button>
)}
</Card>
@@ -876,30 +868,14 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => {
</DrawerContent>
</Drawer>
)}
{/* system prompt show modal */}
{
<Modal isOpen={showQuote.length > 0} onClose={() => setShowQuote([])}>
<ModalOverlay />
<ModalContent maxW={'min(90vw, 700px)'} h={'80vh'} overflow={'overlay'}>
<ModalHeader>({showQuote.length})</ModalHeader>
<ModalCloseButton />
<ModalBody whiteSpace={'pre-wrap'} textAlign={'justify'} fontSize={'sm'}>
{showQuote.map((item) => (
<Box
key={item.id}
p={2}
borderRadius={'sm'}
border={theme.borders.base}
_notLast={{ mb: 2 }}
>
<Box>{item.q}</Box>
<Box>{item.a}</Box>
</Box>
))}
</ModalBody>
</ModalContent>
</Modal>
}
{/* quote modal*/}
{showHistoryQuote && chatId && (
<QuoteModal
historyId={showHistoryQuote}
chatId={chatId}
onClose={() => setShowHistoryQuote(undefined)}
/>
)}
{/* context menu */}
{messageContextMenuData && (
<Box

View File

@@ -30,7 +30,7 @@ const InputDataModal = ({
kbId: string;
defaultValues?: FormData;
}) => {
const [importing, setImporting] = useState(false);
const [loading, setLoading] = useState(false);
const { toast } = useToast();
const { register, handleSubmit, reset } = useForm<FormData>({
@@ -49,7 +49,7 @@ const InputDataModal = ({
});
return;
}
setImporting(true);
setLoading(true);
try {
const res = await postKbDataFromList({
@@ -78,7 +78,7 @@ const InputDataModal = ({
});
console.log(err);
}
setImporting(false);
setLoading(false);
},
[kbId, onSuccess, reset, toast]
);
@@ -88,16 +88,20 @@ const InputDataModal = ({
if (!e.dataId) return;
if (e.a !== defaultValues.a || e.q !== defaultValues.q) {
await putKbDataById({
dataId: e.dataId,
a: e.a,
q: e.q === defaultValues.q ? '' : e.q
});
onSuccess();
setLoading(true);
try {
await putKbDataById({
dataId: e.dataId,
a: e.a,
q: e.q === defaultValues.q ? '' : e.q
});
onSuccess();
} catch (error) {}
setLoading(false);
}
toast({
title: '修改回答成功',
title: '修改数据成功',
status: 'success'
});
onClose();
@@ -116,18 +120,18 @@ const InputDataModal = ({
maxW={'90vw'}
position={'relative'}
>
<ModalHeader></ModalHeader>
<ModalHeader>{defaultValues.dataId ? '变更数据' : '手动导入数据'}</ModalHeader>
<ModalCloseButton />
<Box
display={['block', 'flex']}
flex={'1 0 0'}
h={['100%', 0]}
overflowY={'auto'}
overflow={'overlay'}
px={6}
pb={2}
>
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['230px', '100%']}>
<Box flex={1} mr={[0, 4]} mb={[4, 0]} h={['50%', '100%']}>
<Box h={'30px'}>{'匹配的知识点'}</Box>
<Textarea
placeholder={'匹配的知识点。这部分内容会被搜索,请把控内容的质量。总和最多 3000 字。'}
@@ -139,7 +143,7 @@ const InputDataModal = ({
})}
/>
</Box>
<Box flex={1} h={['330px', '100%']}>
<Box flex={1} h={['50%', '100%']}>
<Box h={'30px'}></Box>
<Textarea
placeholder={
@@ -159,10 +163,10 @@ const InputDataModal = ({
</Button>
<Button
isLoading={importing}
isLoading={loading}
onClick={handleSubmit(defaultValues.dataId ? updateData : sureImportData)}
>
{defaultValues.dataId ? '确认变更' : '确认导入'}
</Button>
</Flex>
</ModalContent>

View File

@@ -48,7 +48,7 @@ const ChatSchema = new Schema({
required: true
},
quote: {
type: [{ id: String, q: String, a: String }],
type: [{ id: String, q: String, a: String, isEdit: Boolean }],
default: []
}
// systemPrompt: {

1
src/types/chat.d.ts vendored
View File

@@ -7,6 +7,7 @@ export type ExportChatType = 'md' | 'pdf' | 'html';
export type ChatItemSimpleType = {
obj: `${ChatRoleEnum}`;
value: string;
quoteLen?: number;
quote?: QuoteItemType[];
};
export type ChatItemType = {