From 9415e22de9a78c1a6ab0db118ce88957a34f4f03 Mon Sep 17 00:00:00 2001 From: Archer <545436317@qq.com> Date: Thu, 24 Aug 2023 21:08:49 +0800 Subject: [PATCH] v4.2 (#217) * fix: chat module link * fix: url fetch check * fix: import file ui * feat: app logs * perf: iframe icon * imgs cdn * perf: click range and pg --- README.md | 2 +- client/package.json | 6 +- client/public/docs/versionIntro.md | 13 +- client/public/js/iframe.js | 37 ++- client/public/locales/en/common.json | 22 +- client/public/locales/zh/common.json | 20 +- client/src/api/app.ts | 3 + client/src/components/ChatBox/QuoteModal.tsx | 1 + client/src/components/ChatBox/index.tsx | 10 +- .../src/components/Icon/icons/light/logs.svg | 14 ++ client/src/components/Icon/index.tsx | 3 +- client/src/constants/chat.ts | 13 +- client/src/constants/flow/ModuleTemplate.ts | 9 +- client/src/pages/_app.tsx | 2 +- client/src/pages/api/chat/getChatLogs.ts | 72 ++++++ .../pages/api/openapi/v1/chat/completions.ts | 5 +- client/src/pages/api/plugins/urlFetch.ts | 39 +-- .../src/pages/app/detail/components/Logs.tsx | 237 ++++++++++++++++++ client/src/pages/app/detail/index.tsx | 8 +- .../kb/detail/components/Import/Chunk.tsx | 4 +- .../pages/kb/detail/components/Import/Csv.tsx | 4 +- .../kb/detail/components/Import/Manual.tsx | 2 +- .../pages/kb/detail/components/Import/QA.tsx | 4 +- client/src/service/models/chatItem.ts | 1 + .../service/moduleDispatch/init/history.tsx | 2 +- .../src/service/moduleDispatch/tools/http.ts | 15 +- client/src/service/pg.ts | 5 +- client/src/types/app.d.ts | 11 +- client/src/types/chat.d.ts | 3 + docSite/vercel.json | 2 +- 30 files changed, 506 insertions(+), 63 deletions(-) create mode 100644 client/src/components/Icon/icons/light/logs.svg create mode 100644 client/src/pages/api/chat/getChatLogs.ts create mode 100644 client/src/pages/app/detail/components/Logs.tsx diff --git a/README.md b/README.md index 36cd58055..acfc576a1 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ FastGPT 是一个基于 LLM 大语言模型的知识库问答系统,提供开 - [x] 支持直接分段导入 - [x] 支持 QA 拆分导入 - [x] 支持手动输入内容 - - [ ] 支持 url 读取导入 + - [x] 支持 url 读取导入 - [x] 支持 CSV 批量导入问答对 - [ ] 支持知识库单独设置向量模型 - [ ] 源文件存储 diff --git a/client/package.json b/client/package.json index b7dc5a63a..35be01fe2 100644 --- a/client/package.json +++ b/client/package.json @@ -15,10 +15,10 @@ "@dqbd/tiktoken": "^1.0.7", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", + "@mozilla/readability": "^0.4.4", "@next/font": "13.1.6", "@tanstack/react-query": "^4.24.10", "@types/nprogress": "^0.2.0", - "@mozilla/readability": "^0.4.4", "axios": "^1.3.3", "cookie": "^0.5.0", "crypto": "^1.0.1", @@ -31,6 +31,7 @@ "i18next": "^22.5.1", "immer": "^9.0.19", "js-cookie": "^3.0.5", + "jsdom": "^22.1.0", "jsonwebtoken": "^9.0.0", "lodash": "^4.17.21", "mammoth": "^1.5.1", @@ -59,7 +60,6 @@ "request-ip": "^3.3.0", "sass": "^1.58.3", "tunnel": "^0.0.6", - "jsdom": "^22.1.0", "winston": "^3.10.0", "winston-mongodb": "^5.1.1", "zustand": "^4.3.5" @@ -69,6 +69,7 @@ "@types/cookie": "^0.5.1", "@types/formidable": "^2.0.5", "@types/js-cookie": "^3.0.3", + "@types/jsdom": "^21.1.1", "@types/jsonwebtoken": "^9.0.1", "@types/lodash": "^4.14.191", "@types/node": "18.14.0", @@ -79,7 +80,6 @@ "@types/react-syntax-highlighter": "^15.5.6", "@types/request-ip": "^0.0.37", "@types/tunnel": "^0.0.3", - "@types/jsdom": "^21.1.1", "eslint": "8.34.0", "eslint-config-next": "13.1.6", "typescript": "4.9.5" diff --git a/client/public/docs/versionIntro.md b/client/public/docs/versionIntro.md index 8aec9ee33..06c1c19b0 100644 --- a/client/public/docs/versionIntro.md +++ b/client/public/docs/versionIntro.md @@ -1,7 +1,8 @@ -### Fast GPT V4.1 +### Fast GPT V4.2 -1. 新增 - 高级编排导入导出功能 -2. 优化对话存储结构 -3. 优化日志存储 -4. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow) -5. 填写个人 OpenAI 账号后,分享和 API 功能也不会走平台余额扣费。 +1. 新增 - 应用日志初版,可观测到所有对话记录 +2. 新增 - 好友邀请链接,[点击查看](/account?currentTab=promotion) +3. 新增 - Iframe 嵌入页面图标可拖拽 +4. 优化 - 知识库搜索提示词 +5. 优化 - [使用文档](https://doc.fastgpt.run/docs/intro/) +6. [点击查看高级编排介绍文档](https://doc.fastgpt.run/docs/workflow) diff --git a/client/public/js/iframe.js b/client/public/js/iframe.js index 9da207a8c..5fe502f3f 100644 --- a/client/public/js/iframe.js +++ b/client/public/js/iframe.js @@ -20,11 +20,9 @@ async function embedChatbot() { const ChatBtn = document.createElement('div'); ChatBtn.id = chatBtnId; ChatBtn.style.cssText = - 'position: fixed; bottom: 1rem; right: 1rem; width: 40px; height: 40px; cursor: pointer; z-index: 2147483647; '; + 'position: fixed; bottom: 1rem; right: 1rem; width: 40px; height: 40px; cursor: pointer; z-index: 2147483647; transition: 0;'; const ChatBtnDiv = document.createElement('div'); - ChatBtnDiv.style.cssText = - 'transition: all 0.2s ease-in-out 0s; left: unset; transform: scale(1); :hover {transform: scale(1.1);} display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 9999;'; ChatBtnDiv.innerHTML = MessageIcon; const iframe = document.createElement('iframe'); @@ -38,7 +36,15 @@ async function embedChatbot() { document.body.appendChild(iframe); + let chatBtnDragged = false; + let chatBtnDown = false; + let chatBtnMouseX; + let chatBtnMouseY; ChatBtn.addEventListener('click', function () { + if (chatBtnDragged) { + chatBtnDragged = false; + return; + } const chatWindow = document.getElementById(chatWindowId); if (!chatWindow) return; @@ -52,6 +58,31 @@ async function embedChatbot() { } }); + ChatBtn.addEventListener('mousedown', (e) => { + if (!chatBtnMouseX && !chatBtnMouseY) { + chatBtnMouseX = e.clientX; + chatBtnMouseY = e.clientY; + } + + chatBtnDown = true; + }); + ChatBtn.addEventListener('mousemove', (e) => { + if (!chatBtnDown) return; + chatBtnDragged = true; + const transformX = e.clientX - chatBtnMouseX; + const transformY = e.clientY - chatBtnMouseY; + + ChatBtn.style.transform = `translate3d(${transformX}px, ${transformY}px, 0)`; + + e.stopPropagation(); + }); + ChatBtn.addEventListener('mouseup', (e) => { + chatBtnDown = false; + }); + ChatBtn.addEventListener('mouseleave', (e) => { + chatBtnDown = false; + }); + ChatBtn.appendChild(ChatBtnDiv); document.body.appendChild(ChatBtn); } diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index a9bb9495e..33e171fe8 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -4,9 +4,12 @@ "Confirm": "Yes", "Running": "Running", "Warning": "Warning", + "UnKnow": "UnKnow", "app": { "Advance App TestTip": "The current application is advanced editing mode \n. If you need to switch to [simple mode], please click the save button on the left", "App Detail": "App Detail", + "Chat Logs Tips": "Logs record the app's online, shared, and API conversations", + "Chat logs": "Chat Logs", "Confirm Del App Tip": "Confirm to delete the app and all its chats", "Confirm Save App Tip": "The application may be in advanced orchestration mode, and the advanced orchestration configuration will be overwritten after saving, please confirm!", "Connection is invalid": "Connecting is invalid", @@ -18,6 +21,11 @@ "Import Config Failed": "Failed to import the configuration, please ensure that the configuration is normal!", "Import Configs": "Import Configs", "Input Field Settings": "Input Field Settings", + "Logs Empty": "Logs is empty", + "Logs Message Total": "Message Count", + "Logs Source": "Source", + "Logs Time": "Time", + "Logs Title": "Title", "My Apps": "My Apps", "Output Field Settings": "Output Field Settings", "Paste Config": "Paste Config" @@ -28,7 +36,13 @@ "Exit Chat": "Exit", "History": "History", "New Chat": "New Chat", - "You need to a chat app": "You need to a chat app" + "You need to a chat app": "You need to a chat app", + "logs": { + "api": "API", + "online": "Online Chat", + "share": "Share", + "test": "Test Chat " + } }, "commom": { "Password inconsistency": "Password inconsistency" @@ -149,6 +163,10 @@ "Update Password": "Update Password", "Update password failed": "Update password failed", "Update password succseful": "Update password succseful", - "Usage Record": "Usage" + "Usage Record": "Usage", + "promotion": { + "pay": "", + "register": "" + } } } diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index bc6fa1d96..d43da7a66 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -4,9 +4,12 @@ "Confirm": "确认", "Running": "运行中", "Warning": "提示", + "UnKnow": "未知", "app": { "Advance App TestTip": "当前应用为高级编排模式\n如需切换为【简易模式】请点击左侧保存按键", "App Detail": "应用详情", + "Chat Logs Tips": "日志会记录该应用的在线、分享和 API 对话记录", + "Chat logs": "对话日志", "Confirm Del App Tip": "确认删除该应用及其所有聊天记录?", "Confirm Save App Tip": "该应用可能为高级编排模式,保存后将会覆盖高级编排配置,请确认!", "Connection is invalid": "连接无效", @@ -18,6 +21,11 @@ "Import Config Failed": "导入配置失败,请确保配置正常!", "Import Configs": "导入配置", "Input Field Settings": "输入字段编辑", + "Logs Empty": "还没有日志噢~", + "Logs Message Total": "消息总数", + "Logs Source": "来源", + "Logs Time": "时间", + "Logs Title": "标题", "My Apps": "我的应用", "Output Field Settings": "输出字段编辑", "Paste Config": "粘贴配置" @@ -28,7 +36,13 @@ "Exit Chat": "退出聊天", "History": "记录", "New Chat": "新对话", - "You need to a chat app": "你需要创建一个应用" + "You need to a chat app": "你需要创建一个应用", + "logs": { + "api": "API 调用", + "online": "在线使用", + "share": "外部链接调用", + "test": "测试" + } }, "commom": { "Password inconsistency": "两次密码不一致" @@ -151,8 +165,8 @@ "Update password succseful": "修改密码成功", "Usage Record": "使用记录", "promotion": { - "register": "好友注册", - "pay": "好友充值" + "pay": "好友充值", + "register": "好友注册" } } } diff --git a/client/src/api/app.ts b/client/src/api/app.ts index 1b446fee1..86ce07f08 100644 --- a/client/src/api/app.ts +++ b/client/src/api/app.ts @@ -51,3 +51,6 @@ export const getAppTotalUsage = (data: { appId: string }) => start: addDays(new Date(), -13), end: addDays(new Date(), 1) }).then((res) => (res.length === 0 ? [{ date: new Date(), total: 0 }] : res)); + +export const getAppChatLogs = (data: RequestPaging & { appId: string }) => + POST(`/chat/getChatLogs`, data); diff --git a/client/src/components/ChatBox/QuoteModal.tsx b/client/src/components/ChatBox/QuoteModal.tsx index 909cf7a17..39968b891 100644 --- a/client/src/components/ChatBox/QuoteModal.tsx +++ b/client/src/components/ChatBox/QuoteModal.tsx @@ -96,6 +96,7 @@ const QuoteModal = ({ _notLast={{ mb: 2 }} position={'relative'} _hover={{ '& .edit': { display: 'flex' } }} + overflow={'hidden'} > {item.source && ({item.source})} {item.q} diff --git a/client/src/components/ChatBox/index.tsx b/client/src/components/ChatBox/index.tsx index 64fcee4f6..0a3887ad9 100644 --- a/client/src/components/ChatBox/index.tsx +++ b/client/src/components/ChatBox/index.tsx @@ -141,7 +141,7 @@ const ChatBox = ( variableModules?: VariableItemType[]; welcomeText?: string; onUpdateVariable?: (e: Record) => void; - onStartChat: (e: StartChatFnProps) => Promise<{ + onStartChat?: (e: StartChatFnProps) => Promise<{ responseText: string; [TaskResponseKeyEnum.responseData]: ChatHistoryItemResType[]; }>; @@ -239,8 +239,7 @@ const ChatBox = ( // 复制内容 const onclickCopy = useCallback( (value: string) => { - const val = value.replace(/\n+/g, '\n'); - copyData(val); + copyData(value); }, [copyData] ); @@ -264,6 +263,7 @@ const ChatBox = ( */ const sendPrompt = useCallback( async (variables: Record = {}, inputVal = '') => { + if (!onStartChat) return; if (isChatting) { toast({ title: '正在聊天中...请等待结束', @@ -272,7 +272,7 @@ const ChatBox = ( return; } // get input value - const val = inputVal.trim().replace(/\n\s*/g, '\n'); + const val = inputVal.trim(); if (!val) { toast({ @@ -698,7 +698,7 @@ const ChatBox = ( {/* input */} - {variableIsFinish ? ( + {onStartChat && variableIsFinish ? ( + + + + + \ No newline at end of file diff --git a/client/src/components/Icon/index.tsx b/client/src/components/Icon/index.tsx index 6f68bc736..b418eac2b 100644 --- a/client/src/components/Icon/index.tsx +++ b/client/src/components/Icon/index.tsx @@ -76,7 +76,8 @@ const map = { addCircle: require('./icons/circle/add.svg').default, playFill: require('./icons/fill/play.svg').default, courseLight: require('./icons/light/course.svg').default, - promotionLight: require('./icons/light/promotion.svg').default + promotionLight: require('./icons/light/promotion.svg').default, + logsLight: require('./icons/light/logs.svg').default }; export type IconName = keyof typeof map; diff --git a/client/src/constants/chat.ts b/client/src/constants/chat.ts index ceb8398f3..659d21384 100644 --- a/client/src/constants/chat.ts +++ b/client/src/constants/chat.ts @@ -31,7 +31,7 @@ export const ChatRoleMap = { }; export enum ChatSourceEnum { - 'test' = 'test', + test = 'test', online = 'online', share = 'share', api = 'api' @@ -39,16 +39,16 @@ export enum ChatSourceEnum { export const ChatSourceMap = { [ChatSourceEnum.test]: { - name: '调试测试' + name: 'chat.logs.test' }, [ChatSourceEnum.online]: { - name: '在线使用' + name: 'chat.logs.online' }, [ChatSourceEnum.share]: { - name: '链接分享' + name: 'chat.logs.share' }, [ChatSourceEnum.api]: { - name: 'API调用' + name: 'chat.logs.api' } }; @@ -56,7 +56,8 @@ export enum ChatModuleEnum { 'AIChat' = 'AI Chat', 'KBSearch' = 'KB Search', 'CQ' = 'Classify Question', - 'Extract' = 'Content Extract' + 'Extract' = 'Content Extract', + 'Http' = 'Http' } export enum OutLinkTypeEnum { diff --git a/client/src/constants/flow/ModuleTemplate.ts b/client/src/constants/flow/ModuleTemplate.ts index 207393272..6d7e9042b 100644 --- a/client/src/constants/flow/ModuleTemplate.ts +++ b/client/src/constants/flow/ModuleTemplate.ts @@ -158,7 +158,8 @@ export const ChatModule: FlowModuleTemplateType = { label: '系统提示词', valueType: FlowValueTypeEnum.string, description: ChatModelSystemTip, - placeholder: ChatModelSystemTip + placeholder: ChatModelSystemTip, + value: '' }, { key: 'limitPrompt', @@ -166,7 +167,8 @@ export const ChatModule: FlowModuleTemplateType = { valueType: FlowValueTypeEnum.string, label: '限定词', description: ChatModelLimitTip, - placeholder: ChatModelLimitTip + placeholder: ChatModelLimitTip, + value: '' }, Input_Template_TFSwitch, { @@ -281,6 +283,7 @@ export const AnswerModule: FlowModuleTemplateType = { key: SpecialInputKeyEnum.answerText, type: FlowInputItemTypeEnum.textarea, valueType: FlowValueTypeEnum.string, + value: '', label: '回复的内容', description: '可以使用 \\n 来实现换行。也可以通过外部模块输入实现回复,外部模块输入时会覆盖当前填写的内容' @@ -337,6 +340,7 @@ export const ClassifyQuestionModule: FlowModuleTemplateType = { key: 'systemPrompt', type: FlowInputItemTypeEnum.textarea, valueType: FlowValueTypeEnum.string, + value: '', label: '系统提示词', description: '你可以添加一些特定内容的介绍,从而更好的识别用户的问题类型。这个内容通常是给模型介绍一个它不知道的内容。', @@ -398,6 +402,7 @@ export const ContextExtractModule: FlowModuleTemplateType = { key: ContextExtractEnum.description, type: FlowInputItemTypeEnum.textarea, valueType: FlowValueTypeEnum.string, + value: '', label: '提取要求描述', description: '写一段提取要求,告诉 AI 需要提取哪些内容', required: true, diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx index c51694ed3..5f9fc5cd0 100644 --- a/client/src/pages/_app.tsx +++ b/client/src/pages/_app.tsx @@ -28,7 +28,7 @@ const queryClient = new QueryClient({ queries: { refetchOnWindowFocus: false, retry: false, - cacheTime: 0 + cacheTime: 10 } } }); diff --git a/client/src/pages/api/chat/getChatLogs.ts b/client/src/pages/api/chat/getChatLogs.ts new file mode 100644 index 000000000..ffc6e5fee --- /dev/null +++ b/client/src/pages/api/chat/getChatLogs.ts @@ -0,0 +1,72 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { Chat, ChatItem, connectToDatabase } from '@/service/mongo'; +import { authUser } from '@/service/utils/auth'; +import type { PagingData } from '@/types'; +import { AppLogsListItemType } from '@/types/app'; +import { Types } from 'mongoose'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { + pageNum = 1, + pageSize = 20, + appId + } = req.body as { pageNum: number; pageSize: number; appId: string }; + + if (!appId) { + throw new Error('缺少参数'); + } + await connectToDatabase(); + + // 凭证校验 + const { userId } = await authUser({ req, authToken: true }); + + const where = { + appId: new Types.ObjectId(appId), + userId: new Types.ObjectId(userId) + }; + + 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' + } + }, + { + $project: { + id: '$chatId', + title: 1, + source: 1, + time: '$updateTime', + messageCount: { $size: '$messageCount' }, + callbackCount: { $literal: 0 } + } + } + ]), + Chat.countDocuments(where) + ]); + + jsonRes>(res, { + data: { + pageNum, + pageSize, + data, + total + } + }); + } catch (error) { + jsonRes(res, { + code: 500, + error + }); + } +} diff --git a/client/src/pages/api/openapi/v1/chat/completions.ts b/client/src/pages/api/openapi/v1/chat/completions.ts index 7790646dd..fed165547 100644 --- a/client/src/pages/api/openapi/v1/chat/completions.ts +++ b/client/src/pages/api/openapi/v1/chat/completions.ts @@ -27,6 +27,7 @@ import { pushTaskBill } from '@/service/events/pushBill'; import { BillSourceEnum } from '@/constants/user'; import { ChatHistoryItemResType } from '@/types/chat'; import { UserModelSchema } from '@/types/mongoSchema'; +import { SystemInputEnum } from '@/constants/app'; export type MessageItemType = ChatCompletionRequestMessage & { dataId?: string }; type FastGptWebChatProps = { @@ -302,6 +303,8 @@ export async function dispatchModules({ if (!set.has(module.moduleId) && checkInputFinish()) { set.add(module.moduleId); + // remove switch + updateInputValue(SystemInputEnum.switch, undefined); return moduleRun(module); } }) @@ -324,6 +327,7 @@ export async function dispatchModules({ // find module const targetModule = runningModules.find((item) => item.moduleId === target.moduleId); if (!targetModule) return; + return moduleInput(targetModule, { [target.key]: outputItem.value }); }) ); @@ -332,7 +336,6 @@ export async function dispatchModules({ } async function moduleRun(module: RunningModuleItemType): Promise { if (res.closed) return Promise.resolve(); - // console.log('run=========', module.flowType); if (stream && detail && module.showStatus) { responseStatus({ diff --git a/client/src/pages/api/plugins/urlFetch.ts b/client/src/pages/api/plugins/urlFetch.ts index 1ff3df6a4..2403f3b02 100644 --- a/client/src/pages/api/plugins/urlFetch.ts +++ b/client/src/pages/api/plugins/urlFetch.ts @@ -12,7 +12,7 @@ export type UrlFetchResponse = FetchResultItem[]; const fetchContent = async (req: NextApiRequest, res: NextApiResponse) => { try { - const { urlList = [] } = req.body as { urlList: string[] }; + let { urlList = [] } = req.body as { urlList: string[] }; if (!urlList || urlList.length === 0) { throw new Error('urlList is empty'); @@ -20,27 +20,36 @@ const fetchContent = async (req: NextApiRequest, res: NextApiResponse) => { await authUser({ req }); + urlList = urlList.filter((url) => /^(http|https):\/\/[^ "]+$/.test(url)); + const response = ( await Promise.allSettled( urlList.map(async (url) => { - const fetchRes = await axios.get(url, { - timeout: 30000 - }); + try { + const fetchRes = await axios.get(url, { + timeout: 30000 + }); - const dom = new JSDOM(fetchRes.data, { - url, - contentType: 'text/html' - }); + const dom = new JSDOM(fetchRes.data, { + url, + contentType: 'text/html' + }); - const reader = new Readability(dom.window.document); - const article = reader.parse(); + const reader = new Readability(dom.window.document); + const article = reader.parse(); - const content = article?.textContent || ''; + const content = article?.textContent || ''; - return { - url, - content: simpleText(`${article?.title}\n${content}`) - }; + return { + url, + content: simpleText(`${article?.title}\n${content}`) + }; + } catch (error) { + return { + url, + content: '' + }; + } }) ) ) diff --git a/client/src/pages/app/detail/components/Logs.tsx b/client/src/pages/app/detail/components/Logs.tsx new file mode 100644 index 000000000..0b6066da4 --- /dev/null +++ b/client/src/pages/app/detail/components/Logs.tsx @@ -0,0 +1,237 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { + Flex, + Box, + TableContainer, + Table, + Thead, + Tr, + Th, + Td, + Tbody, + useTheme +} from '@chakra-ui/react'; +import MyIcon from '@/components/Icon'; +import { useTranslation } from 'next-i18next'; +import { usePagination } from '@/hooks/usePagination'; +import { getAppChatLogs } from '@/api/app'; +import dayjs from 'dayjs'; +import { ChatSourceMap, HUMAN_ICON } from '@/constants/chat'; +import { AppLogsListItemType } from '@/types/app'; +import { useGlobalStore } from '@/store/global'; +import MyTooltip from '@/components/MyTooltip'; +import ChatBox, { type ComponentRef } from '@/components/ChatBox'; +import { useQuery } from '@tanstack/react-query'; +import { getInitChatSiteInfo } from '@/api/chat'; +import Tag from '@/components/Tag'; + +const Logs = ({ appId }: { appId: string }) => { + const { t } = useTranslation(); + const { isPc } = useGlobalStore(); + + const { + data: logs, + isLoading, + Pagination, + getData, + pageNum + } = usePagination({ + api: getAppChatLogs, + pageSize: 20, + params: { + appId + } + }); + + const [detailLogsId, setDetailLogsId] = useState(); + + return ( + + + {isPc && ( + <> + + {t('app.Chat logs')} + + + {t('app.Chat Logs Tips')} + + + )} + + + {/* table */} + + + + + + + + + + + + {logs.map((item) => ( + setDetailLogsId(item.id)} + > + + + + + + ))} + +
{t('app.Logs Source')}{t('app.Logs Time')}{t('app.Logs Title')}{t('app.Logs Message Total')}
{t(ChatSourceMap[item.source]?.name || 'UnKnow')}{dayjs(item.time).format('YYYY/MM/DD HH:mm')} + {item.title} + {item.messageCount}
+
+ + + + {logs.length === 0 && !isLoading && ( + + + + {t('app.Logs Empty')} + + + )} + {!!detailLogsId && ( + setDetailLogsId(undefined)} + /> + )} +
+ ); +}; + +export default Logs; + +function DetailLogsModal({ + appId, + chatId, + onClose +}: { + appId: string; + chatId: string; + onClose: () => void; +}) { + const ChatBoxRef = useRef(null); + const { isPc } = useGlobalStore(); + const theme = useTheme(); + + const { data: chat } = useQuery( + ['getChatDetail', chatId], + () => getInitChatSiteInfo({ appId, chatId }), + { + onSuccess(res) { + const history = res.history.map((item) => ({ + ...item, + status: 'finish' as any + })); + ChatBoxRef.current?.resetHistory(history); + ChatBoxRef.current?.resetVariables(res.variables); + if (res.history.length > 0) { + setTimeout(() => { + ChatBoxRef.current?.scrollToBottom('auto'); + }, 500); + } + } + } + ); + + const history = useMemo(() => (chat?.history ? chat.history : []), [chat]); + + const title = useMemo(() => { + return history[history.length - 2]?.value?.slice(0, 8); + }, [history]); + + return ( + <> + + + {isPc ? ( + <> + + {title} + + + + {`${history.length}条记录`} + + {!!chat?.app?.chatModels && ( + + + {chat.app.chatModels.join(',')} + + )} + + + ) : ( + <> + + + {title} + + + + )} + + + + + + + {}} + /> + + + + + ); +} diff --git a/client/src/pages/app/detail/index.tsx b/client/src/pages/app/detail/index.tsx index ad4520e7b..56909a839 100644 --- a/client/src/pages/app/detail/index.tsx +++ b/client/src/pages/app/detail/index.tsx @@ -26,11 +26,15 @@ const OutLink = dynamic(() => import('./components/OutLink'), { const API = dynamic(() => import('./components/API'), { ssr: false }); +const Logs = dynamic(() => import('./components/Logs'), { + ssr: false +}); enum TabEnum { 'basicEdit' = 'basicEdit', 'adEdit' = 'adEdit', 'outLink' = 'outLink', + 'logs' = 'logs', 'API' = 'API' } @@ -59,6 +63,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { { label: '高级编排', id: TabEnum.adEdit, icon: 'settingLight' }, { label: '外部使用', id: TabEnum.outLink, icon: 'shareLight' }, { label: 'API访问', id: TabEnum.API, icon: 'apiLight' }, + { label: '对话日志', id: TabEnum.logs, icon: 'logsLight' }, { label: '立即对话', id: 'startChat', icon: 'chat' } ], [] @@ -148,7 +153,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { {/* phone tab */} - + {appDetail.name} @@ -174,6 +179,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { setCurrentTab(TabEnum.basicEdit)} /> )} {currentTab === TabEnum.API && } + {currentTab === TabEnum.logs && } {currentTab === TabEnum.outLink && } diff --git a/client/src/pages/kb/detail/components/Import/Chunk.tsx b/client/src/pages/kb/detail/components/Import/Chunk.tsx index d4a107156..cc83e2643 100644 --- a/client/src/pages/kb/detail/components/Import/Chunk.tsx +++ b/client/src/pages/kb/detail/components/Import/Chunk.tsx @@ -132,7 +132,7 @@ const ChunkImport = ({ kbId }: { kbId: string }) => { }; return ( - + { {!emptyFiles && ( <> - + {files.map((item) => ( { maxW: '400px' }; return ( - + { {!emptyFiles && ( <> - + {files.map((item) => ( { }); return ( - + {'匹配的知识点'} diff --git a/client/src/pages/kb/detail/components/Import/QA.tsx b/client/src/pages/kb/detail/components/Import/QA.tsx index 51be31ae3..611fcd665 100644 --- a/client/src/pages/kb/detail/components/Import/QA.tsx +++ b/client/src/pages/kb/detail/components/Import/QA.tsx @@ -123,7 +123,7 @@ const QAImport = ({ kbId }: { kbId: string }) => { }; return ( - + { {!emptyFiles && ( <> - + {files.map((item) => ( ) => { const { maxContext = 5, history = [] } = props as HistoryProps; return { - history: history.slice(-maxContext) + history: maxContext > 0 ? history.slice(-maxContext) : [] }; }; diff --git a/client/src/service/moduleDispatch/tools/http.ts b/client/src/service/moduleDispatch/tools/http.ts index 10c963102..fb0d927f0 100644 --- a/client/src/service/moduleDispatch/tools/http.ts +++ b/client/src/service/moduleDispatch/tools/http.ts @@ -1,4 +1,6 @@ +import { ChatModuleEnum, TaskResponseKeyEnum } from '@/constants/chat'; import { HttpPropsEnum } from '@/constants/flow/flowField'; +import { ChatHistoryItemResType } from '@/types/chat'; import type { NextApiResponse } from 'next'; export type HttpRequestProps = { @@ -11,6 +13,7 @@ export type HttpRequestProps = { export type HttpResponse = { [HttpPropsEnum.finish]: boolean; [HttpPropsEnum.failed]?: boolean; + [TaskResponseKeyEnum.responseData]: ChatHistoryItemResType; [key: string]: any; }; @@ -22,12 +25,22 @@ export const dispatchHttpRequest = async (props: Record): Promise { +export const connectPg = async (): Promise => { if (global.pgClient) { return global.pgClient; } @@ -17,6 +17,7 @@ export const connectPg = async () => { global.pgClient.on('error', (err) => { console.log(err); global.pgClient = null; + connectPg(); }); try { @@ -25,7 +26,7 @@ export const connectPg = async () => { return global.pgClient; } catch (error) { global.pgClient = null; - return Promise.reject(error); + return connectPg(); } }; diff --git a/client/src/types/app.d.ts b/client/src/types/app.d.ts index 74b4c8f43..36b80ffcf 100644 --- a/client/src/types/app.d.ts +++ b/client/src/types/app.d.ts @@ -7,7 +7,7 @@ import { VariableInputEnum } from '../constants/app'; import type { FlowInputItemType, FlowOutputItemType, FlowOutputTargetItemType } from './flow'; -import type { AppSchema, kbSchema } from './mongoSchema'; +import type { AppSchema, ChatSchema, kbSchema } from './mongoSchema'; import { ChatModelType } from '@/constants/model'; import { FlowValueTypeEnum } from '@/constants/flow'; @@ -107,3 +107,12 @@ export type RunningModuleItemType = { }[]; }[]; }; + +export type AppLogsListItemType = { + id: string; + source: ChatSchema['source']; + time: Date; + title: string; + messageCount: number; + callbackCount: number; +}; diff --git a/client/src/types/chat.d.ts b/client/src/types/chat.d.ts index 446e4e377..a501163d2 100644 --- a/client/src/types/chat.d.ts +++ b/client/src/types/chat.d.ts @@ -71,4 +71,7 @@ export type ChatHistoryItemResType = { // content extract extractDescription?: string; extractResult?: Record; + + // http + httpResult?: Record; }; diff --git a/docSite/vercel.json b/docSite/vercel.json index 92b3c64fb..417017b90 100644 --- a/docSite/vercel.json +++ b/docSite/vercel.json @@ -6,7 +6,7 @@ }, { "source": "/imgs/:path*", - "destination": "https://cdn.jsdelivr.us/gh/yangchuansheng/FastGPT@main/docSite/assets/imgs/:path*" + "destination": "https://cdn.jsdelivr.us/gh/labring/FastGPT@main/docSite/assets/imgs/:path*" } ], "headers": [