diff --git a/client/package.json b/client/package.json index 9a2314ed9..206f86cce 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "cookie": "^0.5.0", "crypto": "^1.0.1", "date-fns": "^2.30.0", + "echarts": "^5.4.1", "dayjs": "^1.11.7", "formidable": "^2.1.1", "framer-motion": "^9.0.6", diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index 47c626787..a9274b9c7 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -56,6 +56,9 @@ dependencies: dayjs: specifier: ^1.11.7 version: registry.npmmirror.com/dayjs@1.11.7 + echarts: + specifier: ^5.4.1 + version: registry.npmmirror.com/echarts@5.4.1 formidable: specifier: ^2.1.1 version: registry.npmmirror.com/formidable@2.1.1 @@ -7341,6 +7344,15 @@ packages: safe-buffer: registry.npmmirror.com/safe-buffer@5.2.1 dev: false + registry.npmmirror.com/echarts@5.4.1: + resolution: {integrity: sha512-9ltS3M2JB0w2EhcYjCdmtrJ+6haZcW6acBolMGIuf01Hql1yrIV01L1aRj7jsaaIULJslEP9Z3vKlEmnJaWJVQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/echarts/-/echarts-5.4.1.tgz} + name: echarts + version: 5.4.1 + dependencies: + tslib: registry.npmmirror.com/tslib@2.3.0 + zrender: registry.npmmirror.com/zrender@5.4.1 + dev: false + registry.npmmirror.com/ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz} name: ee-first @@ -11983,6 +11995,12 @@ packages: name: tslib version: 1.14.1 + registry.npmmirror.com/tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz} + name: tslib + version: 2.3.0 + dev: false + registry.npmmirror.com/tslib@2.4.0: resolution: {integrity: sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/tslib/-/tslib-2.4.0.tgz} name: tslib @@ -12580,6 +12598,14 @@ packages: engines: {node: '>=10'} dev: true + registry.npmmirror.com/zrender@5.4.1: + resolution: {integrity: sha512-M4Z05BHWtajY2241EmMPHglDQAJ1UyHQcYsxDNzD9XLSkPDqMq4bB28v9Pb4mvHnVQ0GxyTklZ/69xCFP6RXBA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/zrender/-/zrender-5.4.1.tgz} + name: zrender + version: 5.4.1 + dependencies: + tslib: registry.npmmirror.com/tslib@2.3.0 + dev: false + registry.npmmirror.com/zustand@4.3.5(immer@9.0.19)(react@18.2.0): resolution: {integrity: sha512-2iPUzfwx+g3f0PagOMz2vDO9mZzEp2puFpNe7vrAymVPOEIEUjCPkC4/zy84eAscxIWmTU4j9g6upXYkJdzEFQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/zustand/-/zustand-4.3.5.tgz} id: registry.npmmirror.com/zustand/4.3.5 diff --git a/client/src/api/app.ts b/client/src/api/app.ts index c8cfdc54e..deb4c7b2b 100644 --- a/client/src/api/app.ts +++ b/client/src/api/app.ts @@ -3,6 +3,7 @@ import type { AppSchema } from '@/types/mongoSchema'; import type { AppListItemType, AppUpdateParams } from '@/types/app'; import { RequestPaging } from '../types/index'; import type { Props as CreateAppProps } from '@/pages/api/app/create'; +import { addDays } from 'date-fns'; /** * 获取模型列表 @@ -42,3 +43,11 @@ export const getShareModelList = (data: { searchText?: string } & RequestPaging) */ export const triggerModelCollection = (appId: string) => POST(`/app/share/collection?appId=${appId}`); + +// ====================== data +export const getTokenUsage = (data: { appId: string }) => + POST<{ tokenLen: number; date: Date }[]>(`/app/data/tokenUsage`, { + ...data, + start: addDays(new Date(), -7), + end: new Date() + }); diff --git a/client/src/api/chat.ts b/client/src/api/chat.ts index 15d704a2f..221a55bb8 100644 --- a/client/src/api/chat.ts +++ b/client/src/api/chat.ts @@ -28,15 +28,15 @@ export const delChatHistoryById = (id: string) => GET(`/chat/removeHistory?id=${ /** * get history quotes */ -export const getHistoryQuote = (params: { chatId: string; historyId: string }) => +export const getHistoryQuote = (params: { historyId: string; contentId: string }) => GET<(QuoteItemType & { _id: string })[]>(`/chat/history/getHistoryQuote`, params); /** * update history quote status */ export const updateHistoryQuote = (params: { - chatId: string; historyId: string; + contentId: string; quoteId: string; sourceText: string; }) => GET(`/chat/history/updateHistoryQuote`, params); diff --git a/client/src/components/Charts/Line.tsx b/client/src/components/Charts/Line.tsx new file mode 100644 index 000000000..5e97120ff --- /dev/null +++ b/client/src/components/Charts/Line.tsx @@ -0,0 +1,199 @@ +import React, { useEffect, useMemo, useRef } from 'react'; +import * as echarts from 'echarts'; +import { useGlobalStore } from '@/store/global'; + +const LineChart = ({ + type, + limit = 1000000, + data +}: { + type: 'blue' | 'deepBlue' | 'green' | 'purple'; + limit: number; + data: number[]; +}) => { + const { screenWidth } = useGlobalStore(); + + const Dom = useRef(null); + const myChart = useRef(); + + const map = { + blue: { + backgroundColor: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(3, 190, 232, 0.42)' // 0% 处的颜色 + }, + { + offset: 1, + color: 'rgba(0, 182, 240, 0)' + } + ], + global: false // 缺省为 false + }, + lineColor: '#36ADEF' + }, + deepBlue: { + backgroundColor: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(47, 112, 237, 0.42)' // 0% 处的颜色 + }, + { + offset: 1, + color: 'rgba(94, 159, 235, 0)' + } + ], + global: false + }, + lineColor: '#3293EC' + }, + purple: { + backgroundColor: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(211, 190, 255, 0.42)' // 0% 处的颜色 + }, + { + offset: 1, + color: 'rgba(52, 60, 255, 0)' + } + ], + global: false // 缺省为 false + }, + lineColor: '#8172D8' + }, + green: { + backgroundColor: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(4, 209, 148, 0.42)' // 0% 处的颜色 + }, + { + offset: 1, + color: 'rgba(19, 217, 181, 0)' + } + ], + global: false // 缺省为 false + }, + lineColor: '#00A9A6', + max: 100 + } + }; + + const option = useMemo( + () => ({ + xAxis: { + type: 'category', + show: false, + boundaryGap: false, + data: data.map((_, i) => i) + }, + yAxis: { + type: 'value', + boundaryGap: false, + splitNumber: 2, + max: 100, + min: 0 + }, + grid: { + show: false, + left: 0, + right: 0, + top: 0, + bottom: 2 + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'line' + }, + formatter: (e: any[]) => `${e[0]?.value || 0}%` + }, + series: [ + { + data: new Array(data.length).fill(0), + type: 'line', + showSymbol: false, + smooth: true, + animationDuration: 300, + animationEasingUpdate: 'linear', + areaStyle: { + color: map[type].backgroundColor + }, + lineStyle: { + width: '1', + color: map[type].lineColor + }, + itemStyle: { + width: 1.5, + color: map[type].lineColor + }, + emphasis: { + // highlight + disabled: true + } + } + ] + }), + [limit, type] + ); + + // init chart + useEffect(() => { + if (!Dom.current || myChart?.current?.getOption()) return; + myChart.current = echarts.init(Dom.current); + myChart.current && myChart.current.setOption(option); + }, [Dom]); + + // data changed, update + useEffect(() => { + if (!myChart.current || !myChart?.current?.getOption()) return; + + const uniData = data.map((item) => ((item / limit) * 100).toFixed(2)); + + const x = option.xAxis.data; + option.xAxis.data = [...x.slice(1), x[x.length - 1] + 1]; + option.series[0].data = uniData; + myChart.current.setOption(option); + }, [data, limit]); + + // limit changed, update + useEffect(() => { + if (!myChart.current || !myChart?.current?.getOption()) return; + myChart.current.setOption(option); + }, [limit, option, type]); + + // resize chart + useEffect(() => { + if (!myChart.current || !myChart.current.getOption()) return; + myChart.current.resize(); + }, [screenWidth]); + + return
; +}; + +export default React.memo(LineChart); diff --git a/client/src/components/Icon/icons/model.svg b/client/src/components/Icon/icons/app.svg similarity index 100% rename from client/src/components/Icon/icons/model.svg rename to client/src/components/Icon/icons/app.svg diff --git a/client/src/components/Icon/icons/fill/chat.svg b/client/src/components/Icon/icons/fill/chat.svg index 042a434f5..08f301463 100644 --- a/client/src/components/Icon/icons/fill/chat.svg +++ b/client/src/components/Icon/icons/fill/chat.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/client/src/components/Icon/icons/light/chat.svg b/client/src/components/Icon/icons/light/chat.svg index 314547a0e..aa20dd8d3 100644 --- a/client/src/components/Icon/icons/light/chat.svg +++ b/client/src/components/Icon/icons/light/chat.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/client/src/components/Icon/icons/light/fullScreen.svg b/client/src/components/Icon/icons/light/fullScreen.svg new file mode 100644 index 000000000..4d477da5f --- /dev/null +++ b/client/src/components/Icon/icons/light/fullScreen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/components/Icon/icons/phoneTabbar/model.svg b/client/src/components/Icon/icons/phoneTabbar/app.svg similarity index 100% rename from client/src/components/Icon/icons/phoneTabbar/model.svg rename to client/src/components/Icon/icons/phoneTabbar/app.svg diff --git a/client/src/components/Icon/icons/save.svg b/client/src/components/Icon/icons/save.svg index 8cdbca686..287458c0f 100644 --- a/client/src/components/Icon/icons/save.svg +++ b/client/src/components/Icon/icons/save.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/client/src/components/Icon/index.tsx b/client/src/components/Icon/index.tsx index 6aee39201..ce3273654 100644 --- a/client/src/components/Icon/index.tsx +++ b/client/src/components/Icon/index.tsx @@ -3,7 +3,7 @@ import type { IconProps } from '@chakra-ui/react'; import { Icon } from '@chakra-ui/react'; const map = { - model: require('./icons/model.svg').default, + app: require('./icons/app.svg').default, copy: require('./icons/copy.svg').default, chatSend: require('./icons/chatSend.svg').default, delete: require('./icons/delete.svg').default, @@ -16,7 +16,7 @@ const map = { backFill: require('./icons/fill/back.svg').default, more: require('./icons/more.svg').default, tabbarChat: require('./icons/phoneTabbar/chat.svg').default, - tabbarModel: require('./icons/phoneTabbar/model.svg').default, + tabbarModel: require('./icons/phoneTabbar/app.svg').default, tabbarMore: require('./icons/phoneTabbar/more.svg').default, tabbarMe: require('./icons/phoneTabbar/me.svg').default, closeSolid: require('./icons/closeSolid.svg').default, @@ -50,6 +50,7 @@ const map = { welcomeText: require('./icons/modules/welcomeText.svg').default, variable: require('./icons/modules/variable.svg').default, setTop: require('./icons/light/setTop.svg').default, + fullScreenLight: require('./icons/light/fullScreen.svg').default, voice: require('./icons/voice.svg').default }; diff --git a/client/src/components/Layout/navbar.tsx b/client/src/components/Layout/navbar.tsx index 7dfe824f7..00204c4f0 100644 --- a/client/src/components/Layout/navbar.tsx +++ b/client/src/components/Layout/navbar.tsx @@ -16,21 +16,21 @@ export enum NavbarTypeEnum { const Navbar = ({ unread }: { unread: number }) => { const router = useRouter(); - const { userInfo, lastModelId } = useUserStore(); - const { lastChatAppId, lastChatId } = useChatStore(); + const { userInfo } = useUserStore(); + const { lastChatAppId, lastHistoryId } = useChatStore(); const navbarList = useMemo( () => [ { label: '聊天', icon: 'chatLight', activeIcon: 'chatFill', - link: `/chat?appId=${lastChatAppId}&chatId=${lastChatId}`, + link: `/chat?appId=${lastChatAppId}&historyId=${lastHistoryId}`, activeLink: ['/chat'] }, { label: '应用', icon: 'tabbarModel', - activeIcon: 'model', + activeIcon: 'app', link: `/app/list`, activeLink: ['/app/list', '/app/detail'] }, @@ -56,7 +56,7 @@ const Navbar = ({ unread }: { unread: number }) => { activeLink: ['/number'] } ], - [lastChatId, lastChatAppId] + [lastHistoryId, lastChatAppId] ); const itemStyles: any = { @@ -99,10 +99,8 @@ const Navbar = ({ unread }: { unread: number }) => { {/* 导航列表 */} {navbarList.map((item) => ( - { color: 'myGray.500', backgroundColor: 'transparent' })} + onClick={() => router.push(item.link)} > { {item.label} - + ))} {unread > 0 && ( diff --git a/client/src/components/Layout/navbarPhone.tsx b/client/src/components/Layout/navbarPhone.tsx index be9150490..de4948b55 100644 --- a/client/src/components/Layout/navbarPhone.tsx +++ b/client/src/components/Layout/navbarPhone.tsx @@ -7,13 +7,13 @@ import Badge from '../Badge'; const NavbarPhone = ({ unread }: { unread: number }) => { const router = useRouter(); - const { lastChatAppId, lastChatId } = useChatStore(); + const { lastChatAppId, lastHistoryId } = useChatStore(); const navbarList = useMemo( () => [ { label: '聊天', icon: 'tabbarChat', - link: `/chat?appId=${lastChatAppId}&chatId=${lastChatId}`, + link: `/chat?appId=${lastChatAppId}&historyId=${lastHistoryId}`, activeLink: ['/chat'], unread: 0 }, @@ -39,7 +39,7 @@ const NavbarPhone = ({ unread }: { unread: number }) => { unread } ], - [lastChatId, lastChatAppId, unread] + [lastHistoryId, lastChatAppId, unread] ); return ( diff --git a/client/src/constants/app.ts b/client/src/constants/app.ts index 3f4ff8ab4..ff09156b3 100644 --- a/client/src/constants/app.ts +++ b/client/src/constants/app.ts @@ -1127,563 +1127,3 @@ export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = ] } ]; - -// export const classifyQuestionDemo: AppItemType = { -// id: 'classifyQuestionDemo', -// // 标记字段 -// modules: [ -// { -// moduleId: '1', -// type: AppModuleItemTypeEnum.http, -// url: '/openapi/modules/agent/classifyQuestion', -// body: { -// systemPrompt: -// 'laf 一个云函数开发平台,提供了基于 Node 的 serveless 的快速开发和部署。是一个集「函数计算」、「数据库」、「对象存储」等于一身的一站式开发平台。支持云函数、云数据库、在线编程 IDE、触发器、云存储和静态网站托管等功能。', -// agents: [ -// { -// desc: '打招呼、问候、身份询问等问题', -// key: 'a' -// }, -// { -// desc: "询问 'laf 使用和介绍的问题'", -// key: 'b' -// }, -// { -// desc: "询问 'laf 代码问题'", -// key: 'c' -// }, -// { -// desc: '其他问题', -// key: 'd' -// } -// ] -// }, -// inputs: [ -// { -// key: SystemInputEnum.history, -// value: undefined -// }, -// { -// key: SystemInputEnum.userChatInput, -// value: undefined -// } -// ], -// outputs: [ -// { -// key: 'a', -// value: undefined, -// targets: [ -// { -// moduleId: 'a', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'b', -// value: undefined, -// targets: [ -// { -// moduleId: 'b', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'c', -// value: undefined, -// targets: [ -// { -// moduleId: 'c', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'd', -// value: undefined, -// targets: [ -// { -// moduleId: 'd', -// key: SystemInputEnum.switch -// } -// ] -// } -// ] -// }, -// { -// moduleId: 'a', -// type: 'answer', -// body: {}, -// inputs: [ -// { -// key: SpecificInputEnum.answerText, -// value: '你好,我是 Laf 助手,有什么可以帮助你的?' -// }, -// { -// key: SystemInputEnum.switch, -// value: undefined -// } -// ], -// outputs: [] -// }, -// // laf 知识库 -// { -// moduleId: 'b', -// type: 'http', -// url: '/openapi/modules/kb/search', -// body: { -// kb_ids: ['646627f4f7b896cfd8910e24'], -// similarity: 0.82, -// limit: 4, -// maxToken: 2500 -// }, -// inputs: [ -// { -// key: SystemInputEnum.switch, -// value: undefined -// }, -// { -// key: SystemInputEnum.history, -// value: undefined -// }, -// { -// key: SystemInputEnum.userChatInput, -// value: undefined -// } -// ], -// outputs: [ -// { -// key: 'rawSearch', -// value: undefined, -// response: true, -// targets: [] -// }, -// { -// key: 'quotePrompt', -// value: undefined, -// targets: [ -// { -// moduleId: 'lafchat', -// key: 'quotePrompt' -// } -// ] -// } -// ] -// }, -// // laf 对话 -// { -// moduleId: 'lafchat', -// type: 'http', -// url: '/openapi/modules/chat/gpt', -// body: { -// model: 'gpt-3.5-turbo-16k', -// temperature: 5, -// maxToken: 4000, -// systemPrompt: '知识库是关于 Laf 的内容。', -// limitPrompt: '你仅能参考知识库的内容回答问题,不能超出知识库范围。' -// }, -// inputs: [ -// { -// key: 'quotePrompt', -// value: undefined -// }, -// { -// key: SystemInputEnum.history, -// value: undefined -// }, -// { -// key: SystemInputEnum.userChatInput, -// value: undefined -// } -// ], -// outputs: [ -// { -// key: 'answer', -// answer: true, -// value: undefined, -// targets: [] -// } -// ] -// }, -// // laf 代码知识库 -// { -// moduleId: 'c', -// type: 'http', -// url: '/openapi/modules/kb/search', -// body: { -// kb_ids: ['646627f4f7b896cfd8910e26'], -// similarity: 0.8, -// limit: 4, -// maxToken: 2500 -// }, -// inputs: [ -// { -// key: SystemInputEnum.switch, -// value: undefined -// }, -// { -// key: SystemInputEnum.history, -// value: undefined -// }, -// { -// key: SystemInputEnum.userChatInput, -// value: undefined -// } -// ], -// outputs: [ -// { -// key: 'rawSearch', -// value: undefined, -// response: true, -// targets: [] -// }, -// { -// key: 'quotePrompt', -// value: undefined, -// targets: [ -// { -// moduleId: 'lafcodechat', -// key: 'quotePrompt' -// } -// ] -// } -// ] -// }, -// // laf代码对话 -// { -// moduleId: 'lafcodechat', -// type: 'http', -// url: '/openapi/modules/chat/gpt', -// body: { -// model: 'gpt-3.5-turbo-16k', -// temperature: 5, -// maxToken: 4000, -// systemPrompt: `下例是laf结构\n~~~ts\nimport cloud from '@lafjs/cloud'\nexport default async function(ctx: FunctionContext){\nreturn \"success\"\n};\n~~~\n下例是@lafjs/cloud的api\n~~~\ncloud.fetch//完全等同axios\ncloud.database()// 获取操作数据库实例,和mongo语法相似.\ncloud.getToken(payload)//获取token\ncloud.parseToken(token)//解析token\n// 下面是持久化缓存Api\ncloud.shared.set(key,val); //设置缓存,仅能设置值,无法设置过期时间\ncloud.shared.get(key);\ncloud.shared.has(key); \ncloud.shared.delete(key); \ncloud.shared.clear(); \n~~~\n下例是ctx对象\n~~~\nctx.requestId\nctx.method\nctx.headers//请求的 headers, ctx.headers.get('Content-Type')获取Content-Type的值\nctx.user//Http Bearer Token 认证时,获取token值\nctx.query\nctx.body\nctx.request//同express的Request\nctx.response//同express的Response\nctx.socket/WebSocket 实例\nctx.files//上传的文件 (File对象数组)\nctx.env//自定义的环境变量\n~~~\n下例是数据库获取数据\n~~~ts\nconst db = cloud.database();\nexport default async function(ctx: FunctionContext){\nconst {minMemory} = ctx.query\nconst _ = db.command;\nconst {data: users,total} = collection(\"users\")\n .where({//条件查询\n category: \"computer\",\n type: {\n memory: _gt(minMemory), \n }\n }) \n .skip(10)//跳过10条-分页时使用\n .limit(10)//仅返回10条\n .orderBy(\"name\", \"asc\") \n .orderBy(\"age\", \"desc\")\n .field({age:true,name: false})//返回age不返回name\n}\nconst {data:user} = db.where({phone:req.body.phone}).getOne()//获取一个满足条件的用户\nreturn {users,total}\n~~~\n下例是数据库添加数据\n~~~ts\nconst db = cloud.database();\nexport default async function(ctx: FunctionContext) {\n const {username} = ctx.body\n const {id:userId, ok} = await collection(\"users\")\n .add({\n username, \n })\n if(ok) return {userId}\n return {code:500,message:\"失败\"}\n}\n~~~\n下例是数据库更新数据\n~~~ts\nconst db = cloud.database();\nexport default async function(ctx: FunctionContext){\nconst {id} = req.query\n//id直接修改\nawait collection(\"user\").doc(\"id\").update({\n name: \"Hey\",\n});\n//批量更新\nawait collection\n .where({name:\"1234\"})\n .update({\n age:18\n })\nconst _ = db.command;\nawait collection(\"user\")\n .doc(id)\n .set({\n count: _.inc(1)\n count: _.mul(2)\n count: _.remove()\n users: _.push([\"aaa\", \"bbb\"])\n users: _.push(\"aaa\")\n users: _.pop()\n users: _.unshift()\n users: _.shift()\n })\n}\n~~~\n下例是删除数据库记录\n~~~ts\nconst db = cloud.database();\nexport default async function(ctx: FunctionContext){\nconst {id} = req.query\ncollection(\"user\").doc(id).remove();\n//批量删除\ncollection\n .where({age:18}) \n .remove({multi: true})\nreturn \"success\"\n}\n~~~\n你只需返回 ts 代码块!不需要说明.\n用户的问题与 Laf 代码无关时,你直接回答: \"我不确定,我只会写 Laf 代码。\"`, -// limitPrompt: -// '你是由 Laf 团队开发的代码助手,把我的需求用 Laf 代码实现.参考知识库中 Laf 的例子.' -// }, -// inputs: [ -// { -// key: 'quotePrompt', -// value: undefined -// }, -// { -// key: SystemInputEnum.history, -// value: undefined -// }, -// { -// key: SystemInputEnum.userChatInput, -// value: undefined -// } -// ], -// outputs: [ -// { -// key: 'answer', -// answer: true, -// value: undefined, -// targets: [] -// } -// ] -// }, -// { -// moduleId: 'd', -// type: 'answer', -// body: {}, -// inputs: [ -// { -// key: SpecificInputEnum.answerText, -// value: '你好,我没有理解你的意思,请问你有什么 Laf 相关的问题么?' -// }, -// { -// key: SystemInputEnum.switch, -// value: undefined -// } -// ], -// outputs: [] -// } -// ] -// }; - -// export const lafClassifyQuestionDemo: AppItemType = { -// id: 'test', -// // 标记字段 -// modules: [ -// { -// moduleId: '1', -// type: AppModuleItemTypeEnum.http, -// url: '/openapi/modules/agent/classifyQuestion', -// body: { -// systemPrompt: -// 'laf 一个云函数开发平台,提供了基于 Node 的 serveless 的快速开发和部署。是一个集「函数计算」、「数据库」、「对象存储」等于一身的一站式开发平台。支持云函数、云数据库、在线编程 IDE、触发器、云存储和静态网站托管等功能。\nsealos是一个 k8s 云平台,可以让用户快速部署云服务。', -// agents: [ -// { -// desc: '打招呼、问候、身份询问等问题', -// key: 'a' -// }, -// { -// desc: "询问 'laf 的使用和介绍'", -// key: 'b' -// }, -// { -// desc: "询问 'laf 代码相关问题'", -// key: 'c' -// }, -// { -// desc: "用户希望运行或知道 'laf 代码' 运行结果", -// key: 'g' -// }, -// { -// desc: "询问 'sealos 相关问题'", -// key: 'd' -// }, -// { -// desc: '其他问题', -// key: 'e' -// }, -// { -// desc: '商务类问题', -// key: 'f' -// } -// ] -// }, -// inputs: [ -// { -// key: SystemInputEnum.history, -// value: undefined -// }, -// { -// key: SystemInputEnum.userChatInput, -// value: undefined -// } -// ], -// outputs: [ -// { -// key: 'a', -// value: undefined, -// targets: [ -// { -// moduleId: 'a', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'b', -// value: undefined, -// targets: [ -// { -// moduleId: 'b', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'c', -// value: undefined, -// targets: [ -// { -// moduleId: 'c', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'd', -// value: undefined, -// targets: [ -// { -// moduleId: 'd', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'e', -// value: undefined, -// targets: [ -// { -// moduleId: 'e', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'f', -// value: undefined, -// targets: [ -// { -// moduleId: 'f', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'g', -// value: undefined, -// targets: [ -// { -// moduleId: 'g', -// key: SystemInputEnum.switch -// } -// ] -// } -// ] -// }, -// { -// moduleId: 'a', -// type: 'answer', -// body: {}, -// inputs: [ -// { -// key: SpecificInputEnum.answerText, -// value: '你好,我是 环界云 助手,你有什么 Laf 或者 sealos 的 问题么?' -// }, -// { -// key: SystemInputEnum.switch, -// value: undefined -// } -// ], -// outputs: [] -// }, -// { -// moduleId: 'b', -// type: 'answer', -// body: {}, -// inputs: [ -// { -// key: SpecificInputEnum.answerText, -// value: '查询 Laf 通用知识库:xxxxx' -// }, -// { -// key: SystemInputEnum.switch, -// value: undefined -// } -// ], -// outputs: [] -// }, -// { -// moduleId: 'c', -// type: 'answer', -// body: {}, -// inputs: [ -// { -// key: SpecificInputEnum.answerText, -// value: '查询 Laf 代码知识库:xxxxx' -// }, -// { -// key: SystemInputEnum.switch, -// value: undefined -// } -// ], -// outputs: [] -// }, -// { -// moduleId: 'd', -// type: 'answer', -// body: {}, -// inputs: [ -// { -// key: SpecificInputEnum.answerText, -// value: '查询 sealos 通用知识库: xxxx' -// }, -// { -// key: SystemInputEnum.switch, -// value: undefined -// } -// ], -// outputs: [] -// }, -// { -// moduleId: 'e', -// type: 'answer', -// body: {}, -// inputs: [ -// { -// key: SpecificInputEnum.answerText, -// value: '其他问题。回复引导语:xxxx' -// }, -// { -// key: SystemInputEnum.switch, -// value: undefined -// } -// ], -// outputs: [] -// }, -// { -// moduleId: 'f', -// type: 'answer', -// body: {}, -// inputs: [ -// { -// key: SpecificInputEnum.answerText, -// value: '商务类问题,联系方式:xxxxx' -// }, -// { -// key: SystemInputEnum.switch, -// value: undefined -// } -// ], -// outputs: [] -// }, -// { -// moduleId: 'g', -// type: 'http', -// url: '/openapi/modules/agent/extract', -// body: { -// description: '运行 laf 代码', -// agents: [ -// { -// desc: '代码内容', -// key: 'code' -// } -// ] -// }, -// inputs: [ -// { -// key: SystemInputEnum.switch, -// value: undefined -// }, -// { -// key: SystemInputEnum.history, -// value: undefined -// }, -// { -// key: SystemInputEnum.userChatInput, -// value: undefined -// } -// ], -// outputs: [ -// { -// key: 'code', -// value: undefined, -// targets: [ -// { -// moduleId: 'code_run', -// key: 'code' -// } -// ] -// } -// ] -// }, -// { -// moduleId: 'code_run', -// type: AppModuleItemTypeEnum.http, -// url: 'https://v1cde7.laf.run/tess', -// body: {}, -// inputs: [ -// { -// key: 'code', -// value: undefined -// } -// ], -// outputs: [ -// { -// key: 'star', -// value: undefined, -// targets: [] -// } -// ] -// } -// ] -// }; diff --git a/client/src/constants/model.ts b/client/src/constants/model.ts index f06a55c86..f1ddb5551 100644 --- a/client/src/constants/model.ts +++ b/client/src/constants/model.ts @@ -93,6 +93,5 @@ export const defaultApp: AppSchema = { export const defaultShareChat: ShareChatEditType = { name: '', - password: '', maxContext: 5 }; diff --git a/client/src/pages/api/app/data/tokenUsage.ts b/client/src/pages/api/app/data/tokenUsage.ts new file mode 100644 index 000000000..85c8a9576 --- /dev/null +++ b/client/src/pages/api/app/data/tokenUsage.ts @@ -0,0 +1,53 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; +import { jsonRes } from '@/service/response'; +import { connectToDatabase, Bill } from '@/service/mongo'; +import { authUser } from '@/service/utils/auth'; +import type { ChatHistoryItemType } from '@/types/chat'; +import { Types } from 'mongoose'; + +/* get one app chat history content number. */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { appId, start, end } = req.body as { appId: string; start: Date; end: Date }; + const { userId } = await authUser({ req, authToken: true }); + + await connectToDatabase(); + + const result = await Bill.aggregate([ + { + $match: { + appId: new Types.ObjectId(appId), + userId: new Types.ObjectId(userId), + time: { $gte: new Date(start) } + } + }, + { + $group: { + _id: { + year: { $year: '$time' }, + month: { $month: '$time' }, + day: { $dayOfMonth: '$time' } + }, + tokenLen: { $sum: '$tokenLen' } // 对tokenLen的值求和 + } + }, + { + $project: { + _id: 0, + date: { $dateFromParts: { year: '$_id.year', month: '$_id.month', day: '$_id.day' } }, + tokenLen: 1 + } + }, + { $sort: { date: 1 } } + ]); + + jsonRes(res, { + data: result + }); + } catch (err) { + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/client/src/pages/api/app/del.ts b/client/src/pages/api/app/del.ts index 3a6c47a96..16778c9ec 100644 --- a/client/src/pages/api/app/del.ts +++ b/client/src/pages/api/app/del.ts @@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< await connectToDatabase(); - // 验证是否是该用户的 model + // 验证是否是该用户的 app await authApp({ appId, userId diff --git a/client/src/pages/api/chat/history/getHistoryQuote.ts b/client/src/pages/api/chat/history/getHistoryQuote.ts index cea146f3b..1c7258a49 100644 --- a/client/src/pages/api/chat/history/getHistoryQuote.ts +++ b/client/src/pages/api/chat/history/getHistoryQuote.ts @@ -6,22 +6,22 @@ import { Types } from 'mongoose'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - const { chatId, historyId } = req.query as { - chatId: string; + const { historyId, contentId } = req.query as { historyId: string; + contentId: string; }; await connectToDatabase(); const { userId } = await authUser({ req, authToken: true }); - if (!chatId || !historyId) { + if (!historyId || !contentId) { throw new Error('params is error'); } const history = await Chat.aggregate([ { $match: { - _id: new Types.ObjectId(chatId), + _id: new Types.ObjectId(historyId), userId: new Types.ObjectId(userId) } }, @@ -30,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }, { $match: { - 'content._id': new Types.ObjectId(historyId) + 'content._id': new Types.ObjectId(contentId) } }, { diff --git a/client/src/pages/api/chat/history/updateHistoryQuote.ts b/client/src/pages/api/chat/history/updateHistoryQuote.ts index 30b698da3..431d8e765 100644 --- a/client/src/pages/api/chat/history/updateHistoryQuote.ts +++ b/client/src/pages/api/chat/history/updateHistoryQuote.ts @@ -7,13 +7,13 @@ import { Types } from 'mongoose'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { let { - chatId, historyId, + contentId, quoteId, sourceText = '' } = req.query as { - chatId: string; historyId: string; + contentId: string; quoteId: string; sourceText: string; }; @@ -21,15 +21,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const { userId } = await authUser({ req, authToken: true }); - if (!chatId || !historyId || !quoteId) { + if (!contentId || !historyId || !quoteId) { throw new Error('params is error'); } await Chat.updateOne( { - _id: new Types.ObjectId(chatId), + _id: new Types.ObjectId(historyId), userId: new Types.ObjectId(userId), - 'content._id': new Types.ObjectId(historyId) + 'content._id': new Types.ObjectId(contentId) }, { $set: { diff --git a/client/src/pages/api/chat/init.ts b/client/src/pages/api/chat/init.ts index 64f1dfea4..031cb398e 100644 --- a/client/src/pages/api/chat/init.ts +++ b/client/src/pages/api/chat/init.ts @@ -53,7 +53,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const { chat, history = [] }: { chat?: ChatSchema; history?: ChatItemType[] } = await (async () => { if (historyId) { - // auth chatId + // auth historyId const chat = await Chat.findOne({ _id: historyId, userId diff --git a/client/src/pages/api/openapi/kb/appKbSearch.ts b/client/src/pages/api/openapi/kb/appKbSearch.ts index ad6876c11..43f7cb2d3 100644 --- a/client/src/pages/api/openapi/kb/appKbSearch.ts +++ b/client/src/pages/api/openapi/kb/appKbSearch.ts @@ -53,14 +53,14 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex throw new Error('params is error'); } - // auth model + // auth app const { app } = await authApp({ appId, userId }); const result = await appKbSearch({ - model: app, + app, userId, fixedQuote: [], prompt: prompts[prompts.length - 1], @@ -81,21 +81,21 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex }); export async function appKbSearch({ - model, + app, userId, fixedQuote = [], prompt, similarity = 0.8, limit = 5 }: { - model: AppSchema; + app: AppSchema; userId: string; fixedQuote?: QuoteItemType[]; prompt: ChatItemType; similarity: number; limit: number; }): Promise { - const modelConstantsData = ChatModelMap[model.chat.chatModel]; + const modelConstantsData = ChatModelMap[app.chat.chatModel]; // get vector const promptVector = await openaiEmbedding({ @@ -107,7 +107,7 @@ export async function appKbSearch({ const res: any = await PgClient.query( `BEGIN; SET LOCAL ivfflat.probes = ${global.systemEnv.pgIvfflatProbe || 10}; - select id,q,a,source from modelData where kb_id IN (${model.chat.relatedKbs + select id,q,a,source from modelData where kb_id IN (${app.chat.relatedKbs .map((item) => `'${item}'`) .join(',')}) AND vector <#> '[${promptVector[0]}]' < -${similarity} order by vector <#> '[${ promptVector[0] @@ -133,32 +133,32 @@ export async function appKbSearch({ }); // 计算固定提示词的 token 数量 - const userSystemPrompt = model.chat.systemPrompt // user system prompt + const userSystemPrompt = app.chat.systemPrompt // user system prompt ? [ { obj: ChatRoleEnum.System, - value: model.chat.systemPrompt + value: app.chat.systemPrompt } ] : []; const userLimitPrompt = [ { obj: ChatRoleEnum.Human, - value: model.chat.limitPrompt - ? model.chat.limitPrompt - : `知识库是关于 ${model.name} 的内容,参考知识库回答问题。与 "${model.name}" 无关内容,直接回复: "我不知道"。` + value: app.chat.limitPrompt + ? app.chat.limitPrompt + : `知识库是关于 ${app.name} 的内容,参考知识库回答问题。与 "${app.name}" 无关内容,直接回复: "我不知道"。` } ]; const fixedSystemTokens = modelToolMap.countTokens({ - model: model.chat.chatModel, + model: app.chat.chatModel, messages: [...userSystemPrompt, ...userLimitPrompt] }); // filter part quote by maxToken const sliceResult = modelToolMap .tokenSlice({ - model: model.chat.chatModel, + model: app.chat.chatModel, maxToken: modelConstantsData.systemMaxToken - fixedSystemTokens, messages: filterSearch.map((item, i) => ({ obj: ChatRoleEnum.System, diff --git a/client/src/pages/api/openapi/modules/agent/extract.ts b/client/src/pages/api/openapi/modules/agent/extract.ts index 50270079e..a570d7911 100644 --- a/client/src/pages/api/openapi/modules/agent/extract.ts +++ b/client/src/pages/api/openapi/modules/agent/extract.ts @@ -61,7 +61,7 @@ export async function extract({ agents, history = [], userChatInput, description agents.forEach((item) => { properties[item.key] = { type: 'string', - description: item.desc + description: item.value }; }); diff --git a/client/src/pages/api/openapi/v1/chat/completions.ts b/client/src/pages/api/openapi/v1/chat/completions.ts index f67464f42..adee2724f 100644 --- a/client/src/pages/api/openapi/v1/chat/completions.ts +++ b/client/src/pages/api/openapi/v1/chat/completions.ts @@ -15,6 +15,8 @@ import { Types } from 'mongoose'; import { moduleFetch } from '@/service/api/request'; import { AppModuleItemType, RunningModuleItemType } from '@/types/app'; import { FlowInputItemTypeEnum } from '@/constants/flow'; +import { pushChatBill } from '@/service/events/pushBill'; +import { BillTypeEnum } from '@/constants/user'; export type MessageItemType = ChatCompletionRequestMessage & { _id?: string }; type FastGptWebChatProps = { @@ -168,6 +170,16 @@ export default withNextCors(async function handler(req: NextApiRequest, res: Nex ] }); } + + pushChatBill({ + isPay: true, + chatModel: 'gpt-3.5-turbo', + userId, + appId, + textLen: 1, + tokens: 100, + type: BillTypeEnum.chat + }); } catch (err: any) { if (stream) { res.status(500); diff --git a/client/src/pages/api/plugins/kb/delete.ts b/client/src/pages/api/plugins/kb/delete.ts index 5306aea57..a956cb0e0 100644 --- a/client/src/pages/api/plugins/kb/delete.ts +++ b/client/src/pages/api/plugins/kb/delete.ts @@ -31,7 +31,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< kbId: id }); - // delete related model + // delete related app await App.updateMany( { userId diff --git a/client/src/pages/app/detail/components/Charts/TokenUsage.tsx b/client/src/pages/app/detail/components/Charts/TokenUsage.tsx new file mode 100644 index 000000000..137d1f9c8 --- /dev/null +++ b/client/src/pages/app/detail/components/Charts/TokenUsage.tsx @@ -0,0 +1,198 @@ +import React, { useEffect, useMemo, useRef } from 'react'; +import * as echarts from 'echarts'; +import { useGlobalStore } from '@/store/global'; +import { getTokenUsage } from '@/api/app'; +import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; + +const map = { + blue: { + backgroundColor: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(3, 190, 232, 0.42)' // 0% 处的颜色 + }, + { + offset: 1, + color: 'rgba(0, 182, 240, 0)' + } + ], + global: false // 缺省为 false + }, + lineColor: '#36ADEF' + }, + deepBlue: { + backgroundColor: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(47, 112, 237, 0.42)' // 0% 处的颜色 + }, + { + offset: 1, + color: 'rgba(94, 159, 235, 0)' + } + ], + global: false + }, + lineColor: '#3293EC' + }, + purple: { + backgroundColor: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(211, 190, 255, 0.42)' // 0% 处的颜色 + }, + { + offset: 1, + color: 'rgba(52, 60, 255, 0)' + } + ], + global: false // 缺省为 false + }, + lineColor: '#8172D8' + }, + green: { + backgroundColor: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { + offset: 0, + color: 'rgba(4, 209, 148, 0.42)' // 0% 处的颜色 + }, + { + offset: 1, + color: 'rgba(19, 217, 181, 0)' + } + ], + global: false // 缺省为 false + }, + lineColor: '#00A9A6', + max: 100 + } +}; + +const TokenUsage = ({ appId }: { appId: string }) => { + const { screenWidth } = useGlobalStore(); + + const Dom = useRef(null); + const myChart = useRef(); + const { data = [] } = useQuery(['init'], () => getTokenUsage({ appId })); + + const option = useMemo( + () => ({ + xAxis: { + type: 'category', + show: false, + boundaryGap: false, + data: data.map((item) => item.date) + }, + yAxis: { + type: 'value', + boundaryGap: false, + splitNumber: 5, + max: Math.max(...data.map((item) => item.tokenLen)), + min: 0 + }, + grid: { + show: false, + left: 5, + right: 5, + top: 5, + bottom: 5 + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'line' + }, + formatter: (e: any[]) => { + const data = e[0]; + if (!data) return ''; + + return ` +
+
${dayjs(data.axisValue).format('YYYY/MM/DD')}
+
${((e[0]?.value || 0) / 1000).toFixed(2)}k Tokens
+
+`; + } + }, + series: [ + { + data: data.map((item) => item.tokenLen), + type: 'line', + showSymbol: true, + animationDuration: 300, + animationEasingUpdate: 'linear', + areaStyle: { + color: map['blue'].backgroundColor + }, + lineStyle: { + width: '1', + color: map['blue'].lineColor + }, + itemStyle: { + width: 1.5, + color: map['blue'].lineColor + }, + emphasis: { + // highlight + disabled: true + } + } + ] + }), + [data] + ); + + // init chart + useEffect(() => { + if (!Dom.current || myChart?.current?.getOption()) return; + myChart.current = echarts.init(Dom.current); + myChart.current && myChart.current.setOption(option); + }, [Dom]); + + // data changed, update + useEffect(() => { + if (!myChart.current || !myChart?.current?.getOption()) return; + myChart.current.setOption(option); + }, [data, option]); + + // limit changed, update + useEffect(() => { + if (!myChart.current || !myChart?.current?.getOption()) return; + myChart.current.setOption(option); + }, [option]); + + // resize chart + useEffect(() => { + if (!myChart.current || !myChart.current.getOption()) return; + myChart.current.resize(); + }, [screenWidth]); + + return
; +}; + +export default React.memo(TokenUsage); diff --git a/client/src/pages/app/detail/components/InfoModal.tsx b/client/src/pages/app/detail/components/InfoModal.tsx new file mode 100644 index 000000000..1840682b6 --- /dev/null +++ b/client/src/pages/app/detail/components/InfoModal.tsx @@ -0,0 +1,194 @@ +import React, { useState, useCallback } from 'react'; +import { + Box, + Flex, + Button, + FormControl, + Input, + Textarea, + Modal, + ModalOverlay, + ModalContent, + ModalHeader, + ModalFooter, + ModalBody, + ModalCloseButton +} from '@chakra-ui/react'; +import { useForm } from 'react-hook-form'; +import { AppSchema } from '@/types/mongoSchema'; +import { useToast } from '@/hooks/useToast'; +import { delModelById, putAppById } from '@/api/app'; +import { useSelectFile } from '@/hooks/useSelectFile'; +import { compressImg } from '@/utils/file'; +import { getErrText } from '@/utils/tools'; +import Avatar from '@/components/Avatar'; + +const InfoModal = ({ + defaultApp, + onClose, + onSuccess +}: { + defaultApp: AppSchema; + onClose: () => void; + onSuccess: () => void; +}) => { + const { toast } = useToast(); + const { File, onOpen: onOpenSelectFile } = useSelectFile({ + fileType: '.jpg,.png', + multiple: false + }); + const { + register, + setValue, + getValues, + formState: { errors }, + reset, + handleSubmit + } = useForm({ + defaultValues: defaultApp + }); + const [btnLoading, setBtnLoading] = useState(false); + const [refresh, setRefresh] = useState(false); + + // 提交保存模型修改 + const saveSubmitSuccess = useCallback( + async (data: AppSchema) => { + setBtnLoading(true); + try { + await putAppById(data._id, { + name: data.name, + avatar: data.avatar, + intro: data.intro, + chat: data.chat, + share: data.share + }); + } catch (err: any) { + toast({ + title: err?.message || '更新失败', + status: 'error' + }); + } + setBtnLoading(false); + }, + [toast] + ); + // 提交保存表单失败 + const saveSubmitError = useCallback(() => { + // deep search message + const deepSearch = (obj: any): string => { + if (!obj) return '提交表单错误'; + if (!!obj.message) { + return obj.message; + } + return deepSearch(Object.values(obj)[0]); + }; + toast({ + title: deepSearch(errors), + status: 'error', + duration: 4000, + isClosable: true + }); + }, [errors, toast]); + + const saveUpdateModel = useCallback( + () => handleSubmit(saveSubmitSuccess, saveSubmitError)(), + [handleSubmit, saveSubmitError, saveSubmitSuccess] + ); + + const onSelectFile = useCallback( + async (e: File[]) => { + const file = e[0]; + if (!file) return; + try { + const src = await compressImg({ + file, + maxW: 100, + maxH: 100 + }); + setValue('avatar', src); + setRefresh((state) => !state); + } catch (err: any) { + toast({ + title: getErrText(err, '头像选择异常'), + status: 'warning' + }); + } + }, + [setValue, toast] + ); + + return ( + + + + 应用信息设置 + + + 头像 & 名称 + + onOpenSelectFile()} + /> + + + + + + 应用介绍 + + + 该介绍主要用于记忆和在应用市场展示 + + - + + + 近 7 日 Tokens 消耗 + + + + + + + + + setFullScreen(val)} + fullScreen={fullScreen} + /> + - + {settingAppInfo && ( + setSettingAppInfo(undefined)} + onSuccess={refetch} + /> + )} - - - - - {isOwner && ( - - )} - - - - + ); }; diff --git a/client/src/pages/app/detail/components/Share.tsx b/client/src/pages/app/detail/components/Share.tsx index 474b723d5..32261b1da 100644 --- a/client/src/pages/app/detail/components/Share.tsx +++ b/client/src/pages/app/detail/components/Share.tsx @@ -107,7 +107,7 @@ const Share = ({ appId }: { appId: string }) => { }; return ( - + 免登录聊天窗口 @@ -150,33 +150,37 @@ const Share = ({ appId }: { appId: string }) => { {item.lastTime ? formatTimeToChatTime(item.lastTime) : '未使用'} - { - const url = `${location.origin}/chat/share?shareId=${item.shareId}`; - copyData(url, '已复制分享地址'); - }} - /> - { - setIsLoading(true); - try { - await delShareChatById(item._id); - refetchShareChatList(); - } catch (error) { - console.log(error); - } - setIsLoading(false); - }} - /> + + { + const url = `${location.origin}/chat/share?shareId=${item.shareId}`; + copyData(url, '已复制分享地址'); + }} + /> + + + { + setIsLoading(true); + try { + await delShareChatById(item._id); + refetchShareChatList(); + } catch (error) { + console.log(error); + } + setIsLoading(false); + }} + /> + diff --git a/client/src/pages/app/detail/components/edit/components/TemplateList.tsx b/client/src/pages/app/detail/components/edit/components/TemplateList.tsx index 119977f38..761e9f60c 100644 --- a/client/src/pages/app/detail/components/edit/components/TemplateList.tsx +++ b/client/src/pages/app/detail/components/edit/components/TemplateList.tsx @@ -28,7 +28,7 @@ const ModuleStoreList = ({ - + 系统模块 - - {ModuleTemplates.map((item) => - item.list.map((item) => ( - { - if (e.clientX < 360) return; - onAddNode({ - template: item, - position: { x: e.clientX, y: e.clientY } - }); - }} - > - - - {item.name} - - {item.intro} + + + {ModuleTemplates.map((item) => + item.list.map((item) => ( + { + if (e.clientX < 360) return; + onAddNode({ + template: item, + position: { x: e.clientX, y: e.clientY } + }); + }} + > + + + {item.name} + + {item.intro} + - - - )) - )} + + )) + )} + diff --git a/client/src/pages/app/detail/components/edit/index.tsx b/client/src/pages/app/detail/components/edit/index.tsx index 47f4e8642..d7b1eef52 100644 --- a/client/src/pages/app/detail/components/edit/index.tsx +++ b/client/src/pages/app/detail/components/edit/index.tsx @@ -75,12 +75,12 @@ const nodeTypes = { const edgeTypes = { buttonedge: ButtonEdge }; -type Props = { app: AppSchema; onBack: () => void }; +type Props = { app: AppSchema; fullScreen: boolean; onFullScreen: (val: boolean) => void }; -const AppEdit = ({ app, onBack }: Props) => { +const AppEdit = ({ app, fullScreen, onFullScreen }: Props) => { + const theme = useTheme(); const reactFlowWrapper = useRef(null); const ChatTestRef = useRef(null); - const theme = useTheme(); const { x, y, zoom } = useViewport(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -91,6 +91,14 @@ const AppEdit = ({ app, onBack }: Props) => { } = useDisclosure(); const [testModules, setTestModules] = useState(); + const onFixView = useCallback(() => { + const btn = document.querySelector('.react-flow__controls-fitview') as HTMLButtonElement; + + setTimeout(() => { + btn && btn.click(); + }, 100); + }, []); + const onChangeNode = useCallback( ({ moduleId, key, type = 'inputs', value, valueKey = 'value' }: FlowModuleItemChangeProps) => { setNodes((nodes) => @@ -258,23 +266,57 @@ const AppEdit = ({ app, onBack }: Props) => { }, [app, initData]); return ( - + <> {/* header */} - - } - w={'28px'} - h={'28px'} - borderRadius={'md'} - borderColor={'myGray.300'} - variant={'base'} - aria-label={''} - onClick={onBack} - /> - - {app.name} - + + {fullScreen ? ( + <> + + } + borderRadius={'md'} + borderColor={'myGray.300'} + variant={'base'} + aria-label={''} + onClick={() => { + onFullScreen(false); + onFixView(); + }} + /> + + + {app.name} + + + ) : ( + <> + + 应用编排 + + + } + borderRadius={'lg'} + variant={'base'} + aria-label={'fullScreenLight'} + onClick={() => { + onFullScreen(true); + onFixView(); + }} + /> + + + )} {testModules ? ( { }} > - + @@ -376,14 +414,26 @@ const AppEdit = ({ app, onBack }: Props) => { onClose={() => setTestModules(undefined)} /> - + ); }; const Flow = (data: Props) => ( - - - + + + + {!!data.app._id && } + + + ); export default React.memo(Flow); diff --git a/client/src/pages/app/detail/index.tsx b/client/src/pages/app/detail/index.tsx index 0a9410499..ef8576fb9 100644 --- a/client/src/pages/app/detail/index.tsx +++ b/client/src/pages/app/detail/index.tsx @@ -12,9 +12,6 @@ import Avatar from '@/components/Avatar'; import MyIcon from '@/components/Icon'; import PageContainer from '@/components/PageContainer'; -const EditApp = dynamic(() => import('./components/edit'), { - ssr: false -}); const Share = dynamic(() => import('./components/Share'), { ssr: false }); @@ -24,7 +21,6 @@ const API = dynamic(() => import('./components/API'), { enum TabEnum { 'settings' = 'settings', - 'edit' = 'edit', 'share' = 'share', 'API' = 'API' } @@ -33,17 +29,11 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { const router = useRouter(); const theme = useTheme(); const { appId } = router.query as { appId: string }; - const { appDetail = defaultApp, loadAppDetail, userInfo } = useUserStore(); - - const isOwner = useMemo( - () => appDetail.userId === userInfo?._id, - [appDetail.userId, userInfo?._id] - ); + const { appDetail = defaultApp } = useUserStore(); const setCurrentTab = useCallback( (tab: `${TabEnum}`) => { router.replace({ - pathname: router.pathname, query: { appId, currentTab: tab @@ -56,28 +46,23 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { const tabList = useMemo( () => [ { label: '概览', id: TabEnum.settings, icon: 'overviewLight' }, - ...(isOwner ? [{ label: '高级设置', id: TabEnum.edit, icon: 'settingLight' }] : []), { label: '链接分享', id: TabEnum.share, icon: 'shareLight' }, { label: 'API访问', id: TabEnum.API, icon: 'apiLight' }, { label: '立即对话', id: 'startChat', icon: 'chatLight' } ], - [isOwner] + [] ); - useEffect(() => { - window.onbeforeunload = (e) => { - e.preventDefault(); - e.returnValue = '内容已修改,确认离开页面吗?'; - }; + // useEffect(() => { + // window.onbeforeunload = (e) => { + // e.preventDefault(); + // e.returnValue = '内容已修改,确认离开页面吗?'; + // }; - return () => { - window.onbeforeunload = null; - }; - }, []); - - useEffect(() => { - loadAppDetail(appId); - }, [appId, loadAppDetail]); + // return () => { + // window.onbeforeunload = null; + // }; + // }, []); return ( @@ -156,11 +141,6 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { {currentTab === TabEnum.settings && } - {currentTab === TabEnum.edit && ( - - setCurrentTab(TabEnum.settings)} /> - - )} {currentTab === TabEnum.API && } {currentTab === TabEnum.share && } diff --git a/client/src/pages/chat/components/ChatHistorySlider.tsx b/client/src/pages/chat/components/ChatHistorySlider.tsx index 23e2b4b62..c4223c465 100644 --- a/client/src/pages/chat/components/ChatHistorySlider.tsx +++ b/client/src/pages/chat/components/ChatHistorySlider.tsx @@ -9,9 +9,11 @@ import { MenuList, MenuItem } from '@chakra-ui/react'; -import MyIcon from '@/components/Icon'; import { useGlobalStore } from '@/store/global'; +import { useRouter } from 'next/router'; import Avatar from '@/components/Avatar'; +import MyTooltip from '@/components/MyTooltip'; +import MyIcon from '@/components/Icon'; type HistoryItemType = { id: string; @@ -20,6 +22,7 @@ type HistoryItemType = { }; const ChatHistorySlider = ({ + appId, appName, appAvatar, history, @@ -29,6 +32,7 @@ const ChatHistorySlider = ({ onSetHistoryTop, onCloseSlider }: { + appId?: string; appName: string; appAvatar: string; history: HistoryItemType[]; @@ -39,6 +43,7 @@ const ChatHistorySlider = ({ onCloseSlider: () => void; }) => { const theme = useTheme(); + const router = useRouter(); const { isPc } = useGlobalStore(); const concatHistory = useMemo( @@ -57,12 +62,27 @@ const ChatHistorySlider = ({ whiteSpace={'nowrap'} > {isPc && ( - - - - {appName} - - + + + appId && + router.push({ + pathname: '/app/detail', + query: { appId } + }) + } + > + + + {appName} + + + )} {/* 新对话 */} diff --git a/client/src/pages/chat/components/QuoteModal.tsx b/client/src/pages/chat/components/QuoteModal.tsx index 1493c4939..feb9cc846 100644 --- a/client/src/pages/chat/components/QuoteModal.tsx +++ b/client/src/pages/chat/components/QuoteModal.tsx @@ -21,11 +21,11 @@ import { getErrText } from '@/utils/tools'; const QuoteModal = ({ historyId, - chatId, + contentId, onClose }: { historyId: string; - chatId: string; + contentId: string; onClose: () => void; }) => { const theme = useTheme(); @@ -41,7 +41,7 @@ const QuoteModal = ({ data: quote = [], refetch, isLoading - } = useQuery(['getHistoryQuote'], () => getHistoryQuote({ historyId, chatId })); + } = useQuery(['getHistoryQuote'], () => getHistoryQuote({ historyId, contentId })); /** * update kbData, update mongo status and reload quotes @@ -51,7 +51,7 @@ const QuoteModal = ({ setIsLoading(true); try { await updateHistoryQuote({ - chatId, + contentId, historyId, quoteId, sourceText @@ -66,7 +66,7 @@ const QuoteModal = ({ } setIsLoading(false); }, - [chatId, historyId, refetch, setIsLoading, toast] + [contentId, historyId, refetch, setIsLoading, toast] ); /** diff --git a/client/src/pages/chat/components/SliderApps.tsx b/client/src/pages/chat/components/SliderApps.tsx index 88dcd87ad..091c9f8b1 100644 --- a/client/src/pages/chat/components/SliderApps.tsx +++ b/client/src/pages/chat/components/SliderApps.tsx @@ -21,7 +21,7 @@ const SliderApps = ({ appId }: { appId: string }) => { px={3} borderRadius={'md'} _hover={{ bg: 'myGray.200' }} - onClick={() => router.replace('/app/list')} + onClick={() => router.back()} > { const { lastChatAppId, setLastChatAppId, - lastChatId, - setLastChatId, + lastHistoryId, + setLastHistoryId, history, loadHistory, updateHistory, @@ -192,13 +192,13 @@ const Chat = () => { } catch (e: any) { // reset all chat tore setLastChatAppId(''); - setLastChatId(''); + setLastHistoryId(''); router.replace('/chat'); } setIsLoading(false); return null; }, - [setIsLoading, setChatData, router, setLastChatAppId, setLastChatId] + [setIsLoading, setChatData, router, setLastChatAppId, setLastHistoryId] ); // 初始化聊天框 useQuery(['init', appId, historyId], () => { @@ -207,7 +207,7 @@ const Chat = () => { router.replace({ query: { appId: lastChatAppId, - historyId: lastChatId + historyId: lastHistoryId } }); return null; @@ -215,7 +215,7 @@ const Chat = () => { // store id appId && setLastChatAppId(appId); - setLastChatId(historyId); + setLastHistoryId(historyId); if (forbidRefresh.current) { forbidRefresh.current = false; @@ -254,6 +254,7 @@ const Chat = () => { ); })( { const { lastRoute = '' } = router.query as { lastRoute: string }; const { isPc } = useGlobalStore(); const [pageType, setPageType] = useState<`${PageTypeEnum}`>(PageTypeEnum.login); - const { setUserInfo, setLastModelId, loadKbList, setLastKbId } = useUserStore(); - const { setLastChatId, setLastChatAppId } = useChatStore(); + const { setUserInfo, loadKbList, setLastKbId } = useUserStore(); + const { setLastHistoryId, setLastChatAppId } = useChatStore(); const loginSuccess = useCallback( (res: ResLogin) => { // init store - setLastChatId(''); - setLastModelId(''); + setLastHistoryId(''); setLastChatAppId(''); setLastKbId(''); loadKbList(true); @@ -34,16 +33,7 @@ const Login = () => { router.push(lastRoute ? decodeURIComponent(lastRoute) : '/model'); }, 100); }, - [ - lastRoute, - loadKbList, - router, - setLastChatId, - setLastChatAppId, - setLastKbId, - setLastModelId, - setUserInfo - ] + [lastRoute, loadKbList, router, setLastHistoryId, setLastChatAppId, setLastKbId, setUserInfo] ); function DynamicComponent({ type }: { type: `${PageTypeEnum}` }) { diff --git a/client/src/pages/number/components/BillTable.tsx b/client/src/pages/number/components/BillTable.tsx index 3c48e02a0..be7d78040 100644 --- a/client/src/pages/number/components/BillTable.tsx +++ b/client/src/pages/number/components/BillTable.tsx @@ -69,19 +69,17 @@ const BillTable = () => { )} - {total > pageSize && ( - - getData(1)} - /> - - - - - )} + + getData(1)} + /> + + + + ); diff --git a/client/src/service/events/pushBill.ts b/client/src/service/events/pushBill.ts index 110a9b10b..df5432e9f 100644 --- a/client/src/service/events/pushBill.ts +++ b/client/src/service/events/pushBill.ts @@ -12,7 +12,7 @@ export const pushChatBill = async ({ isPay, chatModel, userId, - chatId, + appId, textLen, tokens, type @@ -20,7 +20,7 @@ export const pushChatBill = async ({ isPay: boolean; chatModel: ChatModelType; userId: string; - chatId?: '' | string; + appId: string; textLen: number; tokens: number; type: BillTypeEnum.chat | BillTypeEnum.openapiChat; @@ -43,7 +43,7 @@ export const pushChatBill = async ({ userId, type, modelName: chatModel, - chatId: chatId ? chatId : undefined, + appId, textLen, tokenLen: tokens, price diff --git a/client/src/service/models/app.ts b/client/src/service/models/app.ts index 523f8a8e0..2efffc54c 100644 --- a/client/src/service/models/app.ts +++ b/client/src/service/models/app.ts @@ -105,4 +105,4 @@ try { console.log(error); } -export const App: Model = models['model'] || model('model', AppSchema); +export const App: Model = models['app'] || model('app', AppSchema); diff --git a/client/src/service/models/bill.ts b/client/src/service/models/bill.ts index aafa84e8f..697db4c93 100644 --- a/client/src/service/models/bill.ts +++ b/client/src/service/models/bill.ts @@ -16,12 +16,11 @@ const BillSchema = new Schema({ }, modelName: { type: String, - enum: [...Object.keys(ChatModelMap), embeddingModel], - required: true + enum: [...Object.keys(ChatModelMap), embeddingModel] }, - chatId: { + appId: { type: Schema.Types.ObjectId, - ref: 'chat' + ref: 'app' }, time: { type: Date, @@ -44,8 +43,9 @@ const BillSchema = new Schema({ }); try { - BillSchema.index({ time: -1 }); BillSchema.index({ userId: 1 }); + // BillSchema.index({ time: -1 }); + // BillSchema.index({ time: 1 }, { expireAfterSeconds: 90 * 24 * 60 }); } catch (error) { console.log(error); } diff --git a/client/src/service/models/chat.ts b/client/src/service/models/chat.ts index 9197bc09c..43dc42b61 100644 --- a/client/src/service/models/chat.ts +++ b/client/src/service/models/chat.ts @@ -10,7 +10,7 @@ const ChatSchema = new Schema({ }, appId: { type: Schema.Types.ObjectId, - ref: 'model', + ref: 'app', required: true }, updateTime: { diff --git a/client/src/service/utils/chat/claude.ts b/client/src/service/utils/chat/claude.ts deleted file mode 100644 index a31f8338a..000000000 --- a/client/src/service/utils/chat/claude.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { ChatCompletionType, StreamResponseType } from './index'; -import { ChatRoleEnum } from '@/constants/chat'; -import axios from 'axios'; - -/* 模型对话 */ -export const claudChat = async ({ apiKey, messages, stream, chatId }: ChatCompletionType) => { - // get system prompt - const systemPrompt = messages - .filter((item) => item.obj === 'System') - .map((item) => item.value) - .join('\n'); - const systemPromptText = systemPrompt ? `你本次知识:${systemPrompt}\n下面是我的问题:` : ''; - - const prompt = `${systemPromptText}'${messages[messages.length - 1].value}'`; - - const response = await axios.post( - process.env.CLAUDE_BASE_URL || '', - { - prompt, - stream, - conversationId: chatId - }, - { - headers: { - Authorization: apiKey - }, - timeout: stream ? 60000 : 480000, - responseType: stream ? 'stream' : 'json' - } - ); - - const responseText = stream ? '' : response.data?.text || ''; - - return { - streamResponse: response, - responseMessages: messages.concat({ - obj: ChatRoleEnum.AI, - value: responseText - }), - responseText, - totalTokens: 0 - }; -}; - -/* openai stream response */ -export const claudStreamResponse = async ({ res, chatResponse, prompts }: StreamResponseType) => { - try { - let responseContent = ''; - - try { - const decoder = new TextDecoder(); - for await (const chunk of chatResponse.data as any) { - if (res.closed) { - break; - } - const content = decoder.decode(chunk); - responseContent += content; - content && res.write(content); - } - } catch (error) { - console.log('pipe error', error); - } - - const finishMessages = prompts.concat({ - obj: ChatRoleEnum.AI, - value: responseContent - }); - - return { - responseContent, - totalTokens: 0, - finishMessages - }; - } catch (error) { - return Promise.reject(error); - } -}; diff --git a/client/src/service/utils/chat/index.ts b/client/src/service/utils/chat/index.ts index 2b67cf2d5..f9d2c2292 100644 --- a/client/src/service/utils/chat/index.ts +++ b/client/src/service/utils/chat/index.ts @@ -14,7 +14,7 @@ export type ChatCompletionType = { temperature: number; maxToken?: number; messages: ChatItemType[]; - chatId?: string; + historyId?: string; [key: string]: any; }; export type ChatCompletionResponseType = { diff --git a/client/src/store/chat.ts b/client/src/store/chat.ts index dc31ccdce..e901b82de 100644 --- a/client/src/store/chat.ts +++ b/client/src/store/chat.ts @@ -15,8 +15,8 @@ type State = { setChatData: (e: InitChatResponse | ((e: InitChatResponse) => InitChatResponse)) => void; lastChatAppId: string; setLastChatAppId: (id: string) => void; - lastChatId: string; - setLastChatId: (id: string) => void; + lastHistoryId: string; + setLastHistoryId: (id: string) => void; }; const defaultChatData: InitChatResponse = { @@ -43,10 +43,10 @@ export const useChatStore = create()( state.lastChatAppId = id; }); }, - lastChatId: '', - setLastChatId(id: string) { + lastHistoryId: '', + setLastHistoryId(id: string) { set((state) => { - state.lastChatId = id; + state.lastHistoryId = id; }); }, history: [], @@ -95,7 +95,7 @@ export const useChatStore = create()( name: 'chatStore', partialize: (state) => ({ lastChatAppId: state.lastChatAppId, - lastChatId: state.lastChatId + lastHistoryId: state.lastHistoryId }) } ) diff --git a/client/src/store/user.ts b/client/src/store/user.ts index 030598241..4f8890634 100644 --- a/client/src/store/user.ts +++ b/client/src/store/user.ts @@ -17,9 +17,6 @@ type State = { initUserInfo: () => Promise; setUserInfo: (user: UserType | null) => void; updateUserInfo: (user: UserUpdateParams) => void; - // model - lastModelId: string; - setLastModelId: (id: string) => void; myApps: AppListItemType[]; myCollectionApps: AppListItemType[]; loadMyModels: () => Promise; @@ -63,12 +60,6 @@ export const useUserStore = create()( }; }); }, - lastModelId: '', - setLastModelId(id: string) { - set((state) => { - state.lastModelId = id; - }); - }, myApps: [], myCollectionApps: [], async loadMyModels() { @@ -119,7 +110,6 @@ export const useUserStore = create()( { name: 'userStore', partialize: (state) => ({ - lastModelId: state.lastModelId, lastKbId: state.lastKbId }) } diff --git a/client/src/types/mongoSchema.d.ts b/client/src/types/mongoSchema.d.ts index 4c7afbcc2..f1d9b4b2b 100644 --- a/client/src/types/mongoSchema.d.ts +++ b/client/src/types/mongoSchema.d.ts @@ -91,8 +91,8 @@ export interface BillSchema { _id: string; userId: string; type: `${BillTypeEnum}`; - modelName: ChatModelType | EmbeddingModelType; - chatId: string; + modelName?: ChatModelType | EmbeddingModelType; + appId?: string; time: Date; textLen: number; tokenLen: number;