From aef42cef9d2f5a3d5f7f68fb74ba23baaec8915f Mon Sep 17 00:00:00 2001 From: archer <545436317@qq.com> Date: Sat, 8 Jul 2023 10:37:25 +0800 Subject: [PATCH] perf: attribute --- .../src/components/Icon/icons/light/chat.svg | 1 + client/src/components/Icon/index.tsx | 3 +- client/src/components/Layout/navbar.tsx | 2 +- client/src/components/Layout/navbarPhone.tsx | 2 +- client/src/constants/app.ts | 1368 +++++++++++++---- .../pages/api/openapi/modules/kb/search.ts | 6 + .../pages/app/detail/components/Settings.tsx | 2 +- .../edit/components/TemplateList.tsx | 4 +- .../app/detail/components/edit/index.tsx | 1 + client/src/pages/app/detail/index.tsx | 6 +- .../pages/app/list/component/CreateModal.tsx | 43 +- client/src/pages/app/list/index.tsx | 28 +- client/src/pages/appStore/components/list.tsx | 2 +- client/src/pages/chat/components/History.tsx | 8 +- .../src/pages/chat/components/ModelList.tsx | 2 +- .../pages/chat/components/PhoneSliderBar.tsx | 8 +- client/src/pages/chat/index.tsx | 42 +- 17 files changed, 1177 insertions(+), 351 deletions(-) create mode 100644 client/src/components/Icon/icons/light/chat.svg diff --git a/client/src/components/Icon/icons/light/chat.svg b/client/src/components/Icon/icons/light/chat.svg new file mode 100644 index 000000000..c2561573d --- /dev/null +++ b/client/src/components/Icon/icons/light/chat.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/client/src/components/Icon/index.tsx b/client/src/components/Icon/index.tsx index 62d4b9c40..895fe49cf 100644 --- a/client/src/components/Icon/index.tsx +++ b/client/src/components/Icon/index.tsx @@ -36,7 +36,8 @@ const map = { date: require('./icons/date.svg').default, apikey: require('./icons/apikey.svg').default, save: require('./icons/save.svg').default, - minus: require('./icons/minus.svg').default + minus: require('./icons/minus.svg').default, + chatLight: require('./icons/light/chat.svg').default }; export type IconName = keyof typeof map; diff --git a/client/src/components/Layout/navbar.tsx b/client/src/components/Layout/navbar.tsx index f36764e4f..f22d1f24a 100644 --- a/client/src/components/Layout/navbar.tsx +++ b/client/src/components/Layout/navbar.tsx @@ -23,7 +23,7 @@ const Navbar = ({ unread }: { unread: number }) => { { label: '聊天', icon: 'chat', - link: `/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`, + link: `/chat?appId=${lastChatModelId}&chatId=${lastChatId}`, activeLink: ['/chat'] }, { diff --git a/client/src/components/Layout/navbarPhone.tsx b/client/src/components/Layout/navbarPhone.tsx index 33b3c2076..fd41f3179 100644 --- a/client/src/components/Layout/navbarPhone.tsx +++ b/client/src/components/Layout/navbarPhone.tsx @@ -13,7 +13,7 @@ const NavbarPhone = ({ unread }: { unread: number }) => { { label: '聊天', icon: 'tabbarChat', - link: `/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`, + link: `/chat?appId=${lastChatModelId}&chatId=${lastChatId}`, activeLink: ['/chat'], unread: 0 }, diff --git a/client/src/constants/app.ts b/client/src/constants/app.ts index aed39f7bf..e37e60ffb 100644 --- a/client/src/constants/app.ts +++ b/client/src/constants/app.ts @@ -1,6 +1,4 @@ import type { AppItemType } from '@/types/app'; -import { FlowInputItemTypeEnum, FlowModuleTypeEnum, FlowOutputItemTypeEnum } from './flow'; -import { chatModelList } from './data'; /* app */ export enum AppModuleItemTypeEnum { @@ -18,282 +16,1110 @@ export enum SpecificInputEnum { 'answerText' = 'answerText' // answer module text key } -export const answerModule = ({ id }: { id: string }) => ({ - moduleId: id, - type: AppModuleItemTypeEnum.answer, - flowType: FlowModuleTypeEnum.answerNode, - inputs: [ - { - key: SystemInputEnum.switch, - type: FlowInputItemTypeEnum.target, - label: '触发器', - connected: true - }, - { - key: SpecificInputEnum.answerText, - value: '', - type: FlowInputItemTypeEnum.input, - label: '响应内容', - connected: true - } - ], - outputs: [] -}); -export const chatModule = ({ - id, - systemPrompt = '', - limitPrompt = '', - history = 10 -}: { - id: string; - systemPrompt?: string; - limitPrompt?: string; - history?: number; -}) => { - return { - moduleId: id, - flowType: FlowModuleTypeEnum.chatNode, - type: AppModuleItemTypeEnum.http, - url: '/openapi/modules/chat/gpt', - inputs: [ +// template +export const appTemplates: (AppItemType & { avatar: string; intro: string })[] = [ + { + id: 'simpleChat', + avatar: '/imgs/module/AI.png', + name: '简单的对话', + intro: '一个极其简单的 AI 对话应用', + modules: [ { - key: 'model', - type: FlowInputItemTypeEnum.select, - label: '对话模型', - value: chatModelList[0].value, - list: chatModelList + logo: '/imgs/module/userChatInput.png', + name: '用户问题', + intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。', + type: 'initInput', + flowType: 'questionInput', + url: '/openapi/modules/init/userChatInput', + inputs: [ + { + key: 'userChatInput', + type: 'systemInput', + label: '用户问题', + connected: false + } + ], + outputs: [ + { + key: 'userChatInput', + label: '用户问题', + type: 'source', + targets: [ + { + moduleId: '3n49vn', + key: 'userChatInput' + } + ] + } + ], + position: { + x: 481.4684021933373, + y: 741.252592445572 + }, + moduleId: 'xzj0oo' }, { - key: 'temperature', - type: FlowInputItemTypeEnum.slider, - label: '温度', - value: 0, - min: 0, - max: 10, - step: 1, - markList: [ - { label: '严谨', value: 0 }, - { label: '发散', value: 10 } - ] + logo: '/imgs/module/history.png', + name: '聊天记录', + intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。', + type: 'initInput', + flowType: 'historyNode', + url: '/openapi/modules/init/history', + inputs: [ + { + key: 'maxContext', + type: 'numberInput', + label: '最长记录数', + value: 4, + min: 0, + max: 50, + connected: false + }, + { + key: 'history', + type: 'hidden', + label: '聊天记录', + connected: false + } + ], + outputs: [ + { + key: 'history', + label: '聊天记录', + type: 'source', + targets: [ + { + moduleId: '3n49vn', + key: 'history' + } + ] + } + ], + position: { + x: 405.6002299937601, + y: 374.16606887857023 + }, + moduleId: 'hh6of9' }, { - key: 'maxToken', - type: FlowInputItemTypeEnum.slider, - label: '回复上限', - value: 3000, - min: 0, - max: 4000, - step: 50, - markList: [ - { label: '0', value: 0 }, - { label: '4000', value: 4000 } - ] - }, - { - key: 'systemPrompt', - type: FlowInputItemTypeEnum.textarea, - label: '系统提示词', - description: - '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', - placeholder: - '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', - value: systemPrompt - }, - { - key: 'limitPrompt', - type: FlowInputItemTypeEnum.textarea, - label: '限定词', - description: - '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', - placeholder: - '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', - value: limitPrompt - }, - { - key: SystemInputEnum.switch, - type: FlowInputItemTypeEnum.target, - label: '触发器', - connected: true - }, - { - key: 'quotePrompt', - type: FlowInputItemTypeEnum.target, - label: '引用内容(字符串)', - connected: true - }, - { - key: SystemInputEnum.history, - type: FlowInputItemTypeEnum.numberInput, - label: '最长上下文', - description: '为 0 时,代表不需要上下文。', - value: history, - min: 0, - max: 50 - }, - { - key: SystemInputEnum.userChatInput, - type: FlowInputItemTypeEnum.none, - label: '用户输入(系统自动填写)', - description: '' - } - ], - outputs: [ - { - key: 'answer', - label: '模型回复', - type: FlowOutputItemTypeEnum.answer, - targets: [] + logo: '/imgs/module/AI.png', + name: 'AI 对话', + intro: 'OpenAI GPT 大模型对话。', + flowType: 'chatNode', + type: 'http', + url: '/openapi/modules/chat/gpt', + inputs: [ + { + key: 'model', + type: 'select', + label: '对话模型', + value: 'gpt-3.5-turbo-16k', + list: [ + { + label: 'Gpt35-16k', + value: 'gpt-3.5-turbo-16k' + }, + { + label: 'Gpt35-4k', + value: 'gpt-3.5-turbo' + }, + { + label: 'Gpt4-8k', + value: 'gpt-4' + } + ], + connected: false + }, + { + key: 'temperature', + type: 'slider', + label: '温度', + value: 0, + min: 0, + max: 10, + step: 1, + markList: [ + { + label: '严谨', + value: 0 + }, + { + label: '发散', + value: 10 + } + ], + connected: false + }, + { + key: 'maxToken', + type: 'slider', + label: '回复上限', + value: 3000, + min: 0, + max: 4000, + step: 50, + markList: [ + { + label: '0', + value: 0 + }, + { + label: '4000', + value: 4000 + } + ], + connected: false + }, + { + key: 'systemPrompt', + type: 'textarea', + label: '系统提示词', + description: + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + placeholder: + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + value: '', + connected: false + }, + { + key: 'limitPrompt', + type: 'textarea', + label: '限定词', + description: + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + placeholder: + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + value: '', + connected: false + }, + { + key: 'switch', + type: 'target', + label: '触发器', + connected: false + }, + { + key: 'quotePrompt', + type: 'target', + label: '引用内容', + connected: false + }, + { + key: 'history', + type: 'target', + label: '聊天记录', + connected: true + }, + { + key: 'userChatInput', + type: 'target', + label: '用户问题', + connected: true + } + ], + outputs: [ + { + key: 'answerText', + label: '模型回复', + description: '直接响应,无需配置', + type: 'hidden', + targets: [] + } + ], + position: { + x: 965.5863241865428, + y: -29.569293606933797 + }, + moduleId: '3n49vn' } ] - }; -}; - -export const chatAppDemo: AppItemType = { - id: 'chat', - name: '', - // 标记字段 - modules: [chatModule({ id: 'chat' })] -}; - -// export const kbChatAppDemo: AppItemType = { -// id: 'kbchat', -// name: 'kbchat', -// // 标记字段 -// modules: [ -// { -// moduleId: 'kbsearch', -// flowType: FlowModuleTypeEnum.kbSearchNode, -// type: AppModuleItemTypeEnum.http, -// url: '/openapi/modules/kb/search', -// position: { x: -500, y: 0 }, -// inputs: [ -// { -// key: 'kb_ids', -// type: FlowInputItemTypeEnum.custom, -// label: '关联的知识库', -// value: ['646627f4f7b896cfd8910e38'], -// list: [] -// }, - -// { -// key: 'similarity', -// type: FlowInputItemTypeEnum.slider, -// label: '相似度', -// value: 0.8, -// min: 0, -// max: 1, -// step: 0.01, -// markList: [ -// { label: '0', value: 0 }, -// { label: '1', value: 1 } -// ] -// }, -// { -// key: 'limit', -// type: FlowInputItemTypeEnum.slider, -// label: '单次搜索上限', -// value: 5, -// min: 1, -// max: 20, -// step: 1, -// markList: [ -// { label: '1', value: 1 }, -// { label: '20', value: 20 } -// ] -// }, -// { -// key: SystemInputEnum.history, -// type: FlowInputItemTypeEnum.hidden, -// label: '引用复用数量', -// value: 1 -// }, -// { -// key: SystemInputEnum.userChatInput, -// type: FlowInputItemTypeEnum.none, -// label: '用户输入(系统自动填写)', -// description: '' -// } -// ], -// outputs: [ -// { -// key: 'rawSearch', -// label: '源搜索数据', -// type: FlowOutputItemTypeEnum.none, -// response: true, -// targets: [] -// }, -// { -// key: 'isEmpty', -// label: '无搜索结果', -// type: FlowOutputItemTypeEnum.source, -// targets: [ -// { -// moduleId: 'tfswitch', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'quotePrompt', -// label: '引用内容(字符串)', -// type: FlowOutputItemTypeEnum.source, -// targets: [ -// { -// moduleId: 'chat', -// key: 'quotePrompt' -// } -// ] -// } -// ] -// }, -// { -// moduleId: 'tfswitch', -// type: AppModuleItemTypeEnum.switch, -// flowType: FlowModuleTypeEnum.tfSwitchNode, -// position: { x: 0, y: 510 }, -// inputs: [ -// { -// key: SystemInputEnum.switch, -// type: FlowInputItemTypeEnum.target, -// label: '触发器', -// connected: true -// } -// ], -// outputs: [ -// { -// key: 'true', -// label: '无搜索数据', -// type: FlowOutputItemTypeEnum.source, -// targets: [ -// { -// moduleId: 'answer', -// key: SystemInputEnum.switch -// } -// ] -// }, -// { -// key: 'false', -// label: '有搜索数据', -// type: FlowOutputItemTypeEnum.source, -// targets: [ -// { -// moduleId: 'chat', -// key: SystemInputEnum.switch -// } -// ] -// } -// ] -// }, -// { -// ...chatModule({ id: 'chat', limitPrompt: '参考知识库内容进行回答', history: 5 }), -// position: { x: 300, y: 240 } -// }, -// { -// ...answerModule({ id: 'answer' }), -// position: { x: 300, y: 0 } -// } -// ] -// }; + }, + { + id: 'simpleKbChat', + avatar: '/imgs/module/db.png', + name: '基础知识库', + intro: '每次提问时进行一次知识库搜索,将搜索结果注入 LLM 模型进行参考回答', + modules: [ + { + logo: '/imgs/module/userChatInput.png', + name: '用户问题', + intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。', + type: 'initInput', + flowType: 'questionInput', + url: '/openapi/modules/init/userChatInput', + inputs: [ + { + key: 'userChatInput', + type: 'systemInput', + label: '用户问题', + connected: false + } + ], + outputs: [ + { + key: 'userChatInput', + label: '用户问题', + type: 'source', + targets: [ + { + moduleId: '3n49vn', + key: 'userChatInput' + }, + { + moduleId: 'zid0fj', + key: 'userChatInput' + } + ] + } + ], + position: { + x: 447.0165784462213, + y: 748.7421193471189 + }, + moduleId: 'xzj0oo' + }, + { + logo: '/imgs/module/history.png', + name: '聊天记录', + intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。', + type: 'initInput', + flowType: 'historyNode', + url: '/openapi/modules/init/history', + inputs: [ + { + key: 'maxContext', + type: 'numberInput', + label: '最长记录数', + value: 4, + min: 0, + max: 50, + connected: false + }, + { + key: 'history', + type: 'hidden', + label: '聊天记录', + connected: false + } + ], + outputs: [ + { + key: 'history', + label: '聊天记录', + type: 'source', + targets: [ + { + moduleId: '3n49vn', + key: 'history' + } + ] + } + ], + position: { + x: 1182.3679138395933, + y: 882.21575235563 + }, + moduleId: 'hh6of9' + }, + { + logo: '/imgs/module/AI.png', + name: 'AI 对话', + intro: 'OpenAI GPT 大模型对话。', + flowType: 'chatNode', + type: 'http', + url: '/openapi/modules/chat/gpt', + inputs: [ + { + key: 'model', + type: 'select', + label: '对话模型', + value: 'gpt-3.5-turbo-16k', + list: [ + { + label: 'Gpt35-16k', + value: 'gpt-3.5-turbo-16k' + }, + { + label: 'Gpt35-4k', + value: 'gpt-3.5-turbo' + }, + { + label: 'Gpt4-8k', + value: 'gpt-4' + } + ], + connected: false + }, + { + key: 'temperature', + type: 'slider', + label: '温度', + value: 0, + min: 0, + max: 10, + step: 1, + markList: [ + { + label: '严谨', + value: 0 + }, + { + label: '发散', + value: 10 + } + ], + connected: false + }, + { + key: 'maxToken', + type: 'slider', + label: '回复上限', + value: 3000, + min: 0, + max: 4000, + step: 50, + markList: [ + { + label: '0', + value: 0 + }, + { + label: '4000', + value: 4000 + } + ], + connected: false + }, + { + key: 'systemPrompt', + type: 'textarea', + label: '系统提示词', + description: + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + placeholder: + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + value: '', + connected: false + }, + { + key: 'limitPrompt', + type: 'textarea', + label: '限定词', + description: + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + placeholder: + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + value: '', + connected: false + }, + { + key: 'switch', + type: 'target', + label: '触发器', + connected: false + }, + { + key: 'quotePrompt', + type: 'target', + label: '引用内容', + connected: true + }, + { + key: 'history', + type: 'target', + label: '聊天记录', + connected: true + }, + { + key: 'userChatInput', + type: 'target', + label: '用户问题', + connected: true + } + ], + outputs: [ + { + key: 'answerText', + label: '模型回复', + description: '直接响应,无需配置', + type: 'hidden', + targets: [] + } + ], + position: { + x: 1611.18354309989, + y: -56.531590452502826 + }, + moduleId: '3n49vn' + }, + { + logo: '/imgs/module/db.png', + name: '知识库搜索', + intro: '去知识库中搜索对应的答案。可作为 AI 对话引用参考。', + flowType: 'kbSearchNode', + type: 'http', + url: '/openapi/modules/kb/search', + inputs: [ + { + key: 'kb_ids', + type: 'custom', + label: '关联的知识库', + value: [], + list: [], + connected: false + }, + { + key: 'similarity', + type: 'slider', + label: '相似度', + value: 0.8, + min: 0, + max: 1, + step: 0.01, + markList: [ + { + label: '0', + value: 0 + }, + { + label: '1', + value: 1 + } + ], + connected: false + }, + { + key: 'limit', + type: 'slider', + label: '单次搜索上限', + value: 5, + min: 1, + max: 20, + step: 1, + markList: [ + { + label: '1', + value: 1 + }, + { + label: '20', + value: 20 + } + ], + connected: false + }, + { + key: 'switch', + type: 'target', + label: '触发器', + connected: false + }, + { + key: 'userChatInput', + type: 'target', + label: '用户问题', + connected: true + } + ], + outputs: [ + { + key: 'rawSearch', + label: '源搜索数据', + type: 'hidden', + response: true, + targets: [] + }, + { + key: 'isEmpty', + label: '搜索结果为空', + type: 'source', + targets: [ + { + moduleId: 'gbnzif', + key: 'switch' + } + ] + }, + { + key: 'quotePrompt', + label: '引用内容', + description: '搜索结果为空时不返回', + type: 'source', + targets: [ + { + moduleId: '3n49vn', + key: 'quotePrompt' + } + ] + } + ], + position: { + x: 718.7528704477357, + y: 112.64438442321625 + }, + moduleId: 'zid0fj' + }, + { + logo: '/imgs/module/reply.png', + name: '指定回复', + intro: '该模块可以直接回复一段指定的内容。常用于引导、提示。', + type: 'answer', + flowType: 'answerNode', + inputs: [ + { + key: 'switch', + type: 'target', + label: '触发器', + connected: true + }, + { + key: 'answerText', + value: '对不起,你的问题不在知识库中。', + type: 'input', + label: '回复的内容', + connected: false + } + ], + outputs: [], + position: { + x: 1171.1202953011716, + y: 213.00404490394536 + }, + moduleId: 'gbnzif' + } + ] + }, + { + id: 'chatGuide', + avatar: '/imgs/module/db.png', + name: '问答前引导', + intro: '可以在每次对话开始前提示用户填写一些内容,作为本次对话的永久内容', + modules: [] + }, + { + id: 'CQ', + avatar: '/imgs/module/cq.png', + name: '意图识别 + 知识库', + intro: '先对用户的问题进行分类,再根据不同类型问题,执行不同的操作', + modules: [ + { + logo: '/imgs/module/userChatInput.png', + name: '用户问题', + intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。', + type: 'initInput', + flowType: 'questionInput', + url: '/openapi/modules/init/userChatInput', + inputs: [ + { + key: 'userChatInput', + type: 'systemInput', + label: '用户问题', + connected: false + } + ], + outputs: [ + { + key: 'userChatInput', + label: '用户问题', + type: 'source', + targets: [ + { + moduleId: '3n49vn', + key: 'userChatInput' + }, + { + moduleId: 'zid0fj', + key: 'userChatInput' + }, + { + moduleId: 'gm15of', + key: 'userChatInput' + } + ] + } + ], + position: { + x: -33.86673792997432, + y: 874.685676808633 + }, + moduleId: 'xzj0oo' + }, + { + logo: '/imgs/module/history.png', + name: '聊天记录', + intro: '用户输入的内容。该模块通常作为应用的入口,用户在发送消息后会首先执行该模块。', + type: 'initInput', + flowType: 'historyNode', + url: '/openapi/modules/init/history', + inputs: [ + { + key: 'maxContext', + type: 'numberInput', + label: '最长记录数', + value: 4, + min: 0, + max: 50, + connected: false + }, + { + key: 'history', + type: 'hidden', + label: '聊天记录', + connected: false + } + ], + outputs: [ + { + key: 'history', + label: '聊天记录', + type: 'source', + targets: [ + { + moduleId: '3n49vn', + key: 'history' + } + ] + } + ], + position: { + x: 1388.8842960266352, + y: 854.1553026226809 + }, + moduleId: 'hh6of9' + }, + { + logo: '/imgs/module/AI.png', + name: 'AI 对话', + intro: 'OpenAI GPT 大模型对话。', + flowType: 'chatNode', + type: 'http', + url: '/openapi/modules/chat/gpt', + inputs: [ + { + key: 'model', + type: 'select', + label: '对话模型', + value: 'gpt-3.5-turbo-16k', + list: [ + { + label: 'Gpt35-16k', + value: 'gpt-3.5-turbo-16k' + }, + { + label: 'Gpt35-4k', + value: 'gpt-3.5-turbo' + }, + { + label: 'Gpt4-8k', + value: 'gpt-4' + } + ], + connected: false + }, + { + key: 'temperature', + type: 'slider', + label: '温度', + value: 0, + min: 0, + max: 10, + step: 1, + markList: [ + { + label: '严谨', + value: 0 + }, + { + label: '发散', + value: 10 + } + ], + connected: false + }, + { + key: 'maxToken', + type: 'slider', + label: '回复上限', + value: 3000, + min: 0, + max: 4000, + step: 50, + markList: [ + { + label: '0', + value: 0 + }, + { + label: '4000', + value: 4000 + } + ], + connected: false + }, + { + key: 'systemPrompt', + type: 'textarea', + label: '系统提示词', + description: + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + placeholder: + '模型固定的引导词,通过调整该内容,可以引导模型聊天方向。该内容会被固定在上下文的开头。', + value: '知识库是关于 Laf 的介绍,根据知识库内容回答问题。', + connected: false + }, + { + key: 'limitPrompt', + type: 'textarea', + label: '限定词', + description: + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + placeholder: + '限定模型对话范围,会被放置在本次提问前,拥有强引导和限定性。例如:\n1. 知识库是关于 Laf 的介绍,参考知识库回答问题,与 "Laf" 无关内容,直接回复: "我不知道"。\n2. 你仅回答关于 "xxx" 的问题,其他问题回复: "xxxx"', + value: '', + connected: false + }, + { + key: 'switch', + type: 'target', + label: '触发器', + connected: false + }, + { + key: 'quotePrompt', + type: 'target', + label: '引用内容', + connected: true + }, + { + key: 'history', + type: 'target', + label: '聊天记录', + connected: true + }, + { + key: 'userChatInput', + type: 'target', + label: '用户问题', + connected: true + } + ], + outputs: [ + { + key: 'answerText', + label: '模型回复', + description: '直接响应,无需配置', + type: 'hidden', + targets: [] + } + ], + position: { + x: 1827.0428559231655, + y: 446.8058354748067 + }, + moduleId: '3n49vn' + }, + { + logo: '/imgs/module/db.png', + name: '知识库搜索', + intro: '去知识库中搜索对应的答案。可作为 AI 对话引用参考。', + flowType: 'kbSearchNode', + type: 'http', + url: '/openapi/modules/kb/search', + inputs: [ + { + key: 'kb_ids', + type: 'custom', + label: '关联的知识库', + value: [], + list: [], + connected: false + }, + { + key: 'similarity', + type: 'slider', + label: '相似度', + value: 0.8, + min: 0, + max: 1, + step: 0.01, + markList: [ + { + label: '0', + value: 0 + }, + { + label: '1', + value: 1 + } + ], + connected: false + }, + { + key: 'limit', + type: 'slider', + label: '单次搜索上限', + value: 5, + min: 1, + max: 20, + step: 1, + markList: [ + { + label: '1', + value: 1 + }, + { + label: '20', + value: 20 + } + ], + connected: false + }, + { + key: 'switch', + type: 'target', + label: '触发器', + connected: true + }, + { + key: 'userChatInput', + type: 'target', + label: '用户问题', + connected: true + } + ], + outputs: [ + { + key: 'rawSearch', + label: '源搜索数据', + type: 'hidden', + response: true, + targets: [] + }, + { + key: 'isEmpty', + label: '搜索结果为空', + type: 'source', + targets: [ + { + moduleId: 'gbnzif', + key: 'switch' + } + ] + }, + { + key: 'quotePrompt', + label: '引用内容', + description: '搜索结果为空时不返回', + type: 'source', + targets: [ + { + moduleId: '3n49vn', + key: 'quotePrompt' + } + ] + } + ], + position: { + x: 850.3203039824494, + y: 919.7043887997417 + }, + moduleId: 'zid0fj' + }, + { + logo: '/imgs/module/reply.png', + name: '指定回复', + intro: '该模块可以直接回复一段指定的内容。常用于引导、提示。', + type: 'answer', + flowType: 'answerNode', + inputs: [ + { + key: 'switch', + type: 'target', + label: '触发器', + connected: true + }, + { + key: 'answerText', + value: '对不起,我找不到你的问题。', + type: 'input', + label: '回复的内容', + connected: false + } + ], + outputs: [], + position: { + x: 1392.0649222586217, + y: 553.0130337399224 + }, + moduleId: 'gbnzif' + }, + { + logo: '/imgs/module/cq.png', + name: '意图识别', + intro: '可以判断用户问题属于哪方面问题,从而执行不同的操作。', + type: 'http', + url: '/openapi/modules/agent/classifyQuestion', + flowType: 'classifyQuestionNode', + inputs: [ + { + key: 'systemPrompt', + type: 'textarea', + label: '系统提示词', + description: + '你可以添加一些特定内容的介绍,从而更好的识别用户的问题类型。这个内容通常是给模型介绍一个它不知道的内容。', + placeholder: '例如: \n1. Laf 是一个云函数开发平台……\n2. Sealos 是一个集群操作系统', + value: + 'Laf 一个云函数开发平台,提供了基于 Node 的 serveless 的快速开发和部署。是一个集「函数计算」、「数据库」、「对象存储」等于一身的一站式开发平台。支持云函数、云数据库、在线编程 IDE、触发器、云存储和静态网站托管等功能。', + connected: false + }, + { + key: 'history', + type: 'target', + label: '聊天记录', + connected: false + }, + { + key: 'userChatInput', + type: 'target', + label: '用户问题', + connected: true + }, + { + key: 'agents', + type: 'custom', + label: '', + value: [ + { + value: '打招呼、问候、身份询问等问题', + key: 'a' + }, + { + value: '商务类、联系方式问题', + key: 'b' + }, + { + value: '其他问题', + key: 'ek3f' + }, + { + value: '关于 Laf 云函数问题', + key: 'psau' + } + ], + connected: false + } + ], + outputs: [ + { + key: 'a', + label: '', + type: 'hidden', + targets: [ + { + moduleId: '6jnrp5', + key: 'switch' + } + ] + }, + { + key: 'b', + label: '', + type: 'hidden', + targets: [ + { + moduleId: 'g13ipe', + key: 'switch' + } + ] + }, + { + key: 'ek3f', + label: '', + type: 'hidden', + targets: [ + { + moduleId: 'gbnzif', + key: 'switch' + } + ] + }, + { + key: 'psau', + label: '', + type: 'hidden', + targets: [ + { + moduleId: 'zid0fj', + key: 'switch' + } + ] + } + ], + position: { + x: 366.0894497581114, + y: 250.81741383805945 + }, + moduleId: 'gm15of' + }, + { + logo: '/imgs/module/reply.png', + name: '指定回复', + intro: '该模块可以直接回复一段指定的内容。常用于引导、提示。', + type: 'answer', + flowType: 'answerNode', + inputs: [ + { + key: 'switch', + type: 'target', + label: '触发器', + connected: true + }, + { + key: 'answerText', + value: '你好,我是 Laf 助手,可以回答你 Laf 相关问题。', + type: 'input', + label: '回复的内容', + connected: false + } + ], + outputs: [], + position: { + x: 855.9439119466947, + y: 15.463108315267931 + }, + moduleId: '6jnrp5' + }, + { + logo: '/imgs/module/reply.png', + name: '指定回复', + intro: '该模块可以直接回复一段指定的内容。常用于引导、提示。', + type: 'answer', + flowType: 'answerNode', + inputs: [ + { + key: 'switch', + type: 'target', + label: '触发器', + connected: true + }, + { + key: 'answerText', + value: '联系方式:xxxxx', + type: 'input', + label: '回复的内容', + connected: false + } + ], + outputs: [], + position: { + x: 854.0492662385566, + y: 320.5010673254856 + }, + moduleId: 'g13ipe' + } + ] + } +]; // export const classifyQuestionDemo: AppItemType = { // id: 'classifyQuestionDemo', diff --git a/client/src/pages/api/openapi/modules/kb/search.ts b/client/src/pages/api/openapi/modules/kb/search.ts index 21ed9cf5f..f9c934a7e 100644 --- a/client/src/pages/api/openapi/modules/kb/search.ts +++ b/client/src/pages/api/openapi/modules/kb/search.ts @@ -72,6 +72,12 @@ export async function kbSearch({ maxToken = 2500, userChatInput }: Props): Promise { + if (kb_ids.length === 0) + return { + isEmpty: true, + rawSearch: [], + quotePrompt: undefined + }; // get vector const promptVector = await openaiEmbedding_system({ input: [userChatInput] diff --git a/client/src/pages/app/detail/components/Settings.tsx b/client/src/pages/app/detail/components/Settings.tsx index 2f7ba4969..e3c0ba1f1 100644 --- a/client/src/pages/app/detail/components/Settings.tsx +++ b/client/src/pages/app/detail/components/Settings.tsx @@ -238,7 +238,7 @@ const Settings = ({ modelId }: { modelId: string }) => { router.prefetch('/chat'); await saveUpdateModel(); } catch (error) {} - router.push(`/chat?modelId=${modelId}`); + router.push(`/chat?appId=${modelId}`); }} > 对话 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 11dd2ce8b..fb817ad70 100644 --- a/client/src/pages/app/detail/components/edit/components/TemplateList.tsx +++ b/client/src/pages/app/detail/components/edit/components/TemplateList.tsx @@ -31,8 +31,8 @@ const ModuleStoreList = ({ position={'fixed'} top={0} left={0} - right={0} bottom={0} + w={'360px'} > { - // if (e.clientX < 400) return; + if (e.clientX < 360) return; onAddNode({ template: item, position: { x: e.clientX, y: e.clientY } diff --git a/client/src/pages/app/detail/components/edit/index.tsx b/client/src/pages/app/detail/components/edit/index.tsx index b8a1b2897..1c62b1ca4 100644 --- a/client/src/pages/app/detail/components/edit/index.tsx +++ b/client/src/pages/app/detail/components/edit/index.tsx @@ -187,6 +187,7 @@ const AppEdit = ({ app, onBack }: Props) => { targets: [] as FlowOutputTargetItemType[] })) })); + console.log(modules); // update inputs and outputs modules.forEach((module) => { diff --git a/client/src/pages/app/detail/index.tsx b/client/src/pages/app/detail/index.tsx index 5009a78e5..4142c1f37 100644 --- a/client/src/pages/app/detail/index.tsx +++ b/client/src/pages/app/detail/index.tsx @@ -83,7 +83,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { {/* pc tab */} - + {appDetail.name} @@ -97,7 +97,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { activeId={currentTab} onChange={(e: any) => { if (e === 'startChat') { - router.push(`/chat?modelId=${appId}`); + router.push(`/chat?appId=${appId}`); } else { setCurrentTab(e); } @@ -118,7 +118,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => { activeId={currentTab} onChange={(e: any) => { if (e === 'startChat') { - router.push(`/chat?modelId=${appId}`); + router.push(`/chat?appId=${appId}`); } else { setCurrentTab(e); } diff --git a/client/src/pages/app/list/component/CreateModal.tsx b/client/src/pages/app/list/component/CreateModal.tsx index 3570569d5..11402ebd4 100644 --- a/client/src/pages/app/list/component/CreateModal.tsx +++ b/client/src/pages/app/list/component/CreateModal.tsx @@ -21,47 +21,16 @@ import { getErrText } from '@/utils/tools'; import { useToast } from '@/hooks/useToast'; import { postCreateApp } from '@/api/app'; import { useRouter } from 'next/router'; -import { chatAppDemo } from '@/constants/app'; +import { appTemplates } from '@/constants/app'; import Avatar from '@/components/Avatar'; import MyIcon from '@/components/Icon'; type FormType = { avatar: string; name: string; - templateId: number; + templateId: string; }; -const templates = [ - { - id: 0, - icon: 'settings', - name: '简单的对话', - intro: '一个极其简单的 AI 对话应用', - modules: chatAppDemo.modules - }, - { - id: 1, - icon: 'settings', - name: '基础知识库', - intro: '每次提问时进行一次知识库搜索,将搜索结果注入 LLM 模型进行参考回答', - modules: chatAppDemo.modules - }, - { - id: 2, - icon: 'settings', - name: '问答前引导', - intro: '可以在每次对话开始前提示用户填写一些内容,作为本次对话的永久内容', - modules: chatAppDemo.modules - }, - { - id: 3, - icon: 'settings', - name: '意图识别 + 知识库', - intro: '先对用户的问题进行分类,再根据不同类型问题,执行不同的操作', - modules: chatAppDemo.modules - } -]; - const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: () => void }) => { const [refresh, setRefresh] = useState(false); const [creating, setCreating] = useState(false); @@ -72,7 +41,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: ( defaultValues: { avatar: '/icon/logo.png', name: '', - templateId: 0 + templateId: appTemplates[0].id } }); @@ -110,7 +79,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: ( const id = await postCreateApp({ avatar: data.avatar, name: data.name, - modules: templates.find((item) => item.id === data.templateId)?.modules || [] + modules: appTemplates.find((item) => item.id === data.templateId)?.modules || [] }); toast({ title: '创建成功', @@ -163,7 +132,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: ( gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)']} gridGap={4} > - {templates.map((item) => ( + {appTemplates.map((item) => ( void; onSuccess: ( }} > - + {item.name} diff --git a/client/src/pages/app/list/index.tsx b/client/src/pages/app/list/index.tsx index 69964c6e7..ef637352c 100644 --- a/client/src/pages/app/list/index.tsx +++ b/client/src/pages/app/list/index.tsx @@ -89,22 +89,27 @@ const MyApps = () => { border={theme.borders.md} boxShadow={'none'} userSelect={'none'} + position={'relative'} _hover={{ boxShadow: '1px 1px 10px rgba(0,0,0,0.2)', borderColor: 'transparent', '& .delete': { display: 'block' + }, + '& .chat': { + display: 'block' } }} onClick={() => router.push(`/app/detail?appId=${app._id}`)} > - + {app.name} } variant={'base'} @@ -129,6 +134,25 @@ const MyApps = () => { > {app.intro || '这个应用还没写介绍~'} + } + variant={'base'} + borderRadius={'md'} + aria-label={'delete'} + display={['', 'none']} + _hover={{ + bg: 'myGray.100' + }} + onClick={(e) => { + e.stopPropagation(); + router.push(`/chat?appId=${app._id}`); + }} + /> ))} diff --git a/client/src/pages/appStore/components/list.tsx b/client/src/pages/appStore/components/list.tsx index ee42104bc..4aaca49a8 100644 --- a/client/src/pages/appStore/components/list.tsx +++ b/client/src/pages/appStore/components/list.tsx @@ -77,7 +77,7 @@ const ShareModelList = ({ size={'sm'} variant={'base'} w={['60px', '70px']} - onClick={() => router.push(`/chat?modelId=${model._id}`)} + onClick={() => router.push(`/chat?appId=${model._id}`)} > 体验 diff --git a/client/src/pages/chat/components/History.tsx b/client/src/pages/chat/components/History.tsx index 1b38eb3a0..a95011b65 100644 --- a/client/src/pages/chat/components/History.tsx +++ b/client/src/pages/chat/components/History.tsx @@ -125,7 +125,7 @@ const PcSliderBar = ({ w={'100%'} h={'100%'} leftIcon={} - onClick={() => router.replace(`/chat?modelId=${modelId}`)} + onClick={() => router.replace(`/chat?appId=${modelId}`)} > 新对话 @@ -176,9 +176,9 @@ const PcSliderBar = ({ onClick={() => { if (item._id === chatId) return; if (isPc) { - router.replace(`/chat?modelId=${item.modelId}&chatId=${item._id}`); + router.replace(`/chat?appId=${item.modelId}&chatId=${item._id}`); } else { - router.push(`/chat?modelId=${item.modelId}&chatId=${item._id}`); + router.push(`/chat?appId=${item.modelId}&chatId=${item._id}`); } }} onContextMenu={(e) => onclickContextMenu(e, item)} @@ -251,7 +251,7 @@ const PcSliderBar = ({ try { await onclickDelHistory(contextMenuData.history._id); if (contextMenuData.history._id === chatId) { - router.replace(`/chat?modelId=${modelId}`); + router.replace(`/chat?appId=${modelId}`); } } catch (error) { console.log(error); diff --git a/client/src/pages/chat/components/ModelList.tsx b/client/src/pages/chat/components/ModelList.tsx index 43c1a8195..6ec39ed18 100644 --- a/client/src/pages/chat/components/ModelList.tsx +++ b/client/src/pages/chat/components/ModelList.tsx @@ -30,7 +30,7 @@ const ModelList = ({ models, modelId }: { models: AppListItemType[]; modelId: st } : {})} onClick={() => { - router.replace(`/chat?modelId=${item._id}`); + router.replace(`/chat?appId=${item._id}`); }} > diff --git a/client/src/pages/chat/components/PhoneSliderBar.tsx b/client/src/pages/chat/components/PhoneSliderBar.tsx index a73c5bf64..cdec7b912 100644 --- a/client/src/pages/chat/components/PhoneSliderBar.tsx +++ b/client/src/pages/chat/components/PhoneSliderBar.tsx @@ -95,7 +95,7 @@ const PhoneSliderBar = ({ color={'white'} leftIcon={} onClick={() => { - router.replace(`/chat?modelId=${modelId}`); + router.replace(`/chat?appId=${modelId}`); onClose(); }} > @@ -128,7 +128,7 @@ const PhoneSliderBar = ({ : {})} onClick={async () => { if (item._id === modelId) return; - router.replace(`/chat?modelId=${item._id}`); + router.replace(`/chat?appId=${item._id}`); onClose(); }} > @@ -159,7 +159,7 @@ const PhoneSliderBar = ({ : {})} onClick={() => { if (item._id === chatId) return; - router.replace(`/chat?modelId=${item.modelId}&chatId=${item._id}`); + router.replace(`/chat?appId=${item.modelId}&chatId=${item._id}`); onClose(); }} > @@ -177,7 +177,7 @@ const PhoneSliderBar = ({ await delChatHistoryById(item._id); loadHistory({ pageNum: 1, init: true }); if (item._id === chatId) { - router.replace(`/chat?modelId=${modelId}`); + router.replace(`/chat?appId=${modelId}`); } }} /> diff --git a/client/src/pages/chat/index.tsx b/client/src/pages/chat/index.tsx index fea76bcb3..bc6982775 100644 --- a/client/src/pages/chat/index.tsx +++ b/client/src/pages/chat/index.tsx @@ -65,7 +65,7 @@ const textareaMinH = '22px'; const Chat = () => { const router = useRouter(); - const { modelId = '', chatId = '' } = router.query as { modelId: string; chatId: string }; + const { appId = '', chatId = '' } = router.query as { appId: string; chatId: string }; const theme = useTheme(); const ChatBox = useRef(null); @@ -179,7 +179,7 @@ const Chat = () => { data: { messages, chatId, - appId: modelId, + appId, model: '' }, onMessage: (text: string) => { @@ -206,7 +206,7 @@ const Chat = () => { // save chat if (newChatId) { setForbidLoadChatData(true); - router.replace(`/chat?modelId=${modelId}&chatId=${newChatId}`); + router.replace(`/chat?appId=${appId}&chatId=${newChatId}`); } abortSignal.signal.aborted && (await delay(500)); @@ -243,7 +243,7 @@ const Chat = () => { }, [ chatId, - modelId, + appId, setChatData, generatingMessage, setForbidLoadChatData, @@ -474,17 +474,17 @@ const Chat = () => { // 获取对话信息 const loadChatInfo = useCallback( async ({ - modelId, + appId, chatId, loading = false }: { - modelId: string; + appId: string; chatId: string; loading?: boolean; }) => { try { loading && setIsLoading(true); - const res = await getInitChatSiteInfo(modelId, chatId); + const res = await getInitChatSiteInfo(appId, chatId); setChatData({ ...res, @@ -502,9 +502,9 @@ const Chat = () => { } // 空 modelId 请求, 重定向到新的 model 聊天 - if (res.modelId !== modelId) { + if (res.modelId !== appId) { setForbidLoadChatData(true); - router.replace(`/chat?modelId=${res.modelId}`); + router.replace(`/chat?appId=${res.modelId}`); } } catch (e: any) { // reset all chat tore @@ -529,15 +529,15 @@ const Chat = () => { ] ); // 初始化聊天框 - useQuery(['init', modelId, chatId], () => { + useQuery(['init', appId, chatId], () => { // pc: redirect to latest model chat - if (!modelId && lastChatModelId) { - router.replace(`/chat?modelId=${lastChatModelId}&chatId=${lastChatId}`); + if (!appId && lastChatModelId) { + router.replace(`/chat?appId=${lastChatModelId}&chatId=${lastChatId}`); return null; } // store id - modelId && setLastChatModelId(modelId); + appId && setLastChatModelId(appId); setLastChatId(chatId); if (forbidLoadChatData) { @@ -546,7 +546,7 @@ const Chat = () => { } return loadChatInfo({ - modelId, + appId, chatId, loading: true }); @@ -559,7 +559,7 @@ const Chat = () => { isLeavePage.current = true; controller.current?.abort(); }; - }, [modelId, chatId]); + }, [appId, chatId]); // context menu component const RenderContextMenu = useCallback( @@ -611,14 +611,14 @@ const Chat = () => { backgroundColor={useColorModeValue('#fdfdfd', '')} > {/* pc always show history. */} - {(isPc || !modelId) && ( + {(isPc || !appId) && ( )} {/* 聊天内容 */} - {modelId && ( + {appId && ( { /> - router.replace(`/chat?modelId=${modelId}`)}> - 新对话 - + router.replace(`/chat?appId=${appId}`)}>新对话 { try { setIsLoading(true); await onclickDelHistory(chatData.chatId); - router.replace(`/chat?modelId=${modelId}`); + router.replace(`/chat?appId=${appId}`); } catch (err) { console.log(err); } @@ -901,7 +899,7 @@ const Chat = () => { - + )}