Sfoglia il codice sorgente

Merge branch 'master' of http://gitlab.smartai.com/zhupei/smartai2dian0

张涛 1 anno fa
parent
commit
9041cb42a3
60 ha cambiato i file con 2804 aggiunte e 314 eliminazioni
  1. 2 1
      README.md
  2. 2 1
      docs/.vitepress/config.js
  3. 2 1
      docs/about.md
  4. BIN
      docs/dify/dify-missing-bootstrap-script.png
  5. 174 0
      docs/other-platform-dify.md
  6. 1 1
      public/i18n/lang-zh.json
  7. 80 0
      src/apis/intelligent.js
  8. 18 0
      src/apis/rgflow-dify.js
  9. 2 0
      src/base/event-bus.js
  10. 1 0
      src/base/storage.js
  11. 32 0
      src/base/store/use-all-agents.js
  12. 9 4
      src/components/wujie/WJExample.vue
  13. 13 0
      src/modules/conversation-board/api-conversation-board.js
  14. 67 0
      src/modules/conversation-board/use-agent-list-4-board.js
  15. 89 0
      src/modules/conversation-board/use-agent-list-4-history.js
  16. 3 7
      src/modules/conversation-chat/DialogFooter.vue
  17. 4 1
      src/modules/conversation-chat/DialogHeader.vue
  18. 122 9
      src/modules/conversation-chat/DialogInput.vue
  19. 16 9
      src/modules/conversation-chat/DialogItem.vue
  20. 31 7
      src/modules/conversation-chat/DialogList.vue
  21. 20 0
      src/modules/conversation-chat/DialogSuggestion.vue
  22. 20 0
      src/modules/conversation-chat/DialogWelcome.vue
  23. 21 0
      src/modules/conversation-chat/dialog-content/DialogContent.vue
  24. 24 0
      src/modules/conversation-chat/dialog-item/BtnSelectItem.vue
  25. 13 0
      src/modules/conversation-chat/dialog-item/BtnStopChat.vue
  26. 62 0
      src/modules/conversation-chat/dialog-item/DialogActions.vue
  27. 74 0
      src/modules/conversation-chat/dialog-item/DialogAnswer.vue
  28. 52 0
      src/modules/conversation-chat/dialog-item/DialogQuestion.vue
  29. 40 0
      src/modules/conversation-chat/dialog-item/dialog-item-common.css
  30. 178 0
      src/modules/conversation-chat/use-websocket-chat.js
  31. 113 0
      src/modules/conversation-dialog/api-dialog.js
  32. 4 5
      src/modules/conversation-history/BtnHistoryItemDelete.vue
  33. 0 1
      src/modules/conversation-history/HistoryItem.vue
  34. 8 74
      src/modules/conversation-history/HistoryTypeDropdown.vue
  35. 52 0
      src/modules/dify/MessageBody.vue
  36. 15 0
      src/modules/dify/TopSetting.vue
  37. 23 28
      src/modules/intelligent/LabelSelected.vue
  38. 42 27
      src/modules/intelligent/ManageTags.vue
  39. 80 19
      src/modules/intelligent/MessageBody.vue
  40. 16 3
      src/modules/intelligent/ModalAddIntel.vue
  41. 9 14
      src/modules/intelligent/ModalSelectIcon.vue
  42. 58 18
      src/modules/intelligent/ModelAddItem.vue
  43. 85 5
      src/modules/intelligent/TopSetting.vue
  44. 605 0
      src/modules/intelligent/rgflow/AgentConfig.vue
  45. 71 0
      src/modules/intelligent/rgflow/Upload.vue
  46. 16 0
      src/modules/intelligent/rgflow/loading.js
  47. 2 2
      src/modules/knowledge/TopSetting.vue
  48. 1 1
      src/modules/login/FormItem.vue
  49. 2 3
      src/views/ConversationBoard.vue
  50. 3 2
      src/views/ConversationHistory.vue
  51. 139 1
      src/views/ConversationQA.vue
  52. 1 1
      src/views/Login.vue
  53. 11 57
      src/views/conversation/ConversationBoardContainer.vue
  54. 210 3
      src/views/conversation/ConversationChatContainer.vue
  55. 3 4
      src/views/conversation/ConversationHistoryContainer.vue
  56. 20 0
      src/views/difyDetail.vue
  57. 2 0
      src/views/manage/CardItem.vue
  58. 32 5
      src/views/manage/common.js
  59. 2 0
      src/views/routes.js
  60. 7 0
      vite.config.mjs

+ 2 - 1
README.md

