mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-21 11:43:56 +00:00
feat: quote change
This commit is contained in:
@@ -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);
|
||||
|
||||
/**
|
||||
* 删除一句对话
|
||||
*/
|
||||
|
@@ -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 || '请求异常');
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
@@ -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数据
|
||||
|
1
src/components/Icon/icons/edit.svg
Normal file
1
src/components/Icon/icons/edit.svg
Normal 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 |
@@ -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;
|
||||
|
@@ -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',
|
||||
|
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -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) {
|
||||
|
49
src/pages/api/chat/getHistoryQuote.ts
Normal file
49
src/pages/api/chat/getHistoryQuote.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
@@ -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' }
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
@@ -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, 创建一个对话
|
||||
|
51
src/pages/api/chat/updateHistoryQuote.ts
Normal file
51
src/pages/api/chat/updateHistoryQuote.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
@@ -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],
|
||||
|
@@ -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 {
|
||||
|
39
src/pages/api/plugins/kb/data/getDataById.ts
Normal file
39
src/pages/api/plugins/kb/data/getDataById.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
175
src/pages/chat/components/QuoteModal.tsx
Normal file
175
src/pages/chat/components/QuoteModal.tsx
Normal 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;
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
@@ -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
1
src/types/chat.d.ts
vendored
@@ -7,6 +7,7 @@ export type ExportChatType = 'md' | 'pdf' | 'html';
|
||||
export type ChatItemSimpleType = {
|
||||
obj: `${ChatRoleEnum}`;
|
||||
value: string;
|
||||
quoteLen?: number;
|
||||
quote?: QuoteItemType[];
|
||||
};
|
||||
export type ChatItemType = {
|
||||
|
Reference in New Issue
Block a user