From b8f08eb33e8b419035443a620414f7f6b657ca77 Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Tue, 23 May 2023 18:35:45 +0800 Subject: [PATCH] feat: quote change --- src/api/chat.ts | 18 +- src/api/fetch.ts | 91 ++++----- src/api/plugins/kb.ts | 6 +- src/components/Icon/icons/edit.svg | 1 + src/components/Icon/index.tsx | 3 +- src/constants/chat.ts | 1 + src/constants/model.ts | 6 +- src/pages/api/chat/chat.ts | 5 +- src/pages/api/chat/getHistoryQuote.ts | 49 +++++ src/pages/api/chat/init.ts | 2 +- src/pages/api/chat/saveChat.ts | 5 +- src/pages/api/chat/updateHistoryQuote.ts | 51 +++++ src/pages/api/openapi/chat/lastChatResult.ts | 38 ---- src/pages/api/openapi/kb/appKbSearch.ts | 4 +- .../api/openapi/text/gptMessagesSlice.ts | 61 +----- src/pages/api/plugins/kb/data/getDataById.ts | 39 ++++ src/pages/chat/components/QuoteModal.tsx | 175 ++++++++++++++++++ src/pages/chat/index.tsx | 60 ++---- src/pages/kb/components/InputDataModal.tsx | 36 ++-- src/service/models/chat.ts | 2 +- src/types/chat.d.ts | 1 + 21 files changed, 439 insertions(+), 215 deletions(-) create mode 100644 src/components/Icon/icons/edit.svg create mode 100644 src/pages/api/chat/getHistoryQuote.ts create mode 100644 src/pages/api/chat/updateHistoryQuote.ts delete mode 100644 src/pages/api/openapi/chat/lastChatResult.ts create mode 100644 src/pages/api/plugins/kb/data/getDataById.ts create mode 100644 src/pages/chat/components/QuoteModal.tsx diff --git a/src/api/chat.ts b/src/api/chat.ts index 2e527492e..2c61ea76a 100644 --- a/src/api/chat.ts +++ b/src/api/chat.ts @@ -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('/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); + /** * 删除一句对话 */ diff --git a/src/api/fetch.ts b/src/api/fetch.ts index c47e0bad1..38f3b185d 100644 --- a/src/api/fetch.ts +++ b/src/api/fetch.ts @@ -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 || '请求异常'); + } } - }); + ); diff --git a/src/api/plugins/kb.ts b/src/api/plugins/kb.ts index 0b148c2c7..e7d7ac4d7 100644 --- a/src/api/plugins/kb.ts +++ b/src/api/plugins/kb.ts @@ -40,10 +40,8 @@ export const getTrainingData = (kbId: string) => embeddingQueue: number; }>(`/plugins/kb/data/getTrainingData?kbId=${kbId}`); -/** - * 获取 web 页面内容 - */ -export const getWebContent = (url: string) => POST(`/model/data/fetchingUrlData`, { url }); +export const getKbDataItemById = (dataId: string) => + GET(`/plugins/kb/data/getDataById`, { dataId }); /** * 直接push数据 diff --git a/src/components/Icon/icons/edit.svg b/src/components/Icon/icons/edit.svg new file mode 100644 index 000000000..f2a5c97ea --- /dev/null +++ b/src/components/Icon/icons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index f77884c3b..34a8e8b55 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -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; diff --git a/src/constants/chat.ts b/src/constants/chat.ts index 2c7cc579f..59b57ec4a 100644 --- a/src/constants/chat.ts +++ b/src/constants/chat.ts @@ -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', diff --git a/src/constants/model.ts b/src/constants/model.ts index e7b4e32f4..8627d3d94 100644 --- a/src/constants/model.ts +++ b/src/constants/model.ts @@ -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 } }; diff --git a/src/pages/api/chat/chat.ts b/src/pages/api/chat/chat.ts index 9301db82e..d4ad11e71 100644 --- a/src/pages/api/chat/chat.ts +++ b/src/pages/api/chat/chat.ts @@ -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) { diff --git a/src/pages/api/chat/getHistoryQuote.ts b/src/pages/api/chat/getHistoryQuote.ts new file mode 100644 index 000000000..f6c307608 --- /dev/null +++ b/src/pages/api/chat/getHistoryQuote.ts @@ -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 + }); + } +} diff --git a/src/pages/api/chat/init.ts b/src/pages/api/chat/init.ts index f24174ca5..c985b3692 100644 --- a/src/pages/api/chat/init.ts +++ b/src/pages/api/chat/init.ts @@ -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' } } } ]); diff --git a/src/pages/api/chat/saveChat.ts b/src/pages/api/chat/saveChat.ts index d2e1e934f..9b96949b6 100644 --- a/src/pages/api/chat/saveChat.ts +++ b/src/pages/api/chat/saveChat.ts @@ -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, 创建一个对话 diff --git a/src/pages/api/chat/updateHistoryQuote.ts b/src/pages/api/chat/updateHistoryQuote.ts new file mode 100644 index 000000000..d906cf932 --- /dev/null +++ b/src/pages/api/chat/updateHistoryQuote.ts @@ -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 + }); + } +} diff --git a/src/pages/api/openapi/chat/lastChatResult.ts b/src/pages/api/openapi/chat/lastChatResult.ts deleted file mode 100644 index 15147f0ad..000000000 --- a/src/pages/api/openapi/chat/lastChatResult.ts +++ /dev/null @@ -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(res, { - data: { - quote: chatItem?.content[0]?.quote || [] - } - }); - } catch (err) { - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/src/pages/api/openapi/kb/appKbSearch.ts b/src/pages/api/openapi/kb/appKbSearch.ts index 6b831d259..f74e16f7d 100644 --- a/src/pages/api/openapi/kb/appKbSearch.ts +++ b/src/pages/api/openapi/kb/appKbSearch.ts @@ -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('modelData', { fields: ['id', 'q', 'a'], where: [ ['status', ModelDataStatusEnum.ready], diff --git a/src/pages/api/openapi/text/gptMessagesSlice.ts b/src/pages/api/openapi/text/gptMessagesSlice.ts index f376135d0..e423b8244 100644 --- a/src/pages/api/openapi/text/gptMessagesSlice.ts +++ b/src/pages/api/openapi/text/gptMessagesSlice.ts @@ -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 { diff --git a/src/pages/api/plugins/kb/data/getDataById.ts b/src/pages/api/plugins/kb/data/getDataById.ts new file mode 100644 index 000000000..f78274cbf --- /dev/null +++ b/src/pages/api/plugins/kb/data/getDataById.ts @@ -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) { + 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('modelData', { + fields: ['id', 'q', 'a', 'status'], + where, + limit: 1 + }); + + jsonRes(res, { + data: searchRes.rows[0] + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/src/pages/chat/components/QuoteModal.tsx b/src/pages/chat/components/QuoteModal.tsx new file mode 100644 index 000000000..3fca5169e --- /dev/null +++ b/src/pages/chat/components/QuoteModal.tsx @@ -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 ( + <> + + + + + 知识库引用({quote.length}条) + + 注意: 修改知识库内容成功后,此处不会显示。点击编辑后,才是显示最新的内容。 + + + + + {quote.map((item) => ( + + {item.isEdit && (编辑过)} + {item.q} + {item.a} + + onclickEdit(item)} + /> + + + ))} + + + + + {editDataItem && ( + setEditDataItem(undefined)} + onSuccess={() => updateQuoteStatus(editDataItem.dataId)} + kbId="" + defaultValues={editDataItem} + /> + )} + + ); +}; + +export default QuoteModal; diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 811d71f53..4aabb01f8 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -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([]); + const [showHistoryQuote, setShowHistoryQuote] = useState(); 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 && ( )} @@ -876,30 +868,14 @@ const Chat = ({ modelId, chatId }: { modelId: string; chatId: string }) => { )} - {/* system prompt show modal */} - { - 0} onClose={() => setShowQuote([])}> - - - 知识库引用({showQuote.length}条) - - - {showQuote.map((item) => ( - - {item.q} - {item.a} - - ))} - - - - } + {/* quote modal*/} + {showHistoryQuote && chatId && ( + setShowHistoryQuote(undefined)} + /> + )} {/* context menu */} {messageContextMenuData && ( { - const [importing, setImporting] = useState(false); + const [loading, setLoading] = useState(false); const { toast } = useToast(); const { register, handleSubmit, reset } = useForm({ @@ -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'} > - 手动导入 + {defaultValues.dataId ? '变更数据' : '手动导入数据'} - + {'匹配的知识点'}