@@ -54,7 +54,8 @@ $ npm run build
 - [对话系统:小数 测试环境](http://192.168.20.119:9301/) 登录账号:`user4` 密码:`123456`
 - [SmartAI 大模型平台 1.0](http://192.168.20.116:8103/#/knowledgeLib) 登录账号:`admin` 密码: `000000`
 - [大模型 2.0 改版需求文档](https://note.youdao.com/ynoteshare/index.html?id=079cd5646c91c18c347505fb271d0e19&type=note&_time=1733222191533)
-- [大模型 2.0 设计稿](https://www.figma.com/design/qV5j9okRkPQZ5HXbTq2EMl/SmartAl%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%B3%E5%8F%B0?node-id=0-1&t=PQswrqVcnsLWyUZ2-1)
+- [大模型 2.0 设计稿 第一版](https://www.figma.com/design/qV5j9okRkPQZ5HXbTq2EMl/SmartAl%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%B3%E5%8F%B0?node-id=0-1&t=PQswrqVcnsLWyUZ2-1)
+- [大模型 2.0 设计稿 第二版](https://www.figma.com/design/mnOQvzIeYeof8vv520qAvL/%E5%A4%A7%E6%A8%A1%E5%9E%8B2.0?node-id=0-1&node-type=canvas&t=xiXMAJnMYHJ8gPEA-0)
 
 ### 技术相关资源
 

+ 2 - 1
docs/.vitepress/config.js

@@ -43,7 +43,8 @@ export default {
       { text: '事件总线', link: '/event-bus'},
       { text: '主题', link: '/theme'},
       { text: '多语言', link: '/i18n'},
-      { text: '网络请求', link: '/api-request'}
+      { text: '网络请求', link: '/api-request'},
+      { text: '其他平台:dify', link: '/other-platform-dify'}
     ]
   }
 };

+ 2 - 1
docs/about.md

@@ -50,7 +50,8 @@ import Alert from '@components/Alert'
 - [对话系统:小数 测试环境](http://192.168.20.119:9301/) 登录账号:`user4` 密码:`123456`
 - [SmartAI 大模型平台 1.0](http://192.168.20.116:8103/#/knowledgeLib) 登录账号:`admin` 密码: `000000`
 - [大模型 2.0 改版需求文档](https://note.youdao.com/ynoteshare/index.html?id=079cd5646c91c18c347505fb271d0e19&type=note&_time=1733222191533)
-- [大模型 2.0 设计稿](https://www.figma.com/design/qV5j9okRkPQZ5HXbTq2EMl/SmartAl%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%B3%E5%8F%B0?node-id=0-1&t=PQswrqVcnsLWyUZ2-1)
+- [大模型 2.0 设计稿 第一版](https://www.figma.com/design/qV5j9okRkPQZ5HXbTq2EMl/SmartAl%E5%A4%A7%E6%A8%A1%E5%9E%8B%E5%B9%B3%E5%8F%B0?node-id=0-1&t=PQswrqVcnsLWyUZ2-1)
+- [大模型 2.0 设计稿 第二版](https://www.figma.com/design/mnOQvzIeYeof8vv520qAvL/%E5%A4%A7%E6%A8%A1%E5%9E%8B2.0?node-id=0-1&node-type=canvas&t=xiXMAJnMYHJ8gPEA-0)
 
 ### 技术相关资源
 

BIN
docs/dify/dify-missing-bootstrap-script.png


+ 174 - 0
docs/other-platform-dify.md

@@ -0,0 +1,174 @@
+# dify
+
+由于此项目有嵌入`dify`部分功能的需求,针对`dify`源码做了部分调整。可以使用`iframe`的方式嵌入`dify`,并支持对其`样式`与`脚本`的调整。
+
+```vue
+<template>
+<iframe :src="defaultUrl" id="dify"></iframe>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+
+const defaultUrl = ref('http://192.168.20.119:28003');
+
+onMounted(()=>{
+  const ele = document.getElementById('dify');
+  
+  ele.onload = function(){
+    if(ele && ele.contentwindow){
+      const msg1 = {
+        type:'insertstyle',
+        value:`body{background-color:red;font-size:58px;}`
+      };
+
+      ele.contentWindow.postMessage(JSON.stringify(msg1), defaultUrl.value);
+
+      const msg2 = {
+        type: 'insertScript',
+        value: `console.log('hello world');`
+      }
+
+      ele.contentWindow.postMessage(JSON.stringify(msg2), defaultUrl.value);
+    }
+  }
+})
+</script>
+
+<style scoped lang="css"></style>
+```
+
+## dify相关修改记录
+
+### 2024-12-12 dify 配置跨域
+
+为了解决无界`微前端跨域`的问题,修改了dify的配置文件。
+
+`nginx`配置文件目录: `/data/disk2/llm_platform/dify/docker/nginx/conf.d`
+
+`nginx`配置文件
+
+修改:`default.conf.template`文件,添加如下内容:
+
+添加内容如下:
+
+```
+    add_header 'Access-Control-Allow-Origin' '*';
+    add_header 'Access-Control-Allow-Methods' '*';
+```
+
+最终结果如下:
+
+```
+# Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration.
+
+server {
+    listen ${NGINX_PORT};
+    server_name ${NGINX_SERVER_NAME};
+
+    add_header 'Access-Control-Allow-Origin' '*';
+    add_header 'Access-Control-Allow-Methods' '*';
+
+    location /console/api {
+      proxy_pass http://api:5001;
+      include proxy.conf;
+    }
+
+    location /api {
+      proxy_pass http://api:5001;
+      include proxy.conf;
+    }
+
+    location /v1 {
+      proxy_pass http://api:5001;
+      include proxy.conf;
+    }
+
+    location /files {
+      proxy_pass http://api:5001;
+      include proxy.conf;
+    }
+
+    location / {
+      proxy_pass http://web:3000;
+      include proxy.conf;
+    }
+
+    # placeholder for acme challenge location
+    ${ACME_CHALLENGE_LOCATION}
+
+    # placeholder for https config defined in https.conf.template
+    ${HTTPS_CONFIG}
+}
+```
+
+然后执行:`docker restart docker-nginx-1`重启容器即可。
+
+### 2024-12-13 dify 添加事件监听,支持iframe样式和脚本插入
+
+在`大模型2.0`中`嵌入dify`项目时,初始使用`微前端`方式,但是使用微前端时,发现dify的前端使用的是`服务器端渲染`框架`nextjs`,在加载dify项目时,会有报错,经过查询资料得知,`无界`当前不支持`服务器端渲染(SSR)`,无界嵌入`next.js`或者`nuxt.js`都没有好的解决方案。
+
+![](./dify/dify-missing-bootstrap-script.png)
+
+为了解决此问题,考虑了两个方案:
+
+1. 调整为使用`iframe`进行嵌入,通过`postMessage`进行通信,调整样式和脚本。此方案需要调整`dify`的源码,与初衷有些许背离。
+2. 继续使用微前端方式,但是需要修改`无界`的源码,调整起来比较困难。
+
+由于修改无界的源码,需要的时间较多,暂时使用`iframe`方式嵌入。此方案需要调整`dify`的源码。下方记录对`dify`的修改。
+
+#### 修改位置
+
+**文件1**
+
+文件远程地址:`http://192.168.20.116/_next/static/chunks/app/layout-4c8b096bae21994c.js`
+
+文件服务器路径:`/data/disk2/llm_platform/dify/docker/web/.next/static/chunks/app/layout-4c8b096bae21994c.js`
+
+**文件2**
+
+文件远程地址:`http://192.168.20.116/_next/static/chunks/app/(commonLayout)/layout-f8be35226350c774.js`
+
+文件服务器路径:`/data/disk2/llm_platform/dify/docker/web/.next/static/chunks/app/(commonLayout)/layout-f8be35226350c774.js`
+
+**文件3**
+
+文件远程地址:`http://192.168.20.116/_next/static/chunks/app/signin/layout-a9277f33772c3201.js` 
+
+文件服务器目录位置:`/data/disk2/llm_platform/dify/docker/web/.next/static/chunks/app/signin`
+
+#### 修改内容
+
+在上述列出的js文件末尾添加下方脚本:
+
+> 此处的修改接收`message`,并进行相应的`css`和`js`的操作。
+
+```js
+function handleMessage(event) {
+    const data = event.data;
+    try{
+        const dataInfo = JSON.parse(event.data);
+        
+        const type = dataInfo.type;
+        const value = dataInfo.value;
+
+        if(type === 'insertStyle'){
+            // 插入样式表
+            const ele = document.createElement('style');
+            ele.innerHTML = value;
+            document.head.appendChild(ele);
+        }else if(type === 'insertScript'){
+            // 插入脚本
+            const ele = document.createElement('script');
+            ele.innerHTML = value;
+            document.head.appendChild(ele);
+        }else{
+            // 其他
+        }
+    }catch(err){
+        console.log('自定义事件出错');
+    }
+}
+
+window.addEventListener("message", handleMessage, false);
+```

+ 1 - 1
public/i18n/lang-zh.json

@@ -56,7 +56,7 @@
     "document": "文档",
     "edit": "编辑",
     "delete": "删除",
-    "online": "线",
+    "online": "线",
     "offline": "下线",
     "createUser": "创建用户",
     "addedModel": "添加了的模型",

+ 80 - 0
src/apis/intelligent.js

@@ -12,3 +12,83 @@ export function getDialogList(current, pageSize, keyword) {
     method: 'get'
   });
 }
+/**
+ * 新增智能体
+ * @param {Object} data 智能体数据
+ * @param {string} data.id 智能体id
+ * @param {string} data.name 智能体名称
+ * @param {string} data.description 智能体描述
+ * @param {string} data.dialogType 智能体类型 1:rgflow 2dify
+ * @param {string} data.icon 智能体图标
+ * @param {string} data.mode dify智能体类型
+ * */
+
+export function createDialog(data) {
+  return request({
+    url: `/api/dialog/create`,
+    method: 'post',
+    data
+  });
+}
+
+/**
+ * 获取标签列表
+ * @param {string} keyword 搜索关键字
+ * @param {number} labelType 标签类型 1-默认智能体
+ * @param {number} objectId 智能体ID
+ * */
+export function getLabelList(keyword, labelType, objectId = undefined) {
+  const params = {
+    keyword,
+    labelType
+  };
+  if (objectId) {
+    params.objectId = objectId;
+  }
+  return request({
+    url: `/api/label/list`,
+    method: 'get',
+    params
+  });
+}
+/**
+ * 添加标签
+ * @param {string} labelName 标签名称
+ * @param {number} labelType 标签类型 1-默认智能体
+ * */
+export function addLabel(labelName, labelType = 1) {
+  return request({
+    url: `/api/label/add_label`,
+    method: 'post',
+    data: {
+      labelName,
+      labelType
+    }
+  });
+}
+/**
+ * 删除标签
+ * @param {number} labelId 标签ID
+ * */
+export function deleteLabel(labelId) {
+  return request({
+    url: `/api/label/delete_label?labelId=${labelId}`,
+    method: 'delete'
+  });
+}
+/**
+ * 添加标签到智能体
+ * @param {Array} labelIdList 标签ID列表
+ * @param {number} objectId 智能体ID
+ * @param {number} objectId 智能体ID
+ * */
+export function addLabelToDialog(labelIdList, objectId) {
+  return request({
+    url: `/api/label/sign_label`,
+    method: 'post',
+    data: {
+      labelIdList,
+      objectId
+    }
+  });
+}

+ 18 - 0
src/apis/rgflow-dify.js

@@ -0,0 +1,18 @@
+import request from '../base/ajax';
+
+/**
+ * 创建Dify智能体,
+ * @param {Object} data 包含创建Dify所需参数的对象
+ * @param {String} data.mode 模式,可选值:'advanced-chat'、'agent-chat'、'workflow'
+ * */
+export function createDify(data) {
+  const initData = { icon_type: 'emoji', icon: '🤖', icon_background: '#FFEAD5' };
+  initData.mode = data.mode;
+  initData.name = data.name;
+  initData.description = data.description;
+  return request({
+    url: `/dify/console/api/apps`,
+    method: 'post',
+    data: initData
+  });
+}

+ 2 - 0
src/base/event-bus.js

@@ -11,3 +11,5 @@ export const bus_themeChange = new EventBus('theme_change');
 
 export const bus_knowledgeSearch = new EventBus('knowledge_search');
 export const bus_intelligentSearch = new EventBus('intelligent_search');
+
+export const bus_intelligentEdit = new EventBus('intelligent_edit');

+ 1 - 0
src/base/storage.js

@@ -11,6 +11,7 @@ import { MyCookie } from './storage/cookie';
 export const localST_Theme = new MyLocalStorage('theme'); // 主题
 export const localST_Locale = new MyLocalStorage('locale'); // 多语言
 export const localST_RecentAgent = new MyLocalStorage('recent_agent'); // 近期使用的agent
+export const localST_AllAgent = new MyLocalStorage('full_agent'); // 所有可用的agent
 
 /**
  * session storage定义

+ 32 - 0
src/base/store/use-all-agents.js

@@ -1,5 +1,26 @@
 import { defineStore } from 'pinia';
 import { jsonStringify, jsonParse } from '../../utils/utils';
+import { localST_AllAgent } from '../storage';
+
+function allLocalSet(arr) {
+  // 在本地保存记录
+  const result_str = jsonStringify(arr);
+
+  localST_AllAgent.setItem(result_str);
+}
+
+function allLocalGet() {
+  const result_str = localST_AllAgent.getItem();
+
+  const result_list = jsonParse(result_str);
+
+  if (result_list === null) {
+    // json解析发生了错误
+    return [];
+  } else {
+    return result_list;
+  }
+}
 
 /**
  * 系统所有可用的agents列表
@@ -16,12 +37,23 @@ export const useAllAgentStore = defineStore('all_agents', {
      * @returns
      */
     getList() {
+      const cur_list = this.agents;
+
+      if (cur_list.length === 0) {
+        const recent_list = allLocalGet();
+
+        this.agents = recent_list;
+
+        return this.agents;
+      }
+
       return this.agents;
     },
     setList(arr) {
       const list = jsonParse(jsonStringify(arr));
 
       this.agents = list;
+      allLocalSet(list);
     }
   }
 });

+ 9 - 4
src/components/wujie/WJExample.vue

@@ -1,9 +1,9 @@
 <template>
-  <div>项目的首页</div>
+  <!-- <div>项目的首页</div>
   <div>
     <div @click="handleChangeApp(1)">应用1</div>
     <div @click="handleChangeApp(2)">应用2</div>
-  </div>
+  </div> -->
 
   <WujieVue
     width="100%"
@@ -19,9 +19,14 @@
 <script setup>
 import { ref } from 'vue';
 
-const props = defineProps({});
+const props = defineProps({
+  url: {
+    type: String,
+    request: true
+  }
+});
 
-const url = ref('http://localhost:4001/');
+// const url = ref('http://localhost:4001/');
 
 const app1_plug = [
   {

+ 13 - 0
src/modules/conversation-board/api-conversation-board.js

@@ -72,6 +72,19 @@ const res_list_example = {
  * @see https://app.apifox.com/link/project/5404857/apis/api-233232945
  */
 export const conversationBoardAgentList = (params) => {
+  return new Promise((resolve, reject) => {
+    setTimeout(() => {
+      resolve(res_list_example);
+    }, 1000);
+  });
+};
+
+/**
+ * 获取所有对话可用的智能体
+ * 正式环境需要使用的会话列表
+ * @see https://app.apifox.com/link/project/5404857/apis/api-233232945
+ */
+export const conversationBoardAgentListBackup = (params) => {
   const request_url = `/api/dialog/list`;
 
   const send_data = {

+ 67 - 0
src/modules/conversation-board/use-agent-list-4-board.js

@@ -0,0 +1,67 @@
+import { ref } from 'vue';
+
+import { conversationBoardAgentList } from './api-conversation-board';
+
+import { useAllAgentStore } from '../../base/store/use-all-agents';
+
+/**
+ * 获取Agent list
+ * 用于会话 智能体大全
+ * @param {object} opts
+ * @returns
+ */
+export default function useAgentList4Board(opts) {
+  // api请求的状态
+  const api_status = ref(1); // api状态 1 请求未启动 2 请求进心中 3 请求已成功 4 请求失败(网络成功,但是code不等于200) 5 请求出错
+  const api_message = ref(''); // 请求消息 与 api_status 对应的 message 消息内容
+
+  // 历史记录列表数据
+  const list_agents = ref([]); // 历史记录的列表数据
+
+  const allAgents = useAllAgentStore();
+
+  /**
+   * 调用api请求
+   */
+  const handleApiRequest = (opts) => {
+    // 进入加载状态
+    api_status.value = 2;
+    api_message.value = '';
+    conversationBoardAgentList(opts)
+      .then((res) => {
+        if (res.code / 1 === 200) {
+          // 成功
+          const res_list = res.data; // 历史记录列表
+
+          list_agents.value = res_list;
+
+          // 如果是获取所有项目的操作,则将结果进行缓存
+          if (opts && Object.keys(opts).length === 0) {
+            // 在store中缓存数据
+            allAgents.setList(res_list);
+          }
+
+          // api请求成功
+          api_status.value = 3;
+          api_message.value = 'success';
+        } else {
+          // api请求成功,但是业务失败
+          api_status.value = 4;
+          api_message.value = res.msg;
+        }
+      })
+      .catch((err) => {
+        console.log(err);
+        // api请求出错
+        api_status.value = 5;
+        api_message.value = err.message;
+      });
+  };
+
+  return {
+    getAgentList: handleApiRequest,
+    api_status,
+    api_message,
+    list_agents
+  };
+}

+ 89 - 0
src/modules/conversation-board/use-agent-list-4-history.js

@@ -0,0 +1,89 @@
+import { ref } from 'vue';
+import { conversationBoardAgentList } from './api-conversation-board';
+
+import { useAllAgentStore } from '../../base/store/use-all-agents';
+
+/**
+ * 获取Agent list
+ * 用于会话历史记录类别筛选
+ * @param {function} onSuccess
+ * @returns
+ */
+export default function useAgentList4History(onSuccess) {
+  // api请求的状态
+  const api_status = ref(1); // api状态 1 请求未启动 2 请求进心中 3 请求已成功 4 请求失败(网络成功,但是code不等于200) 5 请求出错
+  const api_message = ref(''); // 请求消息 与 api_status 对应的 message 消息内容
+
+  // 历史记录列表数据
+  const list_agents = ref([]); // 历史记录的列表数据
+
+  const allAgents = useAllAgentStore();
+
+  /**
+   * 处理请求参数
+   */
+  const formatListOptions = () => {
+    return {};
+  };
+
+  /**
+   * 调用api请求
+   */
+  const handleApiRequest = () => {
+    // TODO: 从本地store中获取,而非api
+    // 否则每次进入此页面,都需要调用接口
+    const cache_agents = allAgents.agents;
+    if (cache_agents.length !== 0) {
+      if (typeof onSuccess === 'function') {
+        onSuccess(cache_agents[0]);
+      }
+
+      list_agents.value = cache_agents;
+      return;
+    }
+
+    // 处理查询参数
+    const opts = formatListOptions();
+
+    // 进入加载状态
+    api_status.value = 2;
+    api_message.value = '';
+    conversationBoardAgentList(opts)
+      .then((res) => {
+        if (res.code / 1 === 200) {
+          // 成功
+          const res_list = res.data; // 历史记录列表
+          if (res_list.length !== 0) {
+            if (typeof onSuccess === 'function') {
+              onSuccess(res_list[0]);
+            }
+          }
+
+          list_agents.value = res_list;
+          // 在store中缓存数据
+          allAgents.setList(res_list);
+
+          // api请求成功
+          api_status.value = 3;
+          api_message.value = 'success';
+        } else {
+          // api请求成功,但是业务失败
+          api_status.value = 4;
+          api_message.value = err.message;
+        }
+      })
+      .catch((err) => {
+        console.log(err);
+        // api请求出错
+        api_status.value = 5;
+        api_message.value = err.msg;
+      });
+  };
+
+  return {
+    getAgentList: handleApiRequest,
+    api_status,
+    api_message,
+    list_agents
+  };
+}

+ 3 - 7
src/modules/conversation-chat/DialogFooter.vue

@@ -8,13 +8,9 @@
 
 <style lang="css" scoped>
 .dialog-footer {
-  /* margin-top: 30px;
-  margin-bottom: 30px; */
-
-  text-align: center;
-
-  /* font-size: 32px;
-  font-weight: bold; */
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
   color: var(--color-text-1);
 }

+ 4 - 1
src/modules/conversation-chat/DialogHeader.vue

@@ -10,8 +10,11 @@
 .dialog-header {
   /* margin-top: 30px;
   margin-bottom: 30px; */
+  display: flex;
+  justify-content: center;
+  align-items: center;
 
-  text-align: center;
+  /* text-align: center; */
 
   color: var(--color-text-1);
 }

+ 122 - 9
src/modules/conversation-chat/DialogInput.vue

@@ -1,21 +1,134 @@
 <template>
   <div class="dialog-input">
-    <div>会话输入组件</div>
+    <div>
+      <a-textarea placeholder="输入文本内容" allow-clear />
+    </div>
+    <div>
+      <div>
+        <div>联网查询</div>
+      </div>
+
+      <div>
+        <div v-if="enable_frequent_question">常用问题定义</div>
+        <div v-if="enable_file_upload">文件上传</div>
+        <div v-if="enable_voice_recognition">语音识别</div>
+        <div @click="handleSubmit">发送</div>
+      </div>
+    </div>
+
+    <div>文件上传预览区: 当点击文件上传,选择某个文件后,此处需要展示进行预览。</div>
   </div>
 </template>
 
-<script setup></script>
+<script setup>
+const props = defineProps({
+  /**
+   * 是否禁用操作
+   * 默认 false
+   * 当此值为 true 时,此组件的任何功能都不可用
+   * 当对话正在进行时,此值应为 true,意味着不允许继续对话
+   * 直到一轮对话结束,或者一轮对话终止,此值才应该变为 false
+   */
+  disabled: {
+    type: Boolean,
+    default: false,
+    required: false
+  },
+  /**
+   * 是否开启文件上传功能
+   * 默认 true,即默认开启
+   * 当此值为 false 时,该组件将没有文件上传按钮
+   */
+  enable_file_upload: {
+    type: Boolean,
+    default: true,
+    required: false
+  },
+  /**
+   * 是否开启 常用问题 功能
+   * 如果此值为false,则 常用问题 定义功能不可用
+   */
+  enable_frequent_question: {
+    type: Boolean,
+    default: true,
+    required: false
+  },
+  /**
+   * 是否开启音频识别功能
+   * 如果此值为 false,则语音识别功能不可用
+   */
+  enable_voice_recognition: {
+    type: Boolean,
+    default: false,
+    required: false
+  },
+  /**
+   * 在 enable_file_upload === true 的前提下
+   * 文件上传的类型,默认是 *,不限类型
+   * 对应html属性 accept
+   * 对应 arco.design Upload accept 属性
+   * @see https://arco.design/vue/component/upload#API
+   * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#htmlattrdefaccept
+   */
+  file_upload_type: {
+    type: String,
+    default: '*',
+    required: false
+  },
+  /**
+   * 在 enable_file_upload === true 的前提下
+   * 文件上传的数量限制
+   * 默认值为0,表示不限制
+   * 对应 arco.design Upload limit 属性
+   */
+  file_upload_limit: {
+    type: Number,
+    default: 0,
+    required: false
+  },
+  /**
+   * 输入框文本占位提示
+   */
+  text_placeholder: {
+    type: String
+  },
+  /**
+   * 输入的文本长度限制
+   * 默认值为0,表示不限制长度
+   * 对应 arco.design textarea max-length 属性
+   * @see https://arco.design/vue/component/textarea#API
+   */
+  text_limit: {
+    type: Number,
+    default: 0,
+    required: false
+  },
+  /**
+   * 是否显示字数统计
+   * 对应 arco.design textarea show-word-limit
+   * @see https://arco.design/vue/component/textarea#API
+   */
+  show_text_count: {
+    type: Boolean,
+    default: false,
+    required: false
+  }
+});
 
-<style lang="css" scoped>
-.dialog-input {
-  /* margin-top: 30px;
-  margin-bottom: 30px; */
+const emit = defineEmits(['inputTextChange', 'inputFileChange', 'submit']);
 
-  /* text-align: center; */
+const handleSubmit = () => {
+  // 检查是否是禁用状态
+  if (props.disabled) {
+    return;
+  }
 
-  /* font-size: 32px;
-  font-weight: bold; */
+  emit('submit');
+};
+</script>
 
+<style lang="css" scoped>
+.dialog-input {
   color: var(--color-text-1);
 }
 </style>

+ 16 - 9
src/modules/conversation-chat/DialogItem.vue

@@ -1,18 +1,25 @@
 <template>
-  <div class="dialog-item">
-    <div>对话的每一项</div>
+  <div class="dialog-item-container">
+    <DialogQuestion />
+    <DialogAnswer />
   </div>
 </template>
 
-<script setup></script>
+<script setup>
+import DialogQuestion from './dialog-item/DialogQuestion.vue';
+import DialogAnswer from './dialog-item/DialogAnswer.vue';
 
-<style lang="css" scoped>
-.dialog-item {
-  margin-top: 30px;
-  margin-bottom: 30px;
+const props = defineProps({});
+</script>
 
-  /* text-align: center; */
+<style lang="css" scoped>
+.dialog-item-container {
+}
 
+/* .dialog-item {
   color: var(--color-text-1);
-}
+
+  margin-top: 5px;
+  margin-bottom: 5px;
+} */
 </style>

+ 31 - 7
src/modules/conversation-chat/DialogList.vue

@@ -1,6 +1,10 @@
 <template>
   <a-scrollbar type="track" :style="container_style">
-    <div class="dialog-list">
+    <div class="dialog-welcome" v-if="show_welcome">
+      <DialogWelcome />
+    </div>
+
+    <div class="dialog-list" v-if="!show_welcome">
       <DialogItem />
 
       <DialogItem />
@@ -23,16 +27,41 @@
       <DialogItem />
       <DialogItem />
       <DialogItem />
+
+      <DialogSuggestion :suggestion="suggestion" />
     </div>
   </a-scrollbar>
 </template>
 
 <script setup>
 import { computed } from 'vue';
+import DialogWelcome from './DialogWelcome.vue';
 import DialogItem from './DialogItem.vue';
+import DialogSuggestion from './DialogSuggestion.vue';
 
 const props = defineProps({
-  height: Number
+  height: Number,
+
+  /**
+   * 智能体的ID
+   */
+  agent_id: String,
+
+  /**
+   * 对话内容数据项
+   */
+  list: Array,
+
+  /**
+   * 推荐项
+   */
+  suggestion: Object,
+
+  /**
+   * 是否显示欢迎页
+   * 当用户进入对话,还未发送消息时,应该显示欢迎页面
+   */
+  show_welcome: Boolean
 });
 
 const container_style = computed(() => {
@@ -45,11 +74,6 @@ const container_style = computed(() => {
 
 <style lang="css" scoped>
 .dialog-list {
-  /* margin-top: 30px;
-  margin-bottom: 30px; */
-
-  /* text-align: center; */
-
   color: var(--color-text-1);
 }
 </style>

+ 20 - 0
src/modules/conversation-chat/DialogSuggestion.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="dialog-suggestion">
+    <div>对话推荐项,当一轮对话结束后,可能有其他推荐的内容项目,在对话结束处进行展示。</div>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  suggestion: Object
+});
+</script>
+
+<style lang="css" scoped>
+.dialog-suggestion {
+  margin-top: 30px;
+  margin-bottom: 30px;
+
+  color: var(--color-text-1);
+}
+</style>

+ 20 - 0
src/modules/conversation-chat/DialogWelcome.vue

@@ -0,0 +1,20 @@
+<template>
+  <div class="dialog-welcome-content">
+    <div>对话欢迎内容:欢迎内容应该按照agent进行配置,最好是从后端进行返回</div>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  suggestion: Object
+});
+</script>
+
+<style lang="css" scoped>
+.dialog-welcome {
+  margin-top: 30px;
+  margin-bottom: 30px;
+
+  color: var(--color-text-1);
+}
+</style>

+ 21 - 0
src/modules/conversation-chat/dialog-content/DialogContent.vue

@@ -0,0 +1,21 @@
+<template>
+  <div class="dialog-chat-answer-content">
+    <div>这是用户提问的内容区域这是用户提问的内容区域这是用户提问的内容区域</div>
+
+    <slot></slot>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+
+// 接收markdown数据,进行一些转换
+</script>
+
+<style lang="css" scoped>
+.dialog-chat-answer-content {
+  background-color: #fff;
+  border-radius: 10px;
+  padding: 18px;
+}
+</style>

+ 24 - 0
src/modules/conversation-chat/dialog-item/BtnSelectItem.vue

@@ -0,0 +1,24 @@
+<template>
+  <div class="dialog-item-select question-select"></div>
+</template>
+
+<script setup>
+const props = defineProps({
+  is_selected: Boolean
+});
+
+const emit = defineEmits(['change']);
+</script>
+
+<style src="./dialog-item-common.css" />
+<style lang="css" scoped>
+.dialog-item-select {
+  width: 20px;
+  height: 20px;
+  background-color: #fff;
+  flex-shrink: 0;
+}
+
+.question-select {
+}
+</style>

+ 13 - 0
src/modules/conversation-chat/dialog-item/BtnStopChat.vue

@@ -0,0 +1,13 @@
+<template>
+  <div @click="handleClick">停止生成</div>
+</template>
+
+<script setup>
+const emit = defineEmits(['stop']);
+
+const handleClick = () => {
+  emit('stop');
+};
+</script>
+
+<style lang="css" scoped></style>

+ 62 - 0
src/modules/conversation-chat/dialog-item/DialogActions.vue

@@ -0,0 +1,62 @@
+<template>
+  <div class="dialog-actions-container">
+    <div class="dialog-action-part dialog-action-left">
+      <div class="action-btn-item" @click="handleCopy">复制</div>
+      <div class="action-btn-item" @click="handleRetry">再试一次</div>
+      <div class="action-btn-item" @click="handleShare">分享</div>
+    </div>
+
+    <div class="dialog-action-part dialog-action-right">
+      <div class="action-btn-item" @click="handleLike">赞</div>
+      <div class="action-btn-item" @click="handleHate">踩</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+const emit = defineEmits(['retry', 'share']);
+
+/**
+ * 复制内容
+ */
+const handleCopy = () => {};
+/**
+ * 重试
+ */
+const handleRetry = () => {
+  emit('retry');
+};
+/**
+ * 分享
+ */
+const handleShare = () => {};
+/**
+ * 点赞
+ */
+const handleLike = () => {};
+/**
+ * 踩
+ */
+const handleHate = () => {};
+</script>
+
+<style lang="css" scoped>
+.dialog-actions-container {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+
+  /* height: 26px; */
+}
+
+.dialog-action-part {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.action-btn-item {
+  margin-left: 5px;
+  margin-right: 5px;
+}
+</style>

+ 74 - 0
src/modules/conversation-chat/dialog-item/DialogAnswer.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="dialog-item dialog-anwser">
+    <BtnSelectItem v-if="select_btn_visible" @change="handleSelectChange" />
+
+    <div class="dialog-main answer-main">
+      <div class="dialog-icon answer-icon"></div>
+
+      <div class="dialog-content answer-content">
+        <div>
+          <DialogContent>
+            <DialogActions class="answer-actions-btns" />
+          </DialogContent>
+
+          <BtnStopChat @stop="handleStopChat" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+/**
+ * 智能体的回答内容,可能需要包括下面一些功能
+ *
+ * 1. 文档引用,预览
+ * 2. 图片展示、编辑、预览
+ * 3. 代码展示
+ * 4. 回答反馈:分享、点赞、复制
+ */
+import { computed } from 'vue';
+import BtnSelectItem from './BtnSelectItem.vue';
+import DialogContent from '../dialog-content/DialogContent.vue';
+import DialogActions from './DialogActions.vue';
+import BtnStopChat from './BtnStopChat.vue';
+const props = defineProps({
+  show_select: true
+});
+
+const select_btn_visible = computed(() => {
+  return props.show_select === true;
+});
+
+const handleSelectChange = () => {};
+
+/**
+ * 停止答案的生成
+ */
+const handleStopChat = () => {};
+</script>
+
+<style src="./dialog-item-common.css" />
+<style lang="css" scoped>
+.dialog-anwser {
+  color: var(--color-text-1);
+
+  display: flex;
+  justify-content: space-between;
+}
+
+.answer-main {
+}
+.answer-content {
+  display: flex;
+  justify-content: flex-start;
+}
+
+.answer-icon {
+  margin-right: 5px;
+}
+
+.answer-actions-btns {
+  margin-top: 5px;
+}
+</style>

+ 52 - 0
src/modules/conversation-chat/dialog-item/DialogQuestion.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="dialog-item dialog-question">
+    <BtnSelectItem v-if="select_btn_visible" @change="handleSelectChange" />
+
+    <div class="dialog-main question-main">
+      <div class="dialog-content question-content">
+        <div>
+          <DialogContent />
+        </div>
+      </div>
+
+      <div class="dialog-icon question-icon"></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import BtnSelectItem from './BtnSelectItem.vue';
+import DialogContent from '../dialog-content/DialogContent.vue';
+
+const props = defineProps({
+  show_select: true
+});
+
+const select_btn_visible = computed(() => {
+  return props.show_select === true;
+});
+
+const handleSelectChange = () => {};
+</script>
+
+<style src="./dialog-item-common.css" />
+<style lang="css" scoped>
+.dialog-question {
+  color: var(--color-text-1);
+
+  display: flex;
+  justify-content: space-between;
+}
+
+.question-main {
+}
+
+.question-content {
+  display: flex;
+  justify-content: flex-end;
+}
+.question-icon {
+  margin-left: 5px;
+}
+</style>

+ 40 - 0
src/modules/conversation-chat/dialog-item/dialog-item-common.css

@@ -0,0 +1,40 @@
+.dialog-item {
+  color: var(--color-text-1);
+
+  display: flex;
+  justify-content: space-between;
+
+  margin-top: 5px;
+  margin-bottom: 5px;
+}
+
+/* .dialog-item-select {
+  width: 20px;
+  height: 20px;
+  background-color: #fff;
+  flex-shrink: 0;
+} */
+
+.dialog-main {
+  /* background-color: green; */
+  flex-grow: 1;
+
+  display: flex;
+
+  margin-left: 10px;
+  margin-right: 10px;
+}
+.dialog-content {
+  flex-grow: 1;
+
+  display: flex;
+}
+.dialog-chat-core-content {
+}
+.dialog-icon {
+  width: 40px;
+  height: 40px;
+  background-color: #fff;
+  border-radius: 25px;
+  flex-shrink: 0;
+}

+ 178 - 0
src/modules/conversation-chat/use-websocket-chat.js

@@ -0,0 +1,178 @@
+import { ref } from 'vue';
+import { kuky_Authorization } from '../../base/storage';
+
+/**
+ * 获取websocket连接的地址
+ * @param {string} type - 对话类型,可选值:report/excel/chat
+ * @param {string} agent_id - 对话使用的agent ID
+ * @param {string} chat_id - 对话记录的ID
+ * @returns
+ */
+function getWSUrl(type, agent_id, chat_id) {
+  const host = location.host;
+  const token = kuky_Authorization.get();
+
+  if (type === 'report') {
+    return `ws://${host}/api/${type}/ws/${agent_id}/${chat_id}?token=${token}`;
+  } else if (type === 'chat') {
+    return `ws://${host}/api/${type}/ws/${agent_id}/${chat_id}?token=${token}`;
+  } else if (type === 'excel') {
+    return `ws://${host}/api/document/ws/excel`;
+  } else {
+    return `ws://${host}/api/chat/ws/${agent_id}/${chat_id}?token=${token}`;
+  }
+}
+
+/**
+ * 确保一个值是函数
+ * 如果值本身是函数,则返回值本身
+ * 如果值本身不是函数,则返回一个空函数
+ * @param {function} fn
+ * @returns
+ */
+function protectedFunction(fn) {
+  if (typeof fn === 'function') {
+    return fn;
+  }
+
+  return Function.prototype;
+}
+
+/**
+ * 使用websocket进行对话
+ * @see https://developer.mozilla.org/zh-CN/docs/Web/API/WebSockets_API
+ * @param {string} agent_id - agent id
+ * @param {string} chat_id - 对话ID
+ * @param {string} type - 对话类型,可选值:report/excel/chat
+ * @param {object} trigger - 触发的方法
+ * @param {function} trigger.onMessage - 当收到消息时触发的方法
+ * @param {function} trigger.onClose - 当websocket关闭时触发的方法
+ * @param {function} trigger.onError - 当websocket出错时触发的方法
+ * @returns
+ */
+export default function useWebSocketChat(trigger) {
+  let delay_message_list = [];
+
+  const onMessage = protectedFunction(trigger.onMessage);
+  const onClose = protectedFunction(trigger.onClose);
+  const onError = protectedFunction(trigger.onError);
+
+  /**
+   * websocket当前状态
+   * @see https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/readyState
+   * 0 CONNECTING 套接字已经创建,但是连接尚未打开
+   * 1 OPEN 连接已经打开,准备进行通信
+   * 2 CLOSING 连接正在关闭中
+   * 3 CLOSED 连接已关闭或者无法打开
+   * -1 此处自定义值,未启动
+   */
+  const websocket_status = ref(-1);
+
+  /**
+   * 创建一个websocket连接
+   * @returns
+   */
+  function createWSConnection(agent_id, chat_id, type) {
+    const wsUrl = getWSUrl(type, agent_id, chat_id);
+
+    const wskt = new WebSocket(wsUrl);
+
+    // 当 WebSocket 打开时...
+    wskt.onopen = function (event) {
+      delay_message_list.forEach((item) => {
+        wskt.send(item);
+      });
+
+      // 所有消息发送完成后,清空消息队列
+      delay_message_list = [];
+    };
+
+    // 当 WebSocket 关闭时...
+    wskt.onclose = function (event) {
+      onClose(event);
+    };
+
+    // 当有错误发生时...
+    wskt.onerror = function (error) {
+      onError(error);
+    };
+
+    /**
+     * 当收到websocket消息时
+     * @param {WebSocketEvent} event
+     */
+    wskt.onmessage = (event) => {
+      onMessage(event);
+    };
+
+    return wskt;
+  }
+
+  let websocket = null;
+
+  return {
+    /**
+     * 初始化websocket连接
+     * @param {string} agent_id
+     * @param {string} chat_id
+     * @param {string} type
+     */
+    initWSConnection(agent_id, chat_id, type) {
+      if (websocket === null) {
+        // websocket未创建:立即创建
+        websocket = createWSConnection(agent_id, chat_id, type);
+      }
+
+      return websocket;
+    },
+    /**
+     * 发送会话消息
+     * @param {string} msg
+     */
+    sendWSMessage(msg) {
+      if (websocket === null) {
+        // websocket未创建:立即创建
+        websocket = createWSConnection();
+      }
+
+      // 判断socket是否开启
+      // 如果开启,则直接发送,如果没有开启,则将数据推入消息队列
+      if (websocket.readyState === 0) {
+        delay_message_list.push(msg);
+      }
+
+      if (websocket.readyState === 1) {
+        websocket.send(msg);
+      }
+
+      if (websocket.readyState === 2) {
+        // 正在关闭
+        console.log('web socket正在关闭');
+      }
+
+      if (websocket.readyState === 3) {
+        delay_message_list.push(msg);
+
+        // 已关闭: 重新创建socket
+        websocket = createWSConnection();
+      }
+    },
+    /**
+     * 获取websocket实例本身
+     * @returns
+     */
+    getWSInstance() {
+      return websocket;
+    },
+    /**
+     * 结束对话,断开websocket连接
+     */
+    closeWSConnect() {
+      if (websocket === null) {
+        return;
+      }
+
+      websocket.close();
+    }
+  };
+}

+ 113 - 0
src/modules/conversation-dialog/api-dialog.js

@@ -1,3 +1,7 @@
+import ajax from '../../base/ajax';
+
+import { kuky_Authorization } from '../../base/storage';
+
 /**
  * 获取会话历史记录,来自于小数系统
  * http://192.168.20.119:9301/api/agent/90d533729c0211efbf6b0242ac160006/sessions
@@ -11,3 +15,112 @@
  * 新建会话
  * http://192.168.20.119:9301/api/agent/get-chat-id/42e4fcdc9bea11efac300242ac160006
  */
+
+/**
+ * 上传文档
+ * @param {string} id - agentId
+ * @param {files} files - files
+ * @returns {Promise<Object>}
+ */
+const uploadFiles = (id, file) => {
+  return ajax.post(`/api/files/upload/${id}`, file, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+const uploadExcels = (file) => {
+  return ajax.post('/api/document/excel/upload', file, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+const uploadFile = (id, chatId, file) => {
+  return ajax.post(`/api/files/upload/${id}?chat_id=${chatId}`, file, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
+
+/**
+ * 新建对话
+ * @returns {Promise<Object>}
+ */
+const createDialog = (agent_id) => {
+  // 返回数据示例: {"code":200,"msg":"","data":{"chat_id":"44f835504943467f9a9cde261299a06f"}}
+
+  return ajax.get(`/api/agent/get-chat-id/${agent_id}`);
+};
+
+/**
+ * 获取某个agent的所有对话记录
+ * @returns {Promise<Object>}
+ */
+const getDialogs = (agent_id) => {
+  return ajax.get(`/api/agent/${agent_id}/sessions`);
+};
+
+/**
+ * 获取某个对话的对话记录
+ * @returns {Promise<Object>}
+ */
+export const getHistoryLogs = (agent_id, conversation_id) => {
+  return ajax.get(`/api/agent/${agent_id}/${conversation_id}/session_log`);
+};
+
+// 对话过程:
+
+// ws://localhost/api/chat/ws/42e4fcdc9bea11efac300242ac160006/44f835504943467f9a9cde261299a06f?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyIiwidXNlcl9pZCI6MjEsImV4cCI6MTczNDAxMTU4NH0.tLVyD5ds3BqJ8UCZ1pnaUVBhqZ0leqXNUQhG5U2A2u0
+
+const create = (token, agentId, dialogId, type) => {
+  // const wsParam = type === 'report' ? 'report' : 'chat';
+  // const wsUrl =
+  //   type === 'excel'
+  //     ? `ws://${location.host}/api/document/ws/excel`
+  //     : `ws://${location.host}/api/${wsParam}/ws/${agentId}/${dialogId}?token=${token}`;
+
+  const wsUrl = getWSUrl(type, agentId, dialogId);
+
+  const ws = new WebSocket(wsUrl);
+  // 当 WebSocket 打开时...
+  ws.onopen = function (event) {
+    console.log('WebSocket 连接已打开');
+  };
+
+  // 当 WebSocket 关闭时...
+  ws.onclose = function (event) {
+    console.log(event.code);
+  };
+
+  // 当有错误发生时...
+  ws.onerror = function (error) {
+    console.error('WebSocket 出错: ', error);
+  };
+  return ws;
+};
+
+/**
+ * 获取websocket连接的地址
+ * @param {string} type - 对话类型,可选值:report/excel/chat
+ * @param {string} agent_id - 对话使用的agent ID
+ * @param {string} dialog_id - 对话记录的ID
+ * @returns
+ */
+function getWSUrl(type, agent_id, dialog_id) {
+  const host = location.host;
+  const token = kuky_Authorization.get();
+
+  if (type === 'report') {
+    return `ws://${host}/api/${type}/ws/${agent_id}/${dialog_id}?token=${token}`;
+  } else if (type === 'chat') {
+    return `ws://${host}/api/${type}/ws/${agent_id}/${dialog_id}?token=${token}`;
+  } else if (type === 'excel') {
+    return `ws://${host}/api/document/ws/excel`;
+  } else {
+    return `ws://${host}/api/chat/ws/${agent_id}/${dialog_id}?token=${token}`;
+  }
+}

+ 4 - 5
src/modules/conversation-history/BtnHistoryItemDelete.vue

@@ -40,12 +40,11 @@ const handleClick = () => {
 
     // content: '删除后无法恢复'
     onOk(e) {
-      console.log('确定');
-
+      // console.log('确定');
       // handleConfirmDelete();
     },
     async onBeforeOk() {
-      console.log('ok之前1');
+      // console.log('ok之前1');
 
       // await (() => {
       //   return new Promise((resolve) => {
@@ -53,7 +52,7 @@ const handleClick = () => {
       //   });
       // })();
 
-      console.log('ok之前2');
+      // console.log('ok之前2');
 
       try {
         const result = await handleConfirmDeleteV2();
@@ -70,7 +69,7 @@ const handleClick = () => {
       }
     },
     onCancel() {
-      console.log('取消');
+      // console.log('取消');
     }
   });
 };

+ 0 - 1
src/modules/conversation-history/HistoryItem.vue

@@ -125,7 +125,6 @@ const time_display = computed(() => {
  * 分发ID值
  */
 const handleClick = () => {
-  console.log('点击一下');
   emit('active', props.history.id);
 };
 

+ 8 - 74
src/modules/conversation-history/HistoryTypeDropdown.vue

@@ -9,7 +9,7 @@
         v-for="(item, index) of list_agents"
         :key="item.id"
         :disabled="disabled_options"
-        @click="handleOptionClick(item)"
+        @click="handleChatTypeConfirm(item)"
         >{{ item.name }}</a-doption
       >
     </template>
@@ -18,43 +18,33 @@
 
 <script setup>
 import { ref, computed, onMounted } from 'vue';
-import { conversationBoardAgentList } from '../conversation-board/api-conversation-board';
 
-import { useAllAgentStore } from '../../base/store/use-all-agents';
+import useAgentList4History from '../conversation-board/use-agent-list-4-history';
 
 const props = defineProps({
   /**
    * api状态
    * 如果状态值为2,则不允许切换
    */
-  api_status: Number
+  list_status: Number
 });
 
 const btn_label = ref('');
 
-// api请求的状态
-const api_status = ref(1); // api状态 1 请求未启动 2 请求进心中 3 请求已成功 4 请求失败(网络成功,但是code不等于200) 5 请求出错
-const api_message = ref(''); // 请求消息 与 api_status 对应的 message 消息内容
-
-// 历史记录列表数据
-const list_agents = ref([]); // 历史记录的列表数据
+const { api_status, api_message, list_agents, getAgentList } = useAgentList4History((item) => {
+  handleChatTypeConfirm(item);
+});
 
 const emit = defineEmits(['typeSelected']);
 
-const allAgents = useAllAgentStore();
-
 /**
  * 是否禁用下拉菜单
  * 当列表正在加载时,应该禁用下拉菜单
  */
 const disabled_options = computed(() => {
-  return props.api_status === 2;
+  return props.list_status === 2;
 });
 
-const handleOptionClick = (item) => {
-  handleChatTypeConfirm(item);
-};
-
 /**
  * 已经确定类型
  */
@@ -64,65 +54,9 @@ const handleChatTypeConfirm = (item) => {
   emit('typeSelected', item);
 };
 
-/**
- * 处理请求参数
- */
-const formatListOptions = () => {
-  return {};
-};
-
-/**
- * 调用api请求
- */
-const handleApiRequest = () => {
-  // TODO: 从本地store中获取,而非api
-  // 否则每次进入此页面,都需要调用接口
-  const cache_agents = allAgents.agents;
-  if (cache_agents.length !== 0) {
-    handleChatTypeConfirm(cache_agents[0]);
-    list_agents.value = cache_agents;
-    return;
-  }
-
-  // 处理查询参数
-  const opts = formatListOptions();
-
-  // 进入加载状态
-  api_status.value = 2;
-  api_message.value = '';
-  conversationBoardAgentList(opts)
-    .then((res) => {
-      if (res.code / 1 === 200) {
-        // 成功
-        const res_list = res.data; // 历史记录列表
-        if (res_list.length !== 0) {
-          handleChatTypeConfirm(res_list[0]);
-        }
-
-        list_agents.value = res_list;
-        // 在store中缓存数据
-        allAgents.setList(res_list);
-
-        // api请求成功
-        api_status.value = 3;
-        api_message.value = 'success';
-      } else {
-        // api请求成功,但是业务失败
-        api_status.value = 4;
-        api_message.value = err.message;
-      }
-    })
-    .catch((err) => {
-      console.log(err);
-      // api请求出错
-      api_status.value = 5;
-      api_message.value = err.msg;
-    });
-};
-
 onMounted(() => {
   // 调用api
-  handleApiRequest();
+  getAgentList();
 });
 </script>
 

+ 52 - 0
src/modules/dify/MessageBody.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="body-container">
+    <!-- <WujieVue
+      width="100%"
+      height="100%"
+      name="sub-app"
+      :url="defaultUrl"
+      :sandbox="['allow-scripts', 'allow-same-origin', 'allow-forms', 'allow-popups']"
+      :debug="true"
+      :mode="'iframe'"
+      :onError="handleError"
+      :onBeforeLoad="beforeLoad"
+      :onAfterLoad="afterLoad"
+      sync
+    ></WujieVue> -->
+    <iframe id="dify" :src="defaultURL" width="100%" height="100%" frameborder="0"></iframe>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue';
+const props = defineProps({
+  id: {
+    type: String,
+    required: true
+  }
+});
+const BASE_URL = 'http://192.168.20.119:28003/app';
+const defaultURL = ref(BASE_URL + '/' + props.id + '/workflow');
+console.log(defaultURL.value);
+
+onMounted(() => {
+  const element = document.getElementById('dify');
+  element.onload = function () {
+    if (element && element.contentWindow) {
+      console.log('iframe loaded', element.contentWindow);
+      element.contentWindow.postMessage(
+        JSON.stringify({
+          type: 'insertStyle',
+          value: `body{background-color:red;font-size:50px;}`
+        })
+      );
+    }
+  };
+});
+</script>
+<style scoped lang="css">
+.body-container {
+  margin-top: 15px;
+  height: calc(100% - 55px);
+}
+</style>

+ 15 - 0
src/modules/dify/TopSetting.vue

@@ -0,0 +1,15 @@
+<template>
+  <a-breadcrumb class="breadcrumb">
+    <a-breadcrumb-item><icon-apps /></a-breadcrumb-item>
+    <a-breadcrumb-item>智能体管理</a-breadcrumb-item>
+    <a-breadcrumb-item>智能体详情</a-breadcrumb-item>
+  </a-breadcrumb>
+</template>
+
+<script setup></script>
+
+<style lang="css" scoped>
+.breadcrumb {
+  height: 40px;
+}
+</style>

+ 23 - 28
src/modules/intelligent/LabelSelected.vue

@@ -11,8 +11,8 @@
         <icon-search />
       </template>
     </a-input>
-    <a-checkbox-group v-model="checkedList" direction="vertical">
-      <a-checkbox v-for="item in labelList" :key="item.id" :value="item.value">{{ item.name }}</a-checkbox>
+    <a-checkbox-group v-model="checkedList" direction="vertical" @change="handleToAdd">
+      <a-checkbox v-for="item in label_list" :key="item.labelId" :value="item.labelId">{{ item.labelName }}</a-checkbox>
     </a-checkbox-group>
     <div class="footer">
       <div class="label-manage" @click="toManageTags"><icon-tags /> 管理标签</div>
@@ -21,39 +21,34 @@
 </template>
 
 <script setup>
-import { ref } from 'vue';
+import { ref, computed, watch } from 'vue';
+import { funcDebounce } from '../../utils/utils';
 
-const emit = defineEmits(['openManageTags']);
-
-const searchText = ref('');
-const checkedList = ref([]);
-const labelList = ref([
-  {
-    id: 1,
-    name: '标签1',
-    value: '1'
-  },
-  {
-    id: 2,
-    name: '标签2',
-    value: '2'
-  },
-  {
-    id: 3,
-    name: '标签3',
-    value: '3'
+const props = defineProps({
+  labels: {
+    type: Array,
+    default: () => []
   },
-  {
-    id: 4,
-    name: '标签4',
-    value: '4'
+  addedLabels: {
+    type: Array,
+    default: () => []
   }
-]);
+});
+const emit = defineEmits(['openManageTags', 'toSearchLabels', 'addLabelToTarget']);
 
-const handleSearch = () => {};
+const searchText = ref('');
+const checkedList = ref([...props.addedLabels.map((item) => item.labelId)]);
+const label_list = computed(() => props.labels);
+
+const handleSearch = funcDebounce(() => {
+  emit('toSearchLabels', searchText.value);
+}, 300);
 const toManageTags = () => {
   emit('openManageTags');
 };
+const handleToAdd = (checkedList) => {
+  emit('addLabelToTarget', checkedList);
+};
 </script>
 <style scoped lang="css">
 .footer {

+ 42 - 27
src/modules/intelligent/ManageTags.vue

@@ -35,45 +35,50 @@
         </template>
         创建新标签
       </a-tag>
-      <a-tag v-for="tag in tags" :key="tag.name">
-        {{ tag.name }}
-        <icon-delete @click="handleDelete(tag)" style="cursor: pointer" />
+      <a-tag v-for="tag in tags" :key="tag.labelId">
+        {{ tag.labelName }} &nbsp;&nbsp;
+        <a-popconfirm type="warning" content="是否确认删除该标签?" @ok="handleDelete(tag)">
+          <icon-delete style="cursor: pointer" />
+        </a-popconfirm>
       </a-tag>
     </a-space>
   </a-modal>
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
+import { ref, computed, nextTick } from 'vue';
+import { Message } from '@arco-design/web-vue';
+import { addLabel, deleteLabel } from '../../apis/intelligent';
 
 const props = defineProps({
   visible: {
     type: Boolean,
     default: false
+  },
+  labels: {
+    type: Array,
+    default: () => []
   }
 });
-const emit = defineEmits(['update:visible']);
+const emit = defineEmits(['update:visible', 'updateLabels']);
 
-const tags = ref([
-  {
-    name: '标签1'
-  },
-  {
-    name: '标签2'
-  },
-  {
-    name: '标签3'
-  },
-  {
-    name: '标签4'
-  }
-]);
+const tags = computed(() => props.labels);
 const input_ref = ref(null);
 const show_input = ref(false);
 const input_val = ref('');
 const show_model = computed(() => props.visible);
-const handleDelete = (tag) => {
-  tags.value = tags.value.filter((t) => t.name !== tag.name);
+const handleDelete = async (tag) => {
+  try {
+    const res = await deleteLabel(tag.labelId);
+    if (res.code == 200) {
+      // 刷新标签列表
+      emit('updateLabels');
+    } else {
+      Message.error(res.msg);
+    }
+  } catch (err) {
+    console.log(err);
+  }
 };
 const handleCancel = () => {
   emit('update:visible', false);
@@ -89,16 +94,26 @@ const handleEdit = () => {
   });
 };
 
-const handleAdd = () => {
-  if (input_val.value) {
-    tags.value.push({ name: input_val.value });
-    input_val.value = '';
+const handleAdd = async () => {
+  if (input_val.value.trim().length) {
+    try {
+      const res = await addLabel(input_val.value.trim());
+      if (res.code == 200) {
+        // 刷新标签列表
+        emit('updateLabels');
+      } else {
+        Message.error(res.msg);
+      }
+      input_val.value = '';
+    } catch (err) {
+      console.log(err);
+    }
   }
   show_input.value = false;
 };
 </script>
 <style scoped lang="css">
-.arco-form-item-content-wrapper {
-  border-color: var(--color-neutral-4);
+.arco-input-wrapper {
+  background-color: var(--color-neutral-3);
 }
 </style>

+ 80 - 19
src/modules/intelligent/MessageBody.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="body-container" ref="body_container">
     <CardItem v-for="item in moke_data" :key="item.id" :name="item.name" :img_src="item.img_src" :height="200">
-      <template #description> {{ item.desc }}> </template>
+      <template #description> {{ item.desc }} </template>
       <template #settings>
         <a-switch v-model="item.online">
           <template #checked> {{ $t('management.online') }} </template>
@@ -9,19 +9,31 @@
         </a-switch>
       </template>
       <template #labelBtn>
-        <!-- <div class="label-btn"> -->
-        <a-dropdown ref="dropdown" v-model="show_down" class="label-btn" position="bl" :popup-max-height="false">
-          <a-button>
-            <icon-subscribe />
-            {{ $t('management.addLabelBtn') }}</a-button
+        <a-dropdown
+          ref="dropdown"
+          v-model="show_down"
+          position="bl"
+          :popup-max-height="false"
+          @popup-visible-change="changeDropdownVisible($event, item.id)"
+        >
+          <a-button v-if="!item.label.length" class="label-btn">
+            <icon-subscribe /> &nbsp;{{ $t('management.addLabelBtn') }}</a-button
+          >
+          <a-button v-else class="label-btn">
+            <icon-subscribe />&nbsp;{{ item.label.map((it) => it.labelName).join(',') }}</a-button
           >
           <template #content>
             <div class="label-add">
-              <LabelSelected @openManageTags="openManageTags"></LabelSelected>
+              <LabelSelected
+                :labels="label_list"
+                :addedLabels="item.label"
+                @openManageTags="openManageTags"
+                @toSearchLabels="toSearchLabels"
+                @addLabelToTarget="addLabelToTarget($event, item.id)"
+              ></LabelSelected>
             </div>
           </template>
         </a-dropdown>
-        <!-- </div> -->
       </template>
       <template #bottom>
         <div class="bottom">
@@ -30,7 +42,7 @@
             <icon-more :style="{ fontSize: '20px' }" />
           </div>
           <div class="tools" v-if="!item.online">
-            <icon-settings :style="{ fontSize: '20px', cursor: 'pointer' }" @click="editIntelligent" />
+            <icon-settings :style="{ fontSize: '20px', cursor: 'pointer' }" @click="editIntelligent(item)" />
             <icon-delete :style="{ fontSize: '20px', cursor: 'pointer' }" @click="deleteIntelligent" />
           </div>
         </div>
@@ -41,17 +53,18 @@
       <div v-if="scroll_bottom && pages.current >= total_pages">{{ $t('management.noMoreData') }}</div>
     </div>
   </div>
-  <ManageTags v-model:visible="visible"></ManageTags>
+  <ManageTags v-model:visible="visible" :labels="label_list" @updateLabels="updateLabels"></ManageTags>
 </template>
 
 <script setup>
 import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
 import CardItem from '../../views/manage/CardItem.vue';
 import ManageTags from './ManageTags.vue';
-import { getImageIntelligent } from '../../views/manage/common';
+import { getIconIntelligent, getImageIntelligent } from '../../views/manage/common';
 import LabelSelected from './LabelSelected.vue';
 import { getDialogList } from '../../apis/intelligent';
-import { bus_intelligentSearch } from '../../base/event-bus';
+import { bus_intelligentSearch, bus_intelligentEdit } from '../../base/event-bus';
+import { getLabelList, addLabelToDialog } from '../../apis/intelligent';
 
 const moke_data = ref([
   // {
@@ -72,6 +85,8 @@ const total_pages = ref(5);
 const body_container = ref(null);
 const scroll_bottom = ref(false);
 const search_text = ref('');
+const search_label = ref('');
+const label_list = ref([]);
 const dropdown = ref(null);
 const visible = ref(false);
 
@@ -79,11 +94,12 @@ const getDialog = async () => {
   try {
     const res = await getDialogList(pages.current, pages.pageSize, search_text.value);
     const data = res.data.rows.map((item) => {
+      console.log(item.icon, getImageIntelligent(item.icon));
       return {
         ...item,
         time: new Date(item.update_time).toLocaleString('zh-CN'),
         desc: item.description,
-        img_src: getImageIntelligent(item.icon),
+        img_src: getImageIntelligent(item.icon) || getIconIntelligent(item.icon),
         user: item.user.userName || '-',
         online: item.status == 1
       };
@@ -109,14 +125,53 @@ const openManageTags = () => {
   visible.value = true;
 };
 
-const editIntelligent = () => {
-  // TODO:微前端执行编辑页面
-  console.log('editIntelligent');
+const editIntelligent = (item) => {
+  bus_intelligentEdit.emit(item);
 };
 const deleteIntelligent = () => {
   console.log('deleteIntelligent');
 };
 
+// 获取标签列表
+const getLabels = async () => {
+  try {
+    const res = await getLabelList(search_label.value, 1);
+    if (res.code == 200) {
+      label_list.value = [...res.data.rows];
+    }
+  } catch (err) {
+    console.log(err);
+  }
+};
+const updateLabels = () => {
+  search_label.value = '';
+  getLabels();
+};
+const toSearchLabels = (text) => {
+  search_label.value = text;
+  getLabels();
+};
+// 添加标签到智能体
+const addLabelToTarget = async (label_id_list, target_id) => {
+  try {
+    await addLabelToDialog(label_id_list, target_id);
+  } catch (err) {
+    console.log(err);
+  }
+};
+// 当下拉框隐藏时,重新更新对应的智能体数据
+const changeDropdownVisible = async (visible, id) => {
+  if (!visible) {
+    const res = await getLabelList(search_label.value, 1, id);
+    if (res.code == 200) {
+      moke_data.value.forEach((item) => {
+        if (item.id === id) {
+          item.label = [...res.data.rows];
+        }
+      });
+    }
+  }
+};
 bus_intelligentSearch.on((text) => {
   search_text.value = text;
   moke_data.value = [];
@@ -126,6 +181,7 @@ bus_intelligentSearch.on((text) => {
 
 onMounted(() => {
   getDialog();
+  getLabels();
   body_container.value.addEventListener('scroll', scrollHandler);
 });
 
@@ -147,13 +203,18 @@ onBeforeUnmount(() => {
   display: none;
 }
 .label-btn {
-  display: flex;
-  gap: 10px;
-  padding: 8px 16px;
+  text-align: left;
+  padding: 8px 10px;
   background-color: var(--color-neutral-2);
   color: var(--color-neutral-6);
   font-size: 12px;
   cursor: pointer;
+  width: 100%;
+  overflow: hidden;
+  display: -webkit-box;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+  text-overflow: ellipsis;
 }
 .label-add {
   width: 240px;

+ 16 - 3
src/modules/intelligent/ModalAddIntel.vue

@@ -39,7 +39,7 @@ const props = defineProps({
     default: false
   }
 });
-const emit = defineEmits(['update:visible']);
+const emit = defineEmits(['update:visible', 'add_intelligent', 'createRAGFLOWIntelligent']);
 
 const showModal = computed(() => props.visible);
 
@@ -51,8 +51,21 @@ const handleCancel = () => {
 
 const addNewIntelligent = (item) => {
   // TODO: 根据item中的type区分新建智能体的类型
-  emit('update:visible', false);
-  emit('add-intelligent', item);
+  if (item.type === 'ragflow') {
+    // TODO: 新建RAGFLOW智能体
+    emit('update:visible', false);
+    emit('createRAGFLOWIntelligent');
+    // } else if (item.type === 'advanced-chat') {
+    //   // TODO: 新建ARGFLOW智能体
+    //   emit('update:visible', false);
+    //   emit('add_intelligent', item);
+    // } else if (item.type === 'agent-chat') {
+    // } else if (item.type === 'workflow') {
+    // }
+  } else {
+    emit('update:visible', false);
+    emit('add_intelligent', item);
+  }
 };
 </script>
 <style scoped lang="css">

+ 9 - 14
src/modules/intelligent/ModalSelectIcon.vue

@@ -17,7 +17,7 @@
           :class="{ 'icon-border-active': icon.id === target_icon }"
           @click="changeItem(icon.id)"
         >
-          <img class="icon" :src="icon.icon" alt="" />
+          <img class="icon" :src="getIconIntelligent(icon.name)" alt="" />
         </div>
       </div>
     </div>
@@ -26,12 +26,7 @@
 
 <script setup>
 import { ref, computed } from 'vue';
-import intellFrame1 from '../../assets/image/management/intellFrame1.png';
-import intellFrame2 from '../../assets/image/management/intellFrame2.png';
-import intellFrame3 from '../../assets/image/management/intellFrame3.png';
-import intellFrame4 from '../../assets/image/management/intellFrame4.png';
-import intellFrame5 from '../../assets/image/management/intellFrame5.png';
-import intellFrame6 from '../../assets/image/management/intellFrame6.png';
+import { getIconIntelligent } from '../../views/manage/common';
 
 const props = defineProps({
   show_select_icon: {
@@ -42,27 +37,27 @@ const props = defineProps({
 const icons = ref([
   {
     id: 1,
-    icon: intellFrame1
+    name: 'intellFrame1'
   },
   {
     id: 2,
-    icon: intellFrame2
+    name: 'intellFrame2'
   },
   {
     id: 3,
-    icon: intellFrame3
+    name: 'intellFrame3'
   },
   {
     id: 4,
-    icon: intellFrame4
+    name: 'intellFrame4'
   },
   {
     id: 5,
-    icon: intellFrame5
+    name: 'intellFrame5'
   },
   {
     id: 6,
-    icon: intellFrame6
+    name: 'intellFrame6'
   }
 ]);
 
@@ -75,7 +70,7 @@ const changeItem = (id) => {
   target_icon.value = id;
 };
 const handleBeforeOk = (done) => {
-  emit('sendIcon', icons.value.filter((icon) => icon.id === target_icon.value)[0].icon);
+  emit('sendIcon', icons.value.filter((icon) => icon.id === target_icon.value)[0].name);
   emit('update:show_select_icon', false);
   done();
 };

+ 58 - 18
src/modules/intelligent/ModelAddItem.vue

@@ -1,23 +1,17 @@
 <template>
-  <a-modal
-    v-model:visible="showModal"
-    :align-center="false"
-    top="15vh"
-    @cancel="handleCancel"
-    @before-ok="handleBeforeOk"
-  >
+  <a-modal v-model:visible="show_modal" :align-center="false" top="15vh" @ok="handleOk" @cancel="handleCancel">
     <template #title> 新建 </template>
     <div class="section">
       <div class="form-item">
         <div class="label">图标&名称</div>
         <div class="value">
-          <img class="icon" :src="target_icon" alt="" @click="handleSelectIcon" />
-          <a-input v-model:value="form.name" placeholder="请输入名称" />
+          <img class="icon" :src="getIconIntelligent(props.target_icon)" alt="" @click="handleSelectIcon" />
+          <a-input v-model="form.name" placeholder="请输入名称" />
         </div>
       </div>
       <div class="form-item">
         <div class="label">描述</div>
-        <a-input v-model:value="form.description" placeholder="请输入名称" />
+        <a-input v-model="form.description" placeholder="请输入名称" />
       </div>
     </div>
   </a-modal>
@@ -25,28 +19,74 @@
 
 <script setup>
 import { ref, computed } from 'vue';
-
+import { createDify } from '../../apis/rgflow-dify';
+import { createDialog } from '../../apis/intelligent';
+import { getIconIntelligent, dialogType } from '../../views/manage/common';
 const props = defineProps({
   show_model_item: {
     type: Boolean,
     default: false
   },
+  target_intell_type: {
+    type: String,
+    default: 'advanced-chat'
+  },
   target_icon: {
     type: String,
-    default: '../../assets/image/management/intelligent-frame1.png'
+    default: 'intellFrame1'
   }
 });
-const form = ref({
+const formInit = {
   name: '',
   description: ''
-});
+};
+const form = ref(Object.assign({}, formInit));
 
-const emit = defineEmits(['update:show_model_item', 'select_icon']);
+const emit = defineEmits(['update:show_model_item', 'select_icon', 'updateIntelligentList']);
 
-const showModal = computed(() => props.show_model_item);
-const handleBeforeOk = (done) => {};
+let show_modal = computed(() => props.show_model_item);
+
+const handleOk = () => {
+  if (form.value.name.trim().length) {
+    const data = {
+      name: form.value.name,
+      description: form.value.description,
+      mode: props.target_intell_type
+    };
+    createDify(data)
+      .then((res) => {
+        if (res.id && res.id.length) {
+          const dialogData = {
+            id: res.id,
+            name: form.value.name,
+            description: form.value.description,
+            icon: props.target_icon,
+            dialogType: dialogType.DIFY,
+            mode: props.target_intell_type
+          };
+          createOwnDialog(dialogData);
+        }
+      })
+      .catch((err) => {
+        console.log('新建失败', err);
+      })
+      .finally(() => {});
+  }
+};
+const createOwnDialog = async (data) => {
+  try {
+    const res = await createDialog(data);
+    if (res.code == 200) {
+      form.value = Object.assign({}, formInit);
+      emit('update:show_model_item', false);
+      emit('updateIntelligentList');
+    }
+  } catch (err) {
+    console.log('创建对话失败', err);
+  }
+};
 const handleCancel = () => {
-  form.value.name = '';
+  form.value = Object.assign({}, formInit);
   emit('update:show_model_item', false);
 };
 const handleSelectIcon = () => {

+ 85 - 5
src/modules/intelligent/TopSetting.vue

@@ -18,28 +18,70 @@
       {{ $t('management.createIntelligent') }}
     </a-button>
   </a-space>
-  <ModalAddIntel v-model:visible="visible" @add-intelligent="addIntelligent"></ModalAddIntel>
+  <ModalAddIntel
+    v-model:visible="visible"
+    @add_intelligent="addIntelligent"
+    @createRAGFLOWIntelligent="createRAGFLOWIntelligent"
+  ></ModalAddIntel>
   <ModelAddItem
     v-model:show_model_item="show_model_item"
     :target_icon="target_icon"
+    :target_intell_type="target_intell_type"
     @select_icon="selectIcon"
+    @updateIntelligentList="updateIntelligentList"
   ></ModelAddItem>
   <ModelSelectIcon v-model:show_select_icon="show_select_icon" @sendIcon="sendIcon"></ModelSelectIcon>
+  <AgentConfig
+    v-model:show_rg_config="show_rg_config"
+    ref="editAgentKuai"
+    :typeAngint="add_type"
+    :formData="itemObj"
+    @queryList="queryList"
+  ></AgentConfig>
 </template>
 
 <script setup>
-import { ref } from 'vue';
+import { ref, reactive } from 'vue';
+import { useRouter } from 'vue-router';
 import ModalAddIntel from './ModalAddIntel.vue';
 import ModelAddItem from './ModelAddItem.vue';
 import ModelSelectIcon from './ModalSelectIcon.vue';
+import AgentConfig from './rgflow/agentConfig.vue';
 import { funcDebounce } from '../../utils/utils';
-import { bus_intelligentSearch } from '../../base/event-bus';
+import { bus_intelligentSearch, bus_intelligentEdit } from '../../base/event-bus';
+
+const router = useRouter();
 
 const searchText = ref('');
 const visible = ref(false);
 const show_model_item = ref(false);
 const show_select_icon = ref(false);
-const target_icon = ref('../../assets/image/management/intelligent-frame1.png');
+const target_icon = ref('');
+// 当前新建智能体类型
+const target_intell_type = ref('');
+// ragflow配置
+const show_rg_config = ref(false);
+const editAgentKuai = ref(null);
+let add_type = ref('add');
+let itemObj = reactive({});
+const queryList = async (params = {}) => {
+  // setLoading(true);
+  // try {
+  //   const { data } = await querydialogList(params);
+  //   console.log(data, 'data');
+  //   agentList.value = data.map((item) => {
+  //     return {
+  //       ...item,
+  //       off: true,
+  //     };
+  //   });
+  // } catch (err) {
+  //   // you can report use errorHandler or other
+  // } finally {
+  //   setLoading(false);
+  // }
+};
+
 // 搜索
 const handleSearch = funcDebounce(
   () => {
@@ -52,9 +94,18 @@ const handleSearch = funcDebounce(
 const handleCreate = () => {
   visible.value = true;
 };
-
+// 创建Dify智能体
+const createRAGFLOWIntelligent = () => {
+  console.log('createRAGFLOWIntelligent');
+  show_rg_config.value = true;
+};
+const closeAgentConfig = () => {
+  show_rg_config.value = false;
+};
 const addIntelligent = (data) => {
+  //根据智能体类型,打开不同的配置面板data.type
   show_model_item.value = true;
+  target_intell_type.value = data.type;
 };
 const selectIcon = () => {
   show_select_icon.value = true;
@@ -63,6 +114,35 @@ const sendIcon = (icon) => {
   console.log('sendIcon', icon);
   target_icon.value = icon;
 };
+
+const updateIntelligentList = () => {
+  bus_intelligentSearch.emit('');
+  target_icon.value = '';
+};
+/**
+ * 监听编辑智能体事件
+ * @param {Object} item 编辑智能体对象
+ * case 1: 编辑ragflow智能体
+ * case 2: 编辑bs智能体
+ * case 3: 编辑basic智能体
+ * case 4: 编辑Dify智能体
+ * */
+bus_intelligentEdit.on((item) => {
+  switch (item.agentType) {
+    case '1':
+      // TODO: 编辑rgflow智能体
+      // itemObj = item;
+      show_rg_config.value = true;
+      break;
+    case '4':
+      // TODO: 进入Dify智能体配置
+      router.push({ path: '/difydetail', query: { id: item.id } });
+      console.log('item1', item);
+      break;
+    default:
+      console.log('暂不支持该智能体类型');
+  }
+});
 </script>
 <style scoped lang="scss"></style>
 <style>

+ 605 - 0
src/modules/intelligent/rgflow/AgentConfig.vue

@@ -0,0 +1,605 @@
+<template>
+  <a-modal
+    v-model:visible="visible"
+    title="智能体配置"
+    @before-open="handleOpened"
+    @cancel="handleCancel"
+    :ok-loading="loading"
+    @before-ok="handleBeforeOk"
+    :footer="true"
+    title-align="start"
+    width="600px"
+  >
+    <a-row class="">
+      <a-col :span="24">
+        <a-tabs type="capsule" size="large" :active-key="activeKey" @change="handleTabChange">
+          <a-tab-pane key="1" title="助理设置">
+            <a-divider style="margin-bottom: 20px; margin-top: 0" />
+            <div style="width: 100%" :style="{ height: height }">
+              <a-form
+                ref="formRef"
+                :rules="rules"
+                :model="form"
+                @submit="handleSubmit"
+                :style="{ width: '90%', margin: '0 auto' }"
+              >
+                <a-form-item field="name" label="智能体名称">
+                  <a-input v-model="form.name" placeholder="请输入名称" />
+                </a-form-item>
+                <a-form-item label="智能体图标">
+                  <a-space direction="vertical" :style="{ width: '100%' }">
+                    <Upload
+                      v-if="avatarShow"
+                      :action="uploadAction"
+                      :limit="1"
+                      :url="form.icon ? httpUrl + form.icon : ''"
+                      @update:fileList="updateFileList"
+                      @success="handleSuccess"
+                    ></Upload>
+                  </a-space>
+                </a-form-item>
+                <a-form-item label="空回复">
+                  <a-input v-model="form.prompt_config.empty_response" placeholder="" />
+                </a-form-item>
+                <a-form-item label="设置开场白">
+                  <a-textarea v-model="form.prompt_config.prologue" placeholder="" style="height: 100px" />
+                </a-form-item>
+                <a-form-item label="显示引文">
+                  <a-space direction="vertical">
+                    <a-switch v-model="form.prompt_config.quote" size="small" />
+                  </a-space>
+                </a-form-item>
+                <a-form-item label="Self-RAG">
+                  <a-space direction="vertical">
+                    <a-switch v-model="form.prompt_config.self_rag" size="small" />
+                  </a-space>
+                </a-form-item>
+                <a-form-item field="kb_ids" label="知识库">
+                  <a-select v-model="form.kb_ids" placeholder="请选择 ..." multiple>
+                    <a-option v-for="item in tabs" :key="item.id" :value="item.id">
+                      {{ item.name }}
+                    </a-option>
+                  </a-select>
+                </a-form-item>
+              </a-form>
+            </div>
+          </a-tab-pane>
+          <a-tab-pane key="2" title="提示引擎">
+            <a-divider style="margin-bottom: 20px; margin-top: 0" />
+            <a-scrollbar style="width: 100%; overflow: auto" :style="{ height: height }">
+              <a-form
+                ref="formRef1"
+                :rules="rules"
+                :model="form"
+                @submit="handleSubmit"
+                :style="{ width: '90%', margin: '0 auto' }"
+              >
+                <a-form-item field="prompt_config.system" label="系统">
+                  <a-textarea v-model="form.prompt_config.system" placeholder="" style="height: 180px" />
+                </a-form-item>
+                <a-divider style="margin: 0; margin-bottom: 10px" />
+                <a-form-item label="相似度阈值">
+                  <a-slider v-model="form.similarity_threshold" :step="0.01" :min="0" :max="1" />
+                </a-form-item>
+                <a-form-item label="关键字相似度权重">
+                  <a-slider v-model="form.vector_similarity_weight" :step="0.01" :min="0" :max="1" />
+                </a-form-item>
+                <a-form-item label="Top N">
+                  <a-slider v-model="form.top_n" :min="0" :max="30" />
+                </a-form-item>
+                <a-form-item label="Rerank模型">
+                  <a-space direction="vertical" size="large">
+                    <a-select :size="'large'" v-model="form.rerank_id" placeholder="请选择 ..." allow-clear>
+                      <a-optgroup :label="index" v-for="(item, index) in rankModelList" :key="index">
+                        <a-option v-for="obj in item" :key="obj.fid" :disabled="!obj.available" :value="obj.llm_name">
+                          {{ obj.llm_name }}
+                        </a-option>
+                      </a-optgroup>
+                    </a-select>
+                  </a-space>
+                </a-form-item>
+                <a-form-item v-if="form.rerank_id" label="Top-K">
+                  <a-slider v-model="form.top_k" :min="1" :max="2048" />
+                </a-form-item>
+                <a-form-item label="变量">
+                  <div style="width: 100%; text-align: right">
+                    <a-button type="outline" @click="addVariable" style="" size="mini" shape="round"> 新建 </a-button>
+                  </div>
+                </a-form-item>
+                <a-form-item label="">
+                  <a-table :columns="columns" :data="form.prompt_config.parameters" style="width: 100%">
+                    <template #name="{ record, rowIndex }">
+                      <a-input v-model="record.name" placeholder="" />
+                      <!--                    <a-input v-model="form.prompt_config.parameters[rowIndex].key" />-->
+                    </template>
+                    <template #optional="{ record }">
+                      <a-switch v-model="record.optional" size="small" />
+                    </template>
+                    <template #action="{ record }">
+                      <a-button type="text" @click="deleteParameters(record)" style="margin-left: 0" size="small">
+                        <template #icon>
+                          <icon-delete />
+                        </template>
+                      </a-button>
+                    </template>
+                  </a-table>
+                </a-form-item>
+              </a-form>
+            </a-scrollbar>
+          </a-tab-pane>
+          <a-tab-pane key="3" title="模型设置">
+            <a-divider style="margin-bottom: 20px; margin-top: 0" />
+            <div style="width: 100%" :style="{ height: height }">
+              <a-form
+                ref="formRef2"
+                :rules="rules"
+                :model="form"
+                @submit="handleSubmit"
+                :style="{ width: '90%', margin: '0 auto' }"
+              >
+                <a-form-item field="llm_id" label="模型">
+                  <a-space direction="vertical" size="large">
+                    <a-select
+                      :size="'large'"
+                      field="llm_id"
+                      v-model="form.llm_id"
+                      style="width: 400px"
+                      placeholder="请选择"
+                    >
+                      <template v-for="(item, index) in modelList" :key="index">
+                        <a-optgroup :label="index" v-if="item.length > 0">
+                          <a-option v-for="obj in item" :key="obj.fid" :value="obj.llm_name">
+                            {{ obj.llm_name }}
+                          </a-option>
+                        </a-optgroup>
+                      </template>
+                    </a-select>
+                  </a-space>
+                </a-form-item>
+                <a-divider style="margin-bottom: 20px; margin-top: 0" />
+                <a-form-item label="自由">
+                  <a-select default-value="2" :style="{ width: '400px' }" placeholder="">
+                    <a-option value="1">即兴创作</a-option>
+                    <a-option value="2">精确</a-option>
+                    <a-option value="3">平衡</a-option>
+                  </a-select>
+                </a-form-item>
+                <a-form-item label="温度">
+                  <a-switch size="small" v-model="temperature" />
+                  <a-space direction="vertical" size="large">
+                    <a-slider
+                      :disabled="!temperature"
+                      v-model="form.llm_setting.temperature"
+                      :step="0.01"
+                      :min="0"
+                      :max="1"
+                      :style="{
+                        width: '350px',
+                        marginLeft: '20px',
+                        display: 'flex'
+                      }"
+                      show-input
+                    />
+                  </a-space>
+                </a-form-item>
+                <a-form-item label="top P">
+                  <a-switch size="small" v-model="top_p" />
+                  <a-space direction="vertical" size="large">
+                    <a-slider
+                      :disabled="!top_p"
+                      v-model="form.llm_setting.top_p"
+                      :step="0.01"
+                      :min="0"
+                      :max="1"
+                      :style="{
+                        width: '350px',
+                        marginLeft: '20px',
+                        display: 'flex'
+                      }"
+                      show-input
+                    />
+                  </a-space>
+                </a-form-item>
+                <a-form-item label="出席处罚">
+                  <a-switch size="small" v-model="presence_penalty" />
+                  <a-space direction="vertical" size="large">
+                    <a-slider
+                      :disabled="!presence_penalty"
+                      v-model="form.llm_setting.presence_penalty"
+                      :step="0.01"
+                      :min="0"
+                      :max="1"
+                      :style="{
+                        width: '350px',
+                        marginLeft: '20px',
+                        display: 'flex'
+                      }"
+                      show-input
+                    />
+                  </a-space>
+                </a-form-item>
+                <a-form-item label="频率处罚">
+                  <a-switch size="small" v-model="frequency_penalty" />
+                  <a-space direction="vertical" size="large">
+                    <a-slider
+                      :disabled="!frequency_penalty"
+                      v-model="form.llm_setting.frequency_penalty"
+                      :step="0.01"
+                      :min="0"
+                      :max="1"
+                      :style="{
+                        width: '350px',
+                        marginLeft: '20px',
+                        display: 'flex'
+                      }"
+                      show-input
+                    />
+                  </a-space>
+                </a-form-item>
+                <a-form-item label="最大token数">
+                  <a-switch size="small" v-model="max_tokens" />
+                  <a-space direction="vertical" size="large">
+                    <a-slider
+                      :disabled="!max_tokens"
+                      v-model="form.llm_setting.max_tokens"
+                      :min="0"
+                      :max="2048"
+                      :style="{
+                        width: '350px',
+                        marginLeft: '20px',
+                        display: 'flex'
+                      }"
+                      show-input
+                    />
+                  </a-space>
+                </a-form-item>
+              </a-form>
+            </div>
+          </a-tab-pane>
+        </a-tabs>
+      </a-col>
+    </a-row>
+  </a-modal>
+</template>
+
+<script setup>
+import { onMounted, onBeforeMount, reactive, ref, nextTick, onUnmounted, onBeforeUnmount, computed } from 'vue';
+// import { kbdocumentupload, queryKbList, queryModelList } from '@/api/kbList';
+import useLoading from './loading';
+import Upload from './Upload.vue';
+import { Message } from '@arco-design/web-vue';
+// import { dialogSet } from '@/api/Agent';
+
+const props = defineProps(['show_rg_config', 'typeAngint', 'formData']);
+const emit = defineEmits(['queryList', 'update:show_rg_config']);
+
+const { loading, setLoading } = useLoading(true);
+const visible = computed(() => props.show_rg_config);
+const modelList = ref({});
+const rankModelList = ref({});
+let tabs = ref([]);
+const file = ref();
+const formRef = ref();
+const formRef1 = ref();
+const formRef2 = ref();
+const activeKey = ref('1');
+const form = reactive({
+  name: '',
+  icon: '',
+  language: 'English',
+  prompt_config: {
+    empty_response: '',
+    prologue: '你好! 我是你的助理,有什么可以帮到你的吗?',
+    quote: true,
+    self_rag: false,
+    system:
+      '你是一个智能助手,请总结知识库的内容来回答问题,请列举知识库中的数据详细回答。当所有知识库内容都与问题无关时,你的回答必须包括“知识库中未找到您要的答案!”这句话。' +
+      '回答需要考虑聊天历史。\n        以下是知识库:\n        {knowledge}\n        以上是知识库。',
+    parameters: [
+      {
+        index: 0,
+        name: 'knowledge',
+        key: 'knowledge',
+        optional: false
+      }
+    ]
+  },
+  kb_ids: [],
+  llm_id: undefined,
+  llm_setting: {
+    temperature: 0.1,
+    top_p: 0.3,
+    presence_penalty: 0.4,
+    frequency_penalty: 0.7,
+    max_tokens: 512
+  },
+  similarity_threshold: 0.2,
+  vector_similarity_weight: 0.30000000000000004,
+  top_n: 8,
+  rerank_id: '',
+  top_k: 1024
+});
+let temperature = ref(true);
+let top_p = ref(true);
+let presence_penalty = ref(true);
+let frequency_penalty = ref(true);
+let max_tokens = ref(true);
+const httpUrl = localStorage.getItem('httpUrl');
+let avatarShow = ref(true);
+
+const height = ref('calc(500px)');
+
+const columns = [
+  {
+    title: '关键字',
+    dataIndex: 'name',
+    slotName: 'name'
+  },
+  {
+    title: '可选',
+    dataIndex: 'optional',
+    slotName: 'optional'
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    slotName: 'action'
+  }
+];
+
+const formatter = (value) => {
+  return `${(value / 100).toFixed(2)}`;
+};
+
+const uploadAction = '/api/v1/llm/upload'; // 替换为你的上传API
+const fileList = ref([]);
+const imageUrls = ref([]);
+
+const updateFileList = (newFileList) => {
+  fileList.value = newFileList;
+  console.log(newFileList, 88);
+};
+
+const handleSuccess = (urls) => {
+  imageUrls.value = urls; // 拿到上传的图片地址
+  form.icon = urls[0];
+};
+
+const rules = {
+  name: [
+    {
+      required: true,
+      message: '名称不允许为空'
+    }
+  ],
+  'prompt_config.system': [
+    {
+      required: true,
+      message: '系统不允许为空'
+    }
+  ],
+  llm_id: [
+    {
+      required: true,
+      message: '模型不能为空'
+    }
+  ],
+  kb_ids: [
+    {
+      required: true,
+      message: '知识库不能为空'
+    }
+  ]
+};
+
+const handleSubmit = ({ values, errors }) => {
+  console.log('values:', values, '\nerrors:', errors);
+  if (!errors) {
+  }
+};
+
+const handleCancel = () => {
+  formRef.value.resetFields();
+  formRef1.value.resetFields();
+  formRef2.value.resetFields();
+  form.name = '';
+  emit('update:show_rg_config', false);
+  emit('queryList');
+};
+
+const handleOpened = (el) => {
+  // Object.assign(form,{
+  //   name: '',// 用户名
+  //   nameJoin: '',// 昵称
+  //   post: '',// 岗位
+  //   txt: '',// 备注
+  // });
+  activeKey.value = '1';
+  formRef.value.resetFields();
+  formRef1.value.resetFields();
+  formRef2.value.resetFields();
+  nextTick(() => {
+    if (props.typeAngint == 'add') {
+      form.name = '';
+      form.icon = '';
+      form.prompt_config.system =
+        '你是一个智能助手,请总结知识库的内容来回答问题,请列举知识库中的数据详细回答。当所有知识库内容都与问题无关时,你的回答必须包括“知识库中未找到您要的答案!”这句话。' +
+        '回答需要考虑聊天历史。\n        以下是知识库:\n        {knowledge}\n        以上是知识库。';
+    }
+    // console.log(form, '新建智能体弹窗');
+    avatarShow.value = false;
+    setTimeout(() => {
+      avatarShow.value = true;
+    }, 100);
+  });
+};
+
+const handleBeforeOk = async (done) => {
+  formRef.value.validate().then((res) => {
+    // console.log('res:', res)
+  });
+  formRef1.value.validate().then((res) => {
+    // console.log('res:', res)
+  });
+  formRef2.value.validate().then((res) => {
+    // console.log('res:', res)
+  });
+  if (form.name && form.kb_ids.length > 0 && form.prompt_config.system && form.llm_id) {
+    let title = '创建成功';
+    let formNew = { ...form };
+    if (formNew.rerank_id) {
+      delete formNew.top_k;
+    }
+
+    if (formNew.rerank_id) {
+      delete formNew.top_k;
+    }
+
+    formNew.prompt_config.parameters = form.prompt_config.parameters.map((item) => {
+      return {
+        ...item,
+        key: item.name
+      };
+    });
+
+    console.log(formNew, 'formNew');
+
+    if (props.typeAngint == 'edit') {
+      formNew.dialog_id = form.id;
+      delete formNew.id;
+      delete formNew.off;
+      title = '修改成功';
+    }
+
+    if (!max_tokens.value) {
+      //formNew.llm_setting.max_tokens = '';
+      delete formNew.llm_setting.max_tokens;
+    }
+
+    setLoading(true);
+    try {
+      // const data = await dialogSet(formNew);
+      // if (data.code == 0) {
+      //   Message.success(title);
+      //   handleCancel();
+      // TODO: 刷新列表
+      // EventBus.emit('queryList');
+      // } else {
+      //   Message.error(data.msg);
+      // }
+      done(true);
+      setLoading(false);
+    } catch (err) {
+      // you can report use errorHandler or other
+      setLoading(false);
+    }
+  } else {
+    Message.warning('请填写必填项');
+    done(false);
+  }
+};
+
+const editClick = (data) => {
+  visible.value = true;
+  nextTick(() => {
+    // console.log(props.formData,'传入数据');
+    if (props.formData.llm_setting.max_tokens == '') {
+      max_tokens.value = false;
+      delete props.formData.llm_setting.max_tokens;
+    } else {
+      max_tokens.value = true;
+    }
+    Object.assign(form, props.formData);
+    console.log(form, '表单数据');
+  });
+};
+
+const addVariable = () => {
+  form.prompt_config.parameters.push({
+    index: form.prompt_config.parameters.length,
+    key: '',
+    name: '',
+    optional: true
+  });
+  nextTick(() => {
+    formRef.value.validate();
+  });
+};
+
+const deleteParameters = (record) => {
+  // console.log(record, 'record');
+  // console.log(form.prompt_config.parameters);
+  form.prompt_config.parameters = form.prompt_config.parameters.filter((item) => item.index !== record.index);
+};
+
+function handleTabChange(key) {
+  // 处理标签改变事件
+  activeKey.value = key;
+}
+
+const queryModel = async (params) => {
+  try {
+    // const data = await queryModelList(params);
+    // const arrObj = {};
+    // for (const key in data.data) {
+    //   if (Object.hasOwnProperty.call(data.data, key)) {
+    //     const element = data.data[key];
+    //     arrObj[key] = element.filter((item) => item.available === true && item.model_type === 'chat');
+    //   }
+    // }
+    // modelList.value = arrObj;
+    // rankModelList.value = {
+    //   BAAI: [data.data.BAAI[1]],
+    //   Jina: data.data.Jina,
+    //   youdao: data.data.youdao
+    // };
+  } catch (err) {
+    // you can report use errorHandler or other
+  } finally {
+  }
+};
+
+const knowledgeData = async (params = { page: 1, page_size: 20 }) => {
+  setLoading(true);
+  try {
+    // const { data } = await queryKbList(params);
+    // nextTick(() => {
+    //   tabs.value = data;
+    // });
+  } catch (err) {
+    // you can report use errorHandler or other
+  } finally {
+    setLoading(false);
+  }
+};
+
+onBeforeMount(() => {
+  queryModel({});
+  knowledgeData();
+});
+onMounted(() => {});
+
+onBeforeUnmount(() => {});
+
+defineExpose({
+  editClick
+});
+</script>
+<style scoped>
+::v-deep .arco-tabs-nav-tab-list {
+  width: 100%;
+
+  .arco-tabs-tab {
+    width: 33%;
+
+    .arco-tabs-tab-title {
+      width: 100%;
+      display: inline-block;
+      text-align: center;
+    }
+  }
+}
+::v-deep .arco-input-wrapper {
+  background-color: var(--color-neutral-2);
+}
+</style>

+ 71 - 0
src/modules/intelligent/rgflow/Upload.vue

@@ -0,0 +1,71 @@
+<template>
+  <a-upload
+    v-model:fileList="fileList"
+    list-type="picture-card"
+    :limit="limit"
+    :action="action"
+    @change="handleChange"
+    @before-remove="beforeRemove"
+    image-preview
+  />
+</template>
+
+<script setup>
+import { computed, ref, onMounted, watch } from 'vue';
+
+const props = defineProps({
+  limit: {
+    type: Number,
+    default: 1
+  },
+  action: String, // 上传的服务器地址
+  url: String //回显的文件地址
+});
+
+const emit = defineEmits(['update:fileList', 'success', 'handleRemove']);
+const urls = computed(() => props.url);
+const fileList = ref([]);
+
+watch(
+  () => props.url,
+  (newVal) => {
+    if (newVal) {
+      fileList.value = newVal.split(',').map((item) => ({
+        uid: item,
+        name: item,
+        status: 'done',
+        url: item
+      }));
+    }
+  },
+  {
+    deep: true // 开启深度监听
+  }
+);
+
+onMounted(() => {
+  if (urls.value) {
+    fileList.value = urls.value.split(',').map((item) => ({
+      uid: item,
+      name: item,
+      status: 'done',
+      url: item
+    }));
+  }
+});
+const beforeRemove = (file) => {
+  emit('handleRemove');
+  fileList.value = [];
+};
+
+const handleChange = (fileList) => {
+  emit('update:fileList', fileList);
+  const successFiles = fileList.filter((item) => item.status === 'done');
+  if (successFiles.length > 0) {
+    emit(
+      'success',
+      successFiles.map((item) => item.response.data)
+    );
+  }
+};
+</script>

+ 16 - 0
src/modules/intelligent/rgflow/loading.js

@@ -0,0 +1,16 @@
+import { ref } from 'vue';
+
+export default function useLoading(initValue = false) {
+  const loading = ref(initValue);
+  const setLoading = (value) => {
+    loading.value = value;
+  };
+  const toggle = () => {
+    loading.value = !loading.value;
+  };
+  return {
+    loading,
+    setLoading,
+    toggle
+  };
+}

+ 2 - 2
src/modules/knowledge/TopSetting.vue

@@ -18,12 +18,12 @@
       {{ $t('management.createKnowledgeBase') }}
     </a-button>
   </a-space>
-  <modalItem v-model:visible="visible" />
+  <ModalItem v-model:visible="visible"></ModalItem>
 </template>
 
 <script setup>
 import { ref } from 'vue';
-import modalItem from './modalItem.vue';
+import ModalItem from './ModalItem.vue';
 import { funcDebounce } from '../../utils/utils';
 import { bus_knowledgeSearch } from '../../base/event-bus';
 

+ 1 - 1
src/modules/login/FormItem.vue

@@ -159,7 +159,7 @@ const changeFormType = (e) => {
   background-color: var(--color-bg-1);
   border-color: var(--color-neutral-3);
 }
-.arco-form-item-label {
+::v-deep .arco-form-item-label-col > .arco-form-item-label {
   display: none;
 }
 </style>

+ 2 - 3
src/views/ConversationBoard.vue

@@ -26,12 +26,11 @@ const content_style = {
   backgroundSize: 'contain'
 };
 
-const handleAgentActive = () => {
+const handleAgentActive = (target_id) => {
   router.push({
     path: '/chat/qa',
     query: {
-      id: 'hsjaklfja',
-      age: 18
+      agent: target_id
     }
   });
 };

+ 3 - 2
src/views/ConversationHistory.vue

@@ -26,11 +26,12 @@ const content_style = {
   backgroundSize: 'cover'
 };
 
-const handleHistoryActive = ({ target_id }) => {
+const handleHistoryActive = ({ target_id, agent_id }) => {
   router.push({
     path: '/chat/qa',
     query: {
-      id: target_id
+      session: target_id,
+      agent: agent_id
     }
   });
 };

+ 139 - 1
src/views/ConversationQA.vue

@@ -3,12 +3,26 @@
     <ContentContainer>
       <ChatAsideMenu />
 
-      <ConversationChatContainer />
+      <template v-if="query_value_status === 2">
+        <ConversationChatContainer :agent_id="chat_agent" :session_id="chat_session" />
+      </template>
+
+      <template v-if="query_value_status === 3">
+        <a-result status="error" :title="query_value_message">
+          <template #extra>
+            <a-space>
+              <a-button type="primary" @click="handleBack">返回</a-button>
+            </a-space>
+          </template>
+        </a-result>
+      </template>
     </ContentContainer>
   </LayoutExperience>
 </template>
 
 <script setup>
+import { ref, onMounted } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
 import LayoutExperience from './layout/LayoutExperience.vue';
 import ContentContainer from './layout/ContentContainer.vue';
 
@@ -16,7 +30,131 @@ import ChatAsideMenu from './conversation/ChatAsideMenu.vue';
 
 import ConversationChatContainer from './conversation/ConversationChatContainer.vue';
 
+import { useAllAgentStore } from '../base/store/use-all-agents';
+
+/**
+ * 此页面为对话页面
+ * 用户可能从如下几个地方进入对话页面:
+ * 1. 从对话历史记录,点击某个记录,进入对话页面,查看此记录的对话内容,此时,路由会存在两个参数
+ *    agent 历史记录对应的agent ID
+ *    session 历史记录的ID
+ * 2. 从对话智能体大全页面,点击某个智能体,进入对话页面,新建该智能体对应的对话,此时,路由会有一个参数
+ *    agent 对应的agent ID
+ * 此页面大致流程如下:
+ * 1. 检查参数是否合法
+ *    A. 检查是否存在必须参数 agent
+ *    B. 如果agent参数存在,则检查agent值是否在缓存中存在,如果不存在,则意味着有人在地址栏修改参数值,无效的agent值于对话没有意义,故,直接做报错处理
+ * 2. 在参数合法的情况下,进入对话流程
+ *
+ * 根据必须参数 agent ID,可能会存在如下相关的操作:
+ * 1. 此agent对应的主题背景
+ * 2. 此agent可用、不可用的功能设定
+ */
+
+const route = useRoute();
+const router = useRouter();
+
+const allAgent = useAllAgentStore();
+
+const chat_agent = ref('');
+const chat_session = ref('');
+
+const query_value_status = ref(1); // 1 初始化状态 2 合法有效 3 非法无效,报错
+const query_value_message = ref('');
+
+const handleBack = () => {
+  router.back();
+};
+
+/**
+ * 检查是否存在合法的agent参数值
+ * @param agent_id
+ */
+async function isValidAgent(agent_id) {
+  if (!agent_id) {
+    return false;
+  }
+
+  // 检查缓存数据中是否存在此agent id值
+  const agent_list = allAgent.getList();
+
+  if (agent_list.length === 0) {
+    // 通过接口获取所有的agent数据
+    // TOOD: 如果agent_list为空,则意味着内存和本地存储中都没有记录,则需要通过接口获取agent列表数据
+    return true;
+  } else {
+    const has_cache = agent_list.find((item) => item.id === agent_id);
+
+    if (!has_cache) {
+      // 缓存数据中不存在此ID值,非法的操作
+      // 可能是有人通过地址栏修改参数值
+      return false;
+    }
+
+    return true;
+  }
+}
+
+onMounted(async () => {
+  const query = route.query;
+
+  const agent_id = query.agent;
+  const session_id = query.session;
+
+  if (!agent_id) {
+    // 不存在参数
+    query_value_status.value = 3;
+    query_value_message.value = '非法操作:参数异常';
+
+    // TODO: 使用默认的通用agent创建对话
+
+    return;
+  }
+
+  // 有agent参数
+  // 判断已经缓存的agent列表是否存在
+
+  const is_valid = await isValidAgent(agent_id);
+
+  if (!is_valid) {
+    // 不存在参数
+    query_value_status.value = 3;
+    query_value_message.value = '非法操作:参数异常';
+
+    return;
+  }
+
+  query_value_status.value = 2;
+  query_value_message.value = 'success';
+
+  chat_agent.value = agent_id;
+  chat_session.value = session_id;
+});
+
 /**
  * 处理路由参数
+ * 0. 系统首页:此时没有任何参数,应该获取通用agent ID
+ * 1. 历史记录里,点击某个历史记录,进入对话页面,查看历史对话,可以进行后续对话
+ *    显示历史对话
+ * 2. 智能体大全,点击某个智能体,进入对话页面,创建新对话
+ * 3. 近期使用的智能体,点击某个智能体,进入对话页面,创建新对话
+ *
+ * 对话中可能存在的其他功能:分享、常用语输入、推荐、文件上传
+ * 当上次对话正在输出时,不允许下一条提问
+ *
+ * 首页:用户直接提问
+ *
+ * 主题背景自定义
+ */
+
+/**
+ * 从历史记录过来的路由参数:
+ * session 历史记录对应的ID
+ * agent agent id
+ */
+
+/**
+ * 从agent过来的路由参数
+ * agent
  */
 </script>

+ 1 - 1
src/views/Login.vue

@@ -47,7 +47,7 @@ import FormItem from '../modules/login/FormItem.vue';
   align-items: center;
   justify-content: center;
   gap: 100px;
-  height: calc(100% - 110px);
+  height: calc(100% - 115px);
 }
 .intro {
   display: flex;

+ 11 - 57
src/views/conversation/ConversationBoardContainer.vue

@@ -44,28 +44,13 @@ import BoardWelcome from '../../modules/conversation-board/BoardWelcome.vue';
 import BoardTags from '../../modules/conversation-board/BoardTags.vue';
 import BoardAgentList from '../../modules/conversation-board/BoardAgentList.vue';
 
-import { conversationBoardAgentList } from '../../modules/conversation-board/api-conversation-board';
-import { useAllAgentStore } from '../../base/store/use-all-agents';
+import useAgentList4Board from '../../modules/conversation-board/use-agent-list-4-board';
 
 const emit = defineEmits(['agentActive']);
 
 const selected_id = ref('');
 
-// api请求的状态
-const api_status = ref(1); // api状态 1 请求未启动 2 请求进心中 3 请求已成功 4 请求失败(网络成功,但是code不等于200) 5 请求出错
-const api_message = ref(''); // 请求消息 与 api_status 对应的 message 消息内容
-
-// 历史记录列表数据
-const list_agents = ref([]); // 历史记录的列表数据
-
-// store中保存的agents
-const allAgents = useAllAgentStore();
-
-const handleTagChange = (tag_id) => {
-  selected_id.value = tag_id;
-
-  handleApiRequest();
-};
+const { api_status, api_message, list_agents, getAgentList } = useAgentList4Board();
 
 const handleItemActive = (target_id) => {
   emit('agentActive', target_id);
@@ -84,52 +69,21 @@ const formatListOptions = () => {
   return obj;
 };
 
-/**
- * 调用api请求
- */
-const handleApiRequest = () => {
-  // const cache_agents = allAgents.agents;
-  // if (cache_agents.length !== 0) {
-  //   list_agents.value = cache_agents;
-  //   return;
-  // }
-
-  // 处理查询参数
+const handleRequest = () => {
   const opts = formatListOptions();
 
-  // 进入加载状态
-  api_status.value = 2;
-  api_message.value = '';
-  conversationBoardAgentList(opts)
-    .then((res) => {
-      if (res.code / 1 === 200) {
-        // 成功
-        const res_list = res.data; // 历史记录列表
-
-        list_agents.value = res_list;
-        // // 在store中缓存数据
-        // allAgents.setList(res_list);
-
-        // api请求成功
-        api_status.value = 3;
-        api_message.value = 'success';
-      } else {
-        // api请求成功,但是业务失败
-        api_status.value = 4;
-        api_message.value = res.msg;
-      }
-    })
-    .catch((err) => {
-      console.log(err);
-      // api请求出错
-      api_status.value = 5;
-      api_message.value = err.message;
-    });
+  getAgentList(opts);
+};
+
+const handleTagChange = (tag_id) => {
+  selected_id.value = tag_id;
+
+  handleRequest();
 };
 
 onMounted(() => {
   // 调用api
-  handleApiRequest();
+  handleRequest();
 });
 </script>
 

+ 210 - 3
src/views/conversation/ConversationChatContainer.vue

@@ -8,9 +8,25 @@
       <DialogHeader class="conversation-chat-header" :style="{ height: header_height + 'px' }" />
 
       <div class="conversation-chat-content">
-        <DialogList class="conversation-chat-list" :height="list_height" />
+        <DialogList
+          class="conversation-chat-list"
+          :height="list_height"
+          :agent_id="agent_id"
+          :list="chat_log_list"
+          :suggestion="chat_suggestion"
+          :show_welcome="show_welcome"
+          @share="handleChatShare"
+          @submit="handleSuggestSubmit"
+        />
 
-        <DialogInput class="conversation-chat-input" :style="{ height: input_height + 'px' }" />
+        <DialogInput
+          class="conversation-chat-input"
+          :style="{ height: input_height + 'px' }"
+          :disabled="input_disabled"
+          @inputTextChange="handleTextChange"
+          @inputFileChange="handleFileChange"
+          @submit="handleInputSubmit"
+        />
       </div>
 
       <DialogFooter class="conversation-chat-footer" :style="{ height: footer_height + 'px' }" />
@@ -34,17 +50,28 @@ import DialogAsideRight from '../../modules/conversation-chat/DialogAsideRight.v
 
 import useEleSize from '../../utils/use-ele-size';
 
+import useWebSocketChat from '../../modules/conversation-chat/use-websocket-chat';
+import { getHistoryLogs } from '../../modules/conversation-dialog/api-dialog';
+import { jsonParse, jsonStringify } from '../../utils/utils';
+
 /**
  * 1. 历史会话进入此页面,展示历史会话内容
  * 2. 点击智能体,新建会话
  */
 
 const props = defineProps({
+  /**
+   * 智能体的ID
+   * 此参数必须传递
+   */
+  agent_id: String,
   /**
    * 会话ID
    * 如果有此参数,则意味着获取该对话的记录内容
    */
-  chat_id: String
+  session_id: String,
+
+  chat_type: String
 });
 
 const header_height = 30;
@@ -59,6 +86,186 @@ const { width: chat_width, height: chat_height } = useEleSize('chat_main');
 const list_height = computed(() => {
   return chat_height.value - header_height - footer_height - input_height;
 });
+
+// ------------------------------------------------------------------
+// ------------------------------------------------------------------
+// ------------------------------------------------------------------
+
+/**
+ * 对话数据列表
+ */
+const chat_log_list = ref([]);
+const chat_suggestion = ref({});
+
+/**
+ * 是否显示欢迎页
+ * 当用户进入对话页面,还未正式开始对话时
+ * 对话区域会显示欢迎内容
+ * 当用户开始对话时,欢迎内容消失,对话列表开始展示
+ */
+const show_welcome = ref(false);
+
+/**
+ * 分享
+ * @param chat_id
+ */
+const handleChatShare = (chat_id) => {};
+/**
+ * 在聊天界面点击推荐项
+ * 继续对话
+ */
+const handleSuggestSubmit = (suggest) => {};
+
+// ---------------------------------------------
+
+/**
+ * 输入区域数据控制
+ */
+const input_text = ref('');
+const input_files = ref([]);
+
+/**
+ * 是否禁用输入组件
+ */
+const input_disabled = ref(false);
+
+/**
+ * 输入文本发生了变化
+ * @param value
+ */
+const handleTextChange = (value) => {};
+/**
+ * 输入的文件发生了变化
+ * @param files
+ */
+const handleFileChange = (files) => {};
+/**
+ * 输入已经确认要提交了
+ * 1. 文件上传
+ * 2. 创建会话
+ *    get-chat-id
+ *    连接socket
+ * 3. socket 发送文本与文件数据内容
+ * 4. 接收 socket 返回的数据项
+ */
+const handleInputSubmit = () => {
+  /**
+   * 创建会话 get chat id
+   */
+
+  /**
+   * 上传文件,获取文件参数
+   */
+
+  /**
+   * 组装socket参数
+   * 发送websocket消息
+   */
+
+  const msg = jsonStringify({
+    message: '消息内容'
+  });
+
+  handleWSSend(msg);
+};
+
+/**
+ * ------------------------------------------------------------
+ * websocket定义以及初始化
+ */
+const wsTrigger = {
+  /**
+   * 当websocket消息回来后
+   * @param e
+   */
+  onMessage: handleWSMessage,
+  /**
+   * 当websocket消息出错时
+   * @param e
+   */
+  onError: handleWSError,
+  /**
+   * 当websocket消息关闭时
+   * @param e
+   */
+  onClose: handleWSClose
+};
+
+/**
+ * 初始化websocket相关的一些方法
+ */
+const { initWSConnection, sendWSMessage, getWSInstance, closeWSConnect } = useWebSocketChat(wsTrigger);
+
+/**
+ * 发送websocket消息
+ * @param chat_id - 对话记录的ID
+ * @param value
+ */
+function handleWSSend(chat_id, value) {
+  // 初始化websocket连接
+  initWSConnection(props.agent_id, chat_id, props.chat_type);
+  sendWSMessage(value);
+}
+
+/**
+ * 接收websocket消息
+ * @param e
+ */
+function handleWSMessage(e) {
+  const res_data = e.data;
+  const res_content = jsonParse(res_data);
+
+  /**
+   * 类别:
+   * close
+   * error
+   * message
+   * stream
+   */
+  const res_type = res_content.type;
+  const res_message = res_content.message; // 消息内容
+}
+
+/**
+ * 处理websocket错误
+ * @param e
+ */
+function handleWSError(e) {}
+
+/**
+ * 处理websocket关闭事件
+ * @param e
+ */
+function handleWSClose(e) {}
+
+onMounted(() => {
+  // 组件挂载
+  if (props.session_id) {
+    // 有历史记录:获取历史记录内容进行展示
+    getHistoryLogs(props.agent_id, props.session_id)
+      .then((res) => {
+        console.log(res);
+
+        if (res.code / 1 === 200) {
+          // 成功
+          const session_list = res.data;
+        } else {
+          // 异常
+        }
+      })
+      .catch((err) => {
+        console.log(err);
+      });
+  } else {
+    // 无历史记录:等待创建会话内容(用户开始输入、提交完成),列表区域显示欢迎界面
+  }
+});
+
+// ---------------------------------------------------------
+onUnmounted(() => {
+  // 组件卸载,关闭组件内的websocket连接
+  closeWSConnect();
+});
 </script>
 
 <style lang="css" scoped>

+ 3 - 4
src/views/conversation/ConversationHistoryContainer.vue

@@ -6,7 +6,7 @@
       <HistoryName />
 
       <HistorySearchInput @search="handleHistorySearch">
-        <HistoryTypeDropdown @typeSelected="handleTypeSelected" :api_status="api_status" />
+        <HistoryTypeDropdown @typeSelected="handleTypeSelected" :list_status="api_status" />
       </HistorySearchInput>
 
       <HistoryList
@@ -82,7 +82,7 @@ const last_month_zero = yesterday_zero - ONE_MONTH; // 上个月(30天之前的)
 
 /**
  * 将数据进行分类
- * 依据时间戳,将列表数据分成三类型:今天、本月、更早
+ * 依据时间戳,将列表数据分成三类型:今天、本月、更早
  * 今天:从今天零点开始到现在
  * 本月:从今天零点之前,往前推30天
  * 更早:在30天之前的数据
@@ -194,8 +194,6 @@ const handleApiRequest = () => {
       }
     })
     .catch((err) => {
-      // console.log(err, '这是错误吗??');
-
       // api请求出错
       api_status.value = 5;
       api_message.value = err.message;
@@ -227,6 +225,7 @@ const handleListLoadMore = () => {
  */
 const handleListItemActive = (history_id) => {
   emit('historyActive', {
+    agent_id: agent_id.value, // agent_id值
     target_id: history_id
   });
 };

+ 20 - 0
src/views/difyDetail.vue

@@ -0,0 +1,20 @@
+<template>
+  <LayoutExperience>
+    <ContentContainer>
+      <TopSetting></TopSetting>
+      <MessageBody :id="id"></MessageBody>
+    </ContentContainer>
+  </LayoutExperience>
+</template>
+
+<script setup>
+import { useRoute } from 'vue-router';
+import LayoutExperience from './layout/LayoutExperience.vue';
+import ContentContainer from './layout/ContentContainer.vue';
+import TopSetting from '../modules/dify/TopSetting.vue';
+import MessageBody from '../modules/dify/MessageBody.vue';
+
+const route = useRoute();
+
+const id = route.query.id;
+</script>

+ 2 - 0
src/views/manage/CardItem.vue

@@ -41,6 +41,7 @@ const props = defineProps({
   position: relative;
   padding: 10px 20px;
   background-color: var(--color-bg-2);
+  overflow: hidden;
 }
 .card-name .name {
   margin-top: 10px;
@@ -53,6 +54,7 @@ const props = defineProps({
   margin-top: 15px;
   color: var(--color-neutral-4);
 }
+
 .card-des {
   display: -webkit-box;
   overflow: hidden;

+ 32 - 5
src/views/manage/common.js

@@ -10,6 +10,12 @@ import intelligentFrame5 from '../../assets/image/management/intelligent-frame5.
 import intelligentFrame6 from '../../assets/image/management/intelligent-frame6.png';
 import intelligentFrame7 from '../../assets/image/management/intelligent-frame7.png';
 import intelligentFrame8 from '../../assets/image/management/intelligent-frame8.png';
+import intellFrame1 from '../../assets/image/management/intellFrame1.png';
+import intellFrame2 from '../../assets/image/management/intellFrame2.png';
+import intellFrame3 from '../../assets/image/management/intellFrame3.png';
+import intellFrame4 from '../../assets/image/management/intellFrame4.png';
+import intellFrame5 from '../../assets/image/management/intellFrame5.png';
+import intellFrame6 from '../../assets/image/management/intellFrame6.png';
 
 const image_knowLedge_map = {
   knowledgeFrame1: knowledgeFrame1,
@@ -27,6 +33,24 @@ const image_intelligent_map = {
   intelligentFrame7: intelligentFrame7,
   intelligentFrame8: intelligentFrame8
 };
+const icon_intelligent_map = {
+  intellFrame1: intellFrame1,
+  intellFrame2: intellFrame2,
+  intellFrame3: intellFrame3,
+  intellFrame4: intellFrame4,
+  intellFrame5: intellFrame5,
+  intellFrame6: intellFrame6
+};
+/**
+ * @description: 智能体类型
+ * */
+export const dialogType = {
+  RAGFLOW_TYPE: '1',
+  BS_TYPE: '2',
+  BASIC: '3',
+  DIFY: '4'
+};
+
 /**
  * @description: 图片映射表
  * avatar: 图片名称
@@ -36,7 +60,10 @@ export const getImageKnowLedge = (avatar) => {
   return image_knowLedge_map[avatar] || knowledgeFrame1;
 };
 export const getImageIntelligent = (avatar) => {
-  return image_intelligent_map[avatar] || intelligentFrame1;
+  return image_intelligent_map[avatar];
+};
+export const getIconIntelligent = (avatar) => {
+  return icon_intelligent_map[avatar] || intellFrame1;
 };
 
 /**
@@ -48,25 +75,25 @@ export const getDefaultIntelligent = () => {
       name: '问答助理',
       description: 'EMBEDDING,SPEECH2TEXT,MODERATION',
       labels: 'LLM,TEXT',
-      type: 'QA'
+      type: 'ragflow'
     },
     {
       name: '工作流助手',
       description: 'EMBEDDING,SPEECH2TEXT,MODERATION',
       labels: 'LLM,TEXT',
-      type: 'ZS'
+      type: 'advanced-chat'
     },
     {
       name: '智能Agent',
       description: 'EMBEDDING,SPEECH2TEXT,MODERATION',
       labels: 'LLM,TEXT',
-      type: 'AG'
+      type: 'agent-chat'
     },
     {
       name: '工作流应用',
       description: 'EMBEDDING,SPEECH2TEXT,MODERATION',
       labels: 'LLM,TEXT',
-      type: 'YY'
+      type: 'workflow'
     }
   ];
 };

+ 2 - 0
src/views/routes.js

@@ -24,6 +24,8 @@ const routes = [
   { path: '/agent', component: () => import('./Agent.vue') },
   // 智能体
   { path: '/intelligent', component: () => import('./Intelligent.vue') },
+  // dify智能体详情
+  { path: '/difydetail', component: () => import('./difyDetail.vue') },
 
   // 权限管理
   {

+ 7 - 0
vite.config.mjs

@@ -27,6 +27,13 @@ export default defineConfig({
         secure: false,
         ws: true,
         rewrite: (path) => path.replace(/^\/proxy_url/, '')
+      },
+      '/dify': {
+        target: 'http://192.168.20.119:28003/',
+        changeOrigin: true,
+        secure: false,
+        ws: true,
+        rewrite: (path) => path.replace(/^\/dify/, '')
       }
     }
   },