ソースを参照

feat:新增首页问答,知识图谱等

hanyang 1 年間 前
コミット
4209bee653
55 ファイル変更3968 行追加801 行削除
  1. 247 256
      package-lock.json
  2. 5 3
      package.json
  3. 4 3
      public/config.js
  4. 17 3
      public/i18n/lang-en.json
  5. 18 3
      public/i18n/lang-zh.json
  6. 4 4
      src/3rd-libs/arco-vue-install-icon.js
  7. 10 0
      src/apis/intelligent.js
  8. BIN
      src/assets/chat/edit-1.png
  9. BIN
      src/assets/chat/file-1.png
  10. BIN
      src/assets/chat/file-2.png
  11. BIN
      src/assets/chat/file-3.png
  12. BIN
      src/assets/chat/internet-active.png
  13. BIN
      src/assets/chat/internet.png
  14. BIN
      src/assets/chat/stop-1.png
  15. 3 0
      src/base/theme/theme-dark.css
  16. 2 0
      src/base/theme/theme-light.css
  17. 2 0
      src/base/theme/theme-party.css
  18. 156 0
      src/components/11.json
  19. 214 0
      src/components/22.json
  20. 156 0
      src/components/33.json
  21. 102 0
      src/components/Editor.vue
  22. 194 71
      src/components/MindMap.vue
  23. 25 0
      src/modules/conversation-aside/ChatAsideExpandBtn.vue
  24. 64 0
      src/modules/conversation-aside/Conversation-chat-mind-tools.vue
  25. 61 0
      src/modules/conversation-aside/Conversation-chat-web-page.vue
  26. 103 0
      src/modules/conversation-aside/web-page/Web-page-list.vue
  27. 69 0
      src/modules/conversation-chat/DialogCommonUsed.vue
  28. 34 5
      src/modules/conversation-chat/DialogInput.vue
  29. 5 1
      src/modules/conversation-chat/DialogItem.vue
  30. 20 2
      src/modules/conversation-chat/DialogList.vue
  31. 30 0
      src/modules/conversation-chat/DialogStopSse.vue
  32. 11 6
      src/modules/conversation-chat/DialogWelcome.vue
  33. 148 24
      src/modules/conversation-chat/dialog-input/InputActions.vue
  34. 13 0
      src/modules/conversation-chat/use-conversation-chat-parameters.js
  35. 33 1
      src/modules/conversation-content/DialogContent.vue
  36. 55 0
      src/modules/conversation-dialog/api-dialog.js
  37. 30 3
      src/modules/conversation-dialog/format-dialog-answer-from-sse_receive_message.js
  38. 16 4
      src/modules/conversation-dialog/use-sse-chat.js
  39. 25 2
      src/modules/conversation-item/DialogActions.vue
  40. 26 2
      src/modules/conversation-item/DialogAnswer.vue
  41. 2 1
      src/modules/conversation-item/DialogQuestion.vue
  42. 5 2
      src/modules/intelligent/MessageBody.vue
  43. 22 3
      src/modules/intelligent/TopSetting.vue
  44. 2 1
      src/modules/intelligent/rgflow/AgentConfig.vue
  45. 1 1
      src/router/index.js
  46. 91 58
      src/views/ConversationQA.vue
  47. 58 78
      src/views/Home.vue
  48. 4 3
      src/views/SettingsPassword.vue
  49. 0 0
      src/views/conversation/ChatAsideExpand.vue
  50. 0 12
      src/views/conversation/ChatAsideExpandBtn.vue
  51. 24 2
      src/views/conversation/ChatAsideMenu.vue
  52. 1133 0
      src/views/conversation/ConversationChatContainer copy.vue
  53. 654 244
      src/views/conversation/ConversationChatContainer.vue
  54. 65 0
      src/views/conversation/ConversationChatWordViewer.vue
  55. 5 3
      vite.config.mjs

ファイルの差分が大きいため隠しています
+ 247 - 256
package-lock.json


+ 5 - 3
package.json

@@ -33,9 +33,11 @@
     "vitepress": "1.5.0"
   },
   "dependencies": {
-    "@antv/g6": "^5.0.43",
+    "@antv/g6": "5.0.43",
     "@arco-design/web-vue": "2.56.3",
     "@microsoft/fetch-event-source": "2.0.1",
+    "@wangeditor/editor": "5.1.23",
+    "@wangeditor/editor-for-vue": "5.1.12",
     "axios": "1.7.8",
     "clipboard": "2.0.11",
     "dayjs": "1.11.13",
@@ -53,10 +55,10 @@
     "mitt": "3.0.1",
     "pinia": "2.2.8",
     "pinia-plugin-persist": "1.0.0",
-    "screenfull": "6.0.2",
     "vue": "3.5.13",
     "vue-i18n": "10.0.5",
     "vue-router": "4.5.0",
-    "wujie-vue3": "1.0.23"
+    "wujie-vue3": "1.0.23",
+    "screenfull": "6.0.2"
   }
 }

+ 4 - 3
public/config.js

@@ -26,7 +26,6 @@ window.RGFLOW_BASE_URL = 'http://smartai.com:8294';
  * dify 接口地址
  * */
 window.DIFY_BASE_URL = 'http://smartai.com:8295';
-
 /**
  * 知识问答 引用效果 引用的静态资源地址(docx,pdf等文件)
  * PDF文档和图片链接需要使用此地址
@@ -62,8 +61,10 @@ window.XIAOSHU_URL = 'http://smartai.com:8401';
  * @see https://note.youdao.com/ynoteshare/index.html?id=d781d48a6d64bab8451026fe65342a7f&type=note&_time=1735105294960
  * 根据需求,将默认agent配置为 智能问答
  */
-window.DEFAULT_CHAT_AGENT_NAME = '智能问答';
-window.DEFAULT_CHAT_AGENT_ID = '90d533729c0211efbf6b0242ac160006';
+// window.DEFAULT_CHAT_AGENT_NAME = '智能问答';
+// window.DEFAULT_CHAT_AGENT_ID = '90d533729c0211efbf6b0242ac160006';
+window.DEFAULT_CHAT_AGENT_NAME = '新会话';
+window.DEFAULT_CHAT_AGENT_ID = '';
 
 /**
  * 临时配置项:语音项目地址

+ 17 - 3
public/i18n/lang-en.json

@@ -426,7 +426,7 @@
     "input_message": "Enter your question",
     "input_placholder_v0": "Enter text content",
     "input_placholder_v1": "Enter “/” to use Smart intelligent dialogue to solve your problem",
-    "input_placholder": "",
+    "input_placholder": "Utilize intelligent dialogue to solve your problems.",
     "input_confirm": "Confirm",
     "back": "Back",
     "source": "Source",
@@ -446,8 +446,20 @@
     "online_search": "Online search",
     "tag_all": "All",
     "copy": "Copy",
+    "retry": "Retry",
+    "edit": "Edit",
+    "download": "Download",
     "please_input_text": "Please enter text",
-    "please_upload_file": "Please upload the file"
+    "please_upload_file": "Please upload the file",
+    "deep_think": "Deep Think",
+    "internet_search": "Internet Search",
+    "knowledge_base": "Knowledge Base",
+    "web_page_title": "Web Page Search",
+    "zoom_in": "Zoom In",
+    "zoom_out": "Zoom Out",
+    "full_screen": "Full Screen",
+    "fit": "Fit",
+    "welcome": "Nice to meet you, how can I help?"
   },
   "ai_search": {
     "placeholder": "Please enter search content",
@@ -504,6 +516,8 @@
     "platform_introduce": "Platform Introduction",
     "platform_name_placeholder": "Please enter the platform name",
     "platform_logo_placeholder": "Please upload a logo",
-    "platform_introduce_placeholder": "Please enter the platform introduction"
+    "platform_introduce_placeholder": "Please enter the platform introduction",
+    "update_success": "Password change successful",
+    "update_fail": "Password change failed"
   }
 }

+ 18 - 3
public/i18n/lang-zh.json

@@ -425,7 +425,7 @@
     "input_message": "输入您的问题",
     "input_placholder_v0": "输入文本内容",
     "input_placholder_v1": "输入“/”使用Smart智能对话,解决你的问题",
-    "input_placholder": "",
+    "input_placholder": "使用智能对话,解决你的问题",
     "input_confirm": "发送",
     "back": "返回",
     "source": "来源",
@@ -445,8 +445,21 @@
     "online_search": "联网搜索",
     "tag_all": "全部",
     "copy": "复制",
+    "retry": "重试",
+    "edit": "修改",
+    "download": "下载",
     "please_input_text": "请输入文字",
-    "please_upload_file": "请上传文件"
+    "please_upload_file": "请上传文件",
+
+    "deep_think": "深度思考",
+    "internet_search": "联网搜索",
+    "knowledge_base": "知识库",
+    "web_page_title": "网页搜索",
+    "zoom_in": "放大",
+    "zoom_out": "缩小",
+    "full_screen": "全屏",
+    "fit": "自适应",
+    "welcome": "很高兴见到你,有什么可以帮忙的?"
   },
   "ai_search": {
     "placeholder": "请输入搜索内容",
@@ -503,6 +516,8 @@
     "platform_introduce": "平台介绍",
     "platform_name_placeholder": "请输入平台名称",
     "platform_logo_placeholder": "请上传logo",
-    "platform_introduce_placeholder": "请输入平台介绍"
+    "platform_introduce_placeholder": "请输入平台介绍",
+    "update_success": "修改密码成功",
+    "update_fail": "修改密码失败"
   }
 }

+ 4 - 4
src/3rd-libs/arco-vue-install-icon.js

@@ -357,7 +357,7 @@ export default function installArcoVue(app) {
   app.use(IconInfoCircle);
   // app.use(IconInfo);
   // app.use(IconMinusCircle);
-  // app.use(IconMinus);
+  app.use(IconMinus);
   // app.use(IconPlusCircle);
   app.use(IconPlus);
   // app.use(IconQuestionCircle);
@@ -391,7 +391,7 @@ export default function installArcoVue(app) {
   app.use(IconRefresh);
   // app.use(IconReply);
   // app.use(IconSave);
-  // app.use(IconScan);
+  app.use(IconScan);
   app.use(IconSearch);
   // app.use(IconSelectAll);
   // app.use(IconSend);
@@ -457,8 +457,8 @@ export default function installArcoVue(app) {
   // app.use(IconSoundFill);
   // app.use(IconBackward);
   // app.use(IconForward);
-  // app.use(IconFullscreenExit);
-  // app.use(IconFullscreen);
+  app.use(IconFullscreenExit);
+  app.use(IconFullscreen);
   // app.use(IconLiveBroadcast);
   // app.use(IconMusic);
   // app.use(IconMute);

+ 10 - 0
src/apis/intelligent.js

@@ -164,3 +164,13 @@ export function syncDialogInfo(dialogId) {
     method: 'get'
   });
 }
+/**
+ * 用我们token调用dify接口用来判断 dify是否能正常登录进去,用来拦截dify页面显示
+ * 在进入dify详情页时调用
+ * */
+export function getDifyProfile() {
+  return request({
+    url: `/dify/console/api/account/profile`,
+    method: 'get'
+  });
+}

BIN
src/assets/chat/edit-1.png


BIN
src/assets/chat/file-1.png


BIN
src/assets/chat/file-2.png


BIN
src/assets/chat/file-3.png


BIN
src/assets/chat/internet-active.png


BIN
src/assets/chat/internet.png


BIN
src/assets/chat/stop-1.png


+ 3 - 0
src/base/theme/theme-dark.css

@@ -1,3 +1,6 @@
 html body[dmx-theme='dark'] {
   --color-link-1: #165dff;
+  --color-text-2: #707379;
+  --color-bolder-11: #007aff;
+  --color-icon-1: #668ff3;
 }

+ 2 - 0
src/base/theme/theme-light.css

@@ -12,4 +12,6 @@ html body[dmx-theme='light'] {
   --color-fill-4: #f0f2f5; */
 
   --color-link-1: #165dff;
+  --color-bolder-11: #007aff;
+  --color-icon-1: #668ff3;
 }

+ 2 - 0
src/base/theme/theme-party.css

@@ -7,6 +7,8 @@ body {
 */
 html body[dmx-theme='party'] {
   --bg-img: url('../../assets/party-bg.jpeg');
+  --color-bolder-11: #007aff;
+  --color-icon-1: #668ff3;
 }
 
 /*

+ 156 - 0
src/components/11.json

@@ -0,0 +1,156 @@
+{
+  "event": "node_finished",
+  "data": {
+    "id": "3340e419-3f5e-44bb-8a4a-fa5d57b5b311",
+    "node_id": "1740982017325",
+    "node_type": "tool",
+    "title": "SearXNG 搜索",
+    "index": 2,
+    "predecessor_node_id": "1740974279269",
+    "inputs": { "query": "大模型" },
+    "process_data": "",
+    "outputs": {
+      "text": "\n\n\n\n\n\n\n\n\n",
+      "files": [],
+      "json": [
+        {
+          "url": "https://www.zhihu.com/question/644878661",
+          "title": "现在市面上的大模型这么多,有没有大佬可以总结分类下 ...",
+          "content": "大模型的评测基准是什么。应用篇1 :面向开发者 : 梳理当前基于大模型,有哪些应用开发生态,他们之间的区别是什么 应用篇2 :面向高级用户 or 企业客户 :当前有哪些 2C 的大模型应用,商业模式 应用篇3 :面向 2C 客户,当前有哪些案例。 本文为第",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/644878661", "", "", ""],
+          "engines": ["bing"],
+          "positions": [1],
+          "score": 1.0,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/652126515",
+          "title": "目前针对大模型剪枝的方法有哪些? - 知乎",
+          "content": "本文简单讲述了目前大模型剪枝的一些工作,在大模型参数量越来越大的今天,该方向的研究对于减少模型的大小以及复杂性非常有用。码字不易,如果觉得有帮助,欢迎点赞收藏加关注。参考文档: A Survey on Model Compression for Large Language Models",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/652126515", "", "", ""],
+          "engines": ["bing"],
+          "positions": [2],
+          "score": 0.5,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/8271237311",
+          "title": "有从业者可以说一下最近国内的大模型deepseek吗?具体 ...",
+          "content": "2025年1月29日 · 大模型deepseek简直无敌了,这次的创新可以说是进入模型时代,中国对业界最大的贡献了,而且不想百度,阿里,腾讯那些资源丰富的大厂,可敬可佩!deepseek底是什么呢?DeepSeek 中文名叫深度求索,是国内杭州的一家初创大模型公司。",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/8271237311", "", "", ""],
+          "engines": ["bing"],
+          "positions": [3],
+          "score": 0.3333333333333333,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/606514058/answers/updated",
+          "title": "为什么大模型输入输出往往只有2K, 4K token? - 知乎",
+          "content": "2024年3月2日 · NLP和大模型中的Token(文本单元) 在NLP和大模型的背景下,\"token\"则指的是文本数据的基本处理单元。Token化是将原始文本分割成一系列tokens的过程,这些tokens可以是单词、字符、子词或其他语言结构。Token的作用 Token在大模型中的作用主要体现在",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/606514058/answers/updated", "", "", ""],
+          "engines": ["bing"],
+          "positions": [4],
+          "score": 0.25,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/9501918714",
+          "title": "国产目前最好用写代码的大模型是哪个? - 知乎",
+          "content": "2025年1月13日 · 哈哈哈,最后我决定让大模型来回答一下这个问题。通义的回答 截至2024年底,国产大模型中在代码生成能力方面表现最好的包括阿里云的通义千问系列中的Qwen2.5-Coder版和DeepSeek-Coder。",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/9501918714", "", "", ""],
+          "engines": ["bing"],
+          "positions": [5],
+          "score": 0.2,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/664986080?write",
+          "title": "以下这7款AI视频生成大模型各自有什么优缺点? - 知乎",
+          "content": "可灵大模型基于快手在视频技术方面的多年积累,采用与Sora相似的技术路线,并结合了多项自研技术创新。 可灵AI现在提供了网页版,集成了文生图、文生视频以及即将支持的视频编辑能力,成为一站式视觉内容创作平台。",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/664986080", "", "write", ""],
+          "engines": ["bing"],
+          "positions": [6],
+          "score": 0.16666666666666666,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/666943660?write",
+          "title": "大模型推理框架,SGLang和vLLM有哪些区别? - 知乎",
+          "content": "2025年1月15日 · 想学习大模型架构、训练、推理的相关知识,有什么好的学习路线推荐,和哪些必看论文? 4 个回答 vLLM等大模型推理框架可以防止大模型请求过多导致爆显存吗?",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/666943660", "", "write", ""],
+          "engines": ["bing"],
+          "positions": [7],
+          "score": 0.14285714285714285,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/653945925",
+          "title": "System prompt 在大模型内部是如何生效的? - 知乎",
+          "content": "2024年4月23日 · System message 在大模型内部是每次加在了用户输入的前面。在 OpenAI 的大模型设计的时候,有三种不同的 message 类型,分别是System Message、Assistant Message 以及 User Message,这三者是有明显区别的。",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/653945925", "", "", ""],
+          "engines": ["bing"],
+          "positions": [8],
+          "score": 0.125,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/8211811008/answers/updated",
+          "title": "开源大模型本地部署和官网的有什么区别? - 知乎",
+          "content": "2025年1月4日 · 这是一个比较有意思的话题,开源大模型本地部署与官方提供的模型具体有哪些区别。可以从几个方面来聊聊。1. 模型本身与硬件资源 以 DeepSeek 为例,我们知道 DeepSeek-R1 官方部署模型参数是 671B,这么大的参数本地部署(个人计算机)基本没可能。",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/8211811008/answers/updated", "", "", ""],
+          "engines": ["bing"],
+          "positions": [9],
+          "score": 0.1111111111111111,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/1022524886/answers/updated",
+          "title": "国产AI大模型最适合写公文的有哪些? - 知乎",
+          "content": "主流国产大模型公文能力大PK: 经过为期两个月的实际测试,我发现以下几款模型在公文写作方面各具特色: 1、 文心一言:最懂\"官方范儿\"的AI 如果说写公文是一门武术,那文心一言就是其中的\"太极大师\"。它最懂得公文写作中的精微之处:",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/1022524886/answers/updated", "", "", ""],
+          "engines": ["bing"],
+          "positions": [10],
+          "score": 0.1,
+          "category": "general"
+        }
+      ]
+    },
+    "status": "succeeded",
+    "error": null,
+    "elapsed_time": 3.19454,
+    "execution_metadata": { "tool_info": { "provider_type": "builtin", "provider_id": "searxng" } },
+    "created_at": 1741249172,
+    "finished_at": 1741249176,
+    "files": [],
+    "parallel_id": null,
+    "parallel_start_node_id": null,
+    "parent_parallel_id": null,
+    "parent_parallel_start_node_id": null,
+    "iteration_id": null
+  },
+  "error": "",
+  "status": 200,
+  "task_id": "ec3253d2-cda5-43e9-9e9f-25385e951d21",
+  "message_id": "",
+  "session_id": "e9d0bb9770f744c6bbd48e2620e08db1"
+}

+ 214 - 0
src/components/22.json

@@ -0,0 +1,214 @@
+{
+  "event": "stream",
+  "data": { "answer": "</think>", "id": "9e4995d9-01de-49dd-80fc-1c0ce37381e7" },
+  "error": "",
+  "status": 200,
+  "task_id": "b6143773-910c-4b56-a1a3-0ef1d3e248f1",
+  "message_id": "9e4995d9-01de-49dd-80fc-1c0ce37381e7",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+},
+// 深度思考开始
+{
+  "event": "node_started",
+  "data": {
+    "id": "963e2f4c-697a-4d34-839e-1ab24b252f92",
+    "node_id": "1741163010596",
+    "node_type": "llm",
+    "title": "LLM 2",
+    "index": 4,
+    "predecessor_node_id": "1741146749557",
+    "inputs": null,
+    "created_at": 1741260927,
+    "extras": {},
+    "parallel_id": null,
+    "parallel_start_node_id": null,
+    "parent_parallel_id": null,
+    "parent_parallel_start_node_id": null,
+    "iteration_id": null,
+    "parallel_run_id": null,
+    "outputs": {},
+    "files": [],
+    "process_data": ""
+  },
+  "error": "",
+  "status": 200,
+  "task_id": "b6143773-910c-4b56-a1a3-0ef1d3e248f1",
+  "message_id": "",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+},
+// 深度思考消息
+{
+  "event": "stream",
+  "data": { "answer": "嗯", "id": "9e4995d9-01de-49dd-80fc-1c0ce37381e7" },
+  "error": "",
+  "status": 200,
+  "task_id": "b6143773-910c-4b56-a1a3-0ef1d3e248f1",
+  "message_id": "9e4995d9-01de-49dd-80fc-1c0ce37381e7",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+}
+// 深度思考结束消息
+{
+  "event": "stream",
+  "data": { "answer": "</think>", "id": "039afd04-35c1-455e-9e43-e69e5d1f6417" },
+  "error": "",
+  "status": 200,
+  "task_id": "e38e200f-14bd-46da-85f7-e48ea3622052",
+  "message_id": "039afd04-35c1-455e-9e43-e69e5d1f6417",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+}
+// 深度思考后的消息
+{
+  "event": "stream",
+  "data": { "answer": "\n\n", "id": "039afd04-35c1-455e-9e43-e69e5d1f6417" },
+  "error": "",
+  "status": 200,
+  "task_id": "e38e200f-14bd-46da-85f7-e48ea3622052",
+  "message_id": "039afd04-35c1-455e-9e43-e69e5d1f6417",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+}
+
+
+{
+  "event": "node_finished",
+  "data": {
+    "id": "625b5e8b-a85d-4a9c-ab19-bf8d49c5bd66",
+    "node_id": "1741163010596",
+    "node_type": "llm",
+    "title": "LLM 2",
+    "index": 4,
+    "predecessor_node_id": "1741146749557",
+    "inputs": null,
+    "process_data": "",
+    "outputs": {
+      "text": "好,我现在需要用中文简体来回答关于“赵勤刚是谁”这个问题。首先,我查看用户是否上传了相关文档,但目前没有文档上传。因此,我需要依靠我的通用知识来回答。\n\n赵勤刚,这个名字听起来可能是一个普通人的名字。在没有更多背景信息的情况下,我无法提供具体的信息。通常情况下,如果赵勤刚不是公众人物或者没有广泛的媒体报道,他的信息可能不会被广泛记录。\n\n如果用户能提供更多的上下文,比如赵勤刚的职业、所在地区或者其他相关细节,我可能能够提供更准确的信息。但在现有情况下,只能说赵勤刚可能是一个普通人,具体信息需要进一步的背景资料。\n\n最后,我会注明这个回答是基于通用知识库提供的,并且在没有文档的情况下,我无法提供更详细的信息。希望这能满足用户的需求。\n</think>\n\n赵勤刚是谁?\n\n基于通用知识,赵勤刚的具体身份和背景信息无法确定。如果您能提供更多的上下文或详细信息,可能会有助于更准确地回答这个问题。在没有文档上传的情况下,无法提供更具体的信息。",
+      "usage": {
+        "prompt_tokens": 514,
+        "prompt_unit_price": "0",
+        "prompt_price_unit": "0",
+        "prompt_price": "0E-7",
+        "completion_tokens": 877,
+        "completion_unit_price": "0",
+        "completion_price_unit": "0",
+        "completion_price": "0E-7",
+        "total_tokens": 1391,
+        "total_price": "0E-7",
+        "currency": "USD",
+        "latency": 19.331347599625587
+      },
+      "finish_reason": "stop"
+    },
+    "status": "succeeded",
+    "error": null,
+    "elapsed_time": 19.47171,
+    "execution_metadata": { "total_tokens": 1391, "total_price": "0.0000000", "currency": "USD" },
+    "created_at": 1741261240,
+    "finished_at": 1741261259,
+    "files": [],
+    "parallel_id": null,
+    "parallel_start_node_id": null,
+    "parent_parallel_id": null,
+    "parent_parallel_start_node_id": null,
+    "iteration_id": null
+  },
+  "error": "",
+  "status": 200,
+  "task_id": "e38e200f-14bd-46da-85f7-e48ea3622052",
+  "message_id": "039afd04-35c1-455e-9e43-e69e5d1f6417",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+}
+{
+  "event": "node_started",
+  "data": {
+    "id": "ca1521e4-1270-4024-9007-b1427e79b726",
+    "node_id": "1741163052603",
+    "node_type": "answer",
+    "title": "深度回答",
+    "index": 5,
+    "predecessor_node_id": "1741163010596",
+    "inputs": null,
+    "created_at": 1741261259,
+    "extras": {},
+    "parallel_id": null,
+    "parallel_start_node_id": null,
+    "parent_parallel_id": null,
+    "parent_parallel_start_node_id": null,
+    "iteration_id": null,
+    "parallel_run_id": null,
+    "outputs": {},
+    "files": [],
+    "process_data": ""
+  },
+  "error": "",
+  "status": 200,
+  "task_id": "e38e200f-14bd-46da-85f7-e48ea3622052",
+  "message_id": "039afd04-35c1-455e-9e43-e69e5d1f6417",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+}
+{
+  "event": "node_finished",
+  "data": {
+    "id": "ca1521e4-1270-4024-9007-b1427e79b726",
+    "node_id": "1741163052603",
+    "node_type": "answer",
+    "title": "深度回答",
+    "index": 5,
+    "predecessor_node_id": "1741163010596",
+    "inputs": null,
+    "process_data": "",
+    "outputs": {
+      "answer": "好,我现在需要用中文简体来回答关于“赵勤刚是谁”这个问题。首先,我查看用户是否上传了相关文档,但目前没有文档上传。因此,我需要依靠我的通用知识来回答。\n\n赵勤刚,这个名字听起来可能是一个普通人的名字。在没有更多背景信息的情况下,我无法提供具体的信息。通常情况下,如果赵勤刚不是公众人物或者没有广泛的媒体报道,他的信息可能不会被广泛记录。\n\n如果用户能提供更多的上下文,比如赵勤刚的职业、所在地区或者其他相关细节,我可能能够提供更准确的信息。但在现有情况下,只能说赵勤刚可能是一个普通人,具体信息需要进一步的背景资料。\n\n最后,我会注明这个回答是基于通用知识库提供的,并且在没有文档的情况下,我无法提供更详细的信息。希望这能满足用户的需求。\n</think>\n\n赵勤刚是谁?\n\n基于通用知识,赵勤刚的具体身份和背景信息无法确定。如果您能提供更多的上下文或详细信息,可能会有助于更准确地回答这个问题。在没有文档上传的情况下,无法提供更具体的信息。",
+      "files": []
+    },
+    "status": "succeeded",
+    "error": null,
+    "elapsed_time": 0.054719,
+    "execution_metadata": null,
+    "created_at": 1741261259,
+    "finished_at": 1741261259,
+    "files": [],
+    "parallel_id": null,
+    "parallel_start_node_id": null,
+    "parent_parallel_id": null,
+    "parent_parallel_start_node_id": null,
+    "iteration_id": null
+  },
+  "error": "",
+  "status": 200,
+  "task_id": "e38e200f-14bd-46da-85f7-e48ea3622052",
+  "message_id": "039afd04-35c1-455e-9e43-e69e5d1f6417",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+}
+{
+  "event": "workflow_finished",
+  "data": {
+    "id": "10ad7f0a-bdad-4848-9940-40395ef4ca12",
+    "workflow_id": "eb080f9f-e4b6-46a3-9ce8-7c2858102655",
+    "sequence_number": 192,
+    "status": "succeeded",
+    "outputs": {
+      "answer": "好,我现在需要用中文简体来回答关于“赵勤刚是谁”这个问题。首先,我查看用户是否上传了相关文档,但目前没有文档上传。因此,我需要依靠我的通用知识来回答。\n\n赵勤刚,这个名字听起来可能是一个普通人的名字。在没有更多背景信息的情况下,我无法提供具体的信息。通常情况下,如果赵勤刚不是公众人物或者没有广泛的媒体报道,他的信息可能不会被广泛记录。\n\n如果用户能提供更多的上下文,比如赵勤刚的职业、所在地区或者其他相关细节,我可能能够提供更准确的信息。但在现有情况下,只能说赵勤刚可能是一个普通人,具体信息需要进一步的背景资料。\n\n最后,我会注明这个回答是基于通用知识库提供的,并且在没有文档的情况下,我无法提供更详细的信息。希望这能满足用户的需求。\n</think>\n\n赵勤刚是谁?\n\n基于通用知识,赵勤刚的具体身份和背景信息无法确定。如果您能提供更多的上下文或详细信息,可能会有助于更准确地回答这个问题。在没有文档上传的情况下,无法提供更具体的信息。"
+    },
+    "error": null,
+    "elapsed_time": 19.74018283840269,
+    "total_tokens": 1391,
+    "total_steps": 5,
+    "created_by": { "id": "4476de14-df0f-453e-a6c2-bc70f1405105", "user": "104" },
+    "created_at": 1741261240,
+    "finished_at": 1741261259,
+    "files": []
+  },
+  "error": "",
+  "status": 200,
+  "task_id": "e38e200f-14bd-46da-85f7-e48ea3622052",
+  "message_id": "039afd04-35c1-455e-9e43-e69e5d1f6417",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+},
+{
+  "event": "message_end",
+  "data": {},
+  "error": "",
+  "status": 200,
+  "task_id": "e38e200f-14bd-46da-85f7-e48ea3622052",
+  "message_id": "039afd04-35c1-455e-9e43-e69e5d1f6417",
+  "session_id": "3b189497ffce4a2fa0a3f89bee50b4d5"
+}

+ 156 - 0
src/components/33.json

@@ -0,0 +1,156 @@
+{
+  "event": "node_finished",
+  "data": {
+    "id": "b4c50cc4-564f-47fc-ab97-e6f29e0b1928",
+    "node_id": "1740982017325",
+    "node_type": "tool",
+    "title": "SearXNG 搜索",
+    "index": 2,
+    "predecessor_node_id": "1740974279269",
+    "inputs": { "query": "大模型是什么" },
+    "process_data": "",
+    "outputs": {
+      "text": "\n\n\n\n\n\n\n\n\n",
+      "files": [],
+      "json": [
+        {
+          "url": "https://www.zhihu.com/question/498275802",
+          "title": "什么是大模型?超大模型和 Foundation Model 呢? - 知乎",
+          "content": "回到大模型,2017年Transformer结构的提出,使得深度学习模型参数突破了1亿。下面这个图呢,就是从一开始的lenet、Alexnet、ResNet开始,模型参数一个比一个大,到了BERT网络模 …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/498275802", "", "", ""],
+          "engines": ["bing"],
+          "positions": [1],
+          "score": 1.0,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/639709186",
+          "title": "人工智能中大模型和小模型的实际应用区别是什么? - 知乎",
+          "content": "2024年1月17日 · 应用场景:大模型适用于大规模的商业应用,如搜索引擎、广告推荐系统等,而小模型适用于特定的场景,如嵌入式系统、物联网设备等。 隐私和安全:由于大模型需要处理 …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/639709186", "", "", ""],
+          "engines": ["bing"],
+          "positions": [2],
+          "score": 0.5,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/606514058/answers/updated",
+          "title": "为什么大模型输入输出往往只有2K, 4K token? - 知乎",
+          "content": "2024年3月2日 · 什么是 token?它是怎么计算的?一个 token 是指一个字吗?中文和英文的 token 是一样的吗?这篇文章我们就来聊聊这些问题,大模型中的\"token\"是指文本的最小处理单位, …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/606514058/answers/updated", "", "", ""],
+          "engines": ["bing"],
+          "positions": [3],
+          "score": 0.3333333333333333,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/633365088",
+          "title": "llama.cpp里面的Q8_0,Q6_K_M,Q4_K_M量化原理是什么?",
+          "content": "量化:量化是一种通过降低模型参数的表示精度来减少模型大小和计算需求的方法,比如,把单精度FP32转变为INT8来减少存储和计算成本。 量化有非常多的计算方法,比如常见的线性(仿 …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/633365088", "", "", ""],
+          "engines": ["bing"],
+          "positions": [4],
+          "score": 0.25,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/12430119658",
+          "title": "请问什么是DeepSeek的模型蒸馏? - 知乎",
+          "content": "那么,什么是模型蒸馏呢?模型蒸馏,是一种将大模型所掌握的知识、技能和推理能力,以一种\"浓缩\"的方式传授给小模型的技术。在这个过程中,大模型扮演着\"教师\"的角色,而小模型则相当 …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/12430119658", "", "", ""],
+          "engines": ["bing"],
+          "positions": [5],
+          "score": 0.2,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/tardis/bd/ans/3596055130",
+          "title": "什么是 AI 智能体,和大模型有什么关系?",
+          "content": "2024年8月16日 · 二、什么是大模型?1. 大模型的定义 大模型,通常指的是由海量数据训练而成的深度学习模型,拥有巨大的参数规模和复杂的架构。这类模型以其强大的表达能力和泛化能 …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/tardis/bd/ans/3596055130", "", "", ""],
+          "engines": ["bing"],
+          "positions": [6],
+          "score": 0.16666666666666666,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/608995573",
+          "title": "什么是AIGC?什么是大模型?具体有什么应用? - 知乎",
+          "content": "什么是大模型?题主问题中提到的大模型,英文名称为Large Mdel,大模型也被称为基础模型(Foundation Model),是一种具有大规模参数的机器学习模型,参数量级可以达到数亿或成百 …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/608995573", "", "", ""],
+          "engines": ["bing"],
+          "positions": [7],
+          "score": 0.14285714285714285,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/644878661",
+          "title": "现在市面上的大模型这么多,有没有大佬可以总结分类下 ...",
+          "content": "大模型的评测基准是什么。应用篇1 :面向开发者 : 梳理当前基于大模型,有哪些应用开发生态,他们之间的区别是什么 应用篇2 :面向高级用户 or 企业客户 :当前有哪些 2C 的大模型应 …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/644878661", "", "", ""],
+          "engines": ["bing"],
+          "positions": [8],
+          "score": 0.125,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/639709186/answers/updated",
+          "title": "人工智能中大模型和小模型的实际应用区别是什么? - 知乎",
+          "content": "传统AI和大模型的AI什么区别昵?1、通用大模型其能力足够强,我们甚至不需要进行额外的工作,可以直接应用大模型。2、在应用场景中,企业开发工作将逐渐变得轻松。我们所做的更多 …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/639709186/answers/updated", "", "", ""],
+          "engines": ["bing"],
+          "positions": [9],
+          "score": 0.1111111111111111,
+          "category": "general"
+        },
+        {
+          "url": "https://www.zhihu.com/question/10744161372",
+          "title": "为什么同为开源追平 OpenAI,Qwen 没有像 DeepSeek 一样 ...",
+          "content": "2025年1月28日 · 其独创的GRPO降低ppo训练成本的同时保证算法可靠性,Qwen是效法者。Qwen靠什么呢, 现状是,大部分算法都在折腾数据,post train基本都做sft,RL玩的都很浅( …",
+          "engine": "bing",
+          "template": "default.html",
+          "parsed_url": ["https", "www.zhihu.com", "/question/10744161372", "", "", ""],
+          "engines": ["bing"],
+          "positions": [10],
+          "score": 0.1,
+          "category": "general"
+        }
+      ]
+    },
+    "status": "succeeded",
+    "error": null,
+    "elapsed_time": 3.080723,
+    "execution_metadata": { "tool_info": { "provider_type": "builtin", "provider_id": "searxng" } },
+    "created_at": 1741261673,
+    "finished_at": 1741261676,
+    "files": [],
+    "parallel_id": null,
+    "parallel_start_node_id": null,
+    "parent_parallel_id": null,
+    "parent_parallel_start_node_id": null,
+    "iteration_id": null
+  },
+  "error": "",
+  "status": 200,
+  "task_id": "6b421f61-a97c-4b2d-99d5-85a933404119",
+  "message_id": "",
+  "session_id": "591a6956895d4a06aa879ae2a16123dd"
+}

+ 102 - 0
src/components/Editor.vue

@@ -0,0 +1,102 @@
+<template>
+  <!-- <div style="padding: 20%"> -->
+  <!-- <Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" /> -->
+  <Editor
+    v-model="valueHtml"
+    :defaultConfig="editorConfig"
+    :mode="mode"
+    @onCreated="handleCreated"
+    :style="editor_style"
+  />
+  <!-- </div> -->
+</template>
+<script setup>
+import '@wangeditor/editor/dist/css/style.css'; // 引入 css
+
+import { onBeforeUnmount, ref, shallowRef, onMounted, watch } from 'vue';
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue';
+const props = defineProps({
+  content: {
+    type: String,
+    default: '欢迎'
+  },
+  editor_style: {
+    type: Object,
+    default: () => {
+      return {
+        width: '100%',
+        height: '100%',
+        // overflow: 'hidden'
+        padding: '10px 20px'
+      };
+    }
+  }
+});
+
+// 编辑器实例,必须用 shallowRef
+const editorRef = shallowRef();
+
+// 内容 HTML
+const valueHtml = ref(props.content);
+
+watch(
+  () => props.content,
+  (val) => {
+    valueHtml.value = props.content;
+  },
+  {
+    immediate: true
+  }
+);
+// 自定义按钮
+const customButton = {
+  key: 'custom-btn',
+  title: '自定义按钮',
+  iconSvg: '<svg ...></svg>', // 这里可以放置你的按钮 SVG 图标
+  command: () => {
+    const editor = editorRef.value;
+    if (!editor) return;
+
+    // 获取选中的文本
+    const selectionText = editor.getSelectionText();
+    alert(`你选中的文本是: ${selectionText}`);
+  }
+};
+
+// 模拟 ajax 异步获取内容
+onMounted(() => {
+  setTimeout(() => {
+    // valueHtml.value = '<p>模拟 Ajax 异步设置内容</p>';
+  }, 1500);
+});
+
+const toolbarConfig = {
+  toolbarKeys: [
+    // 添加其他工具按钮的 key 这里
+    'bold',
+    'italic',
+    'underline',
+    'custom-btn' // 将自定义按钮添加到工具栏中
+  ],
+  customButtons: [customButton] // 注册自定义按钮
+};
+const editorConfig = { placeholder: '请输入内容...', toolbarConfig };
+
+// 组件销毁时,也及时销毁编辑器
+onBeforeUnmount(() => {
+  const editor = editorRef.value;
+  if (editor == null) return;
+  editor.destroy();
+});
+
+const handleCreated = (editor) => {
+  editorRef.value = editor; // 记录 editor 实例,重要!
+};
+</script>
+<style>
+.editor {
+  border: 1px solid #ccc;
+  padding: 20px;
+  /* text-align: left; */
+}
+</style>

+ 194 - 71
src/components/MindMap.vue

@@ -21,7 +21,8 @@ const props = defineProps({
    * Markmap 文档内容,此内容用于生成脑图节点数据
    */
   content: {
-    type: String
+    type: String,
+    default: ''
   },
   /**
    * 外层div节点的样式
@@ -45,14 +46,14 @@ const props = defineProps({
       width: '100%',
       height: '100%'
     }
-  },
+  }
   /**
    * 当点击扩展时的加载进度
    */
-  progress: {
-    type: Number,
-    default: 0
-  }
+  // progress: {
+  //   type: Number,
+  //   default: 0
+  // }
 });
 
 const emits = defineEmits(['extend']);
@@ -62,6 +63,8 @@ const emits = defineEmits(['extend']);
  * 文档中文:https://www.min2k.com/intro/online-markmap.html
  * 在线编辑器:https://www.min2k.com/tools/markmap/
  */
+// 缩放比例
+let scale = 1;
 
 const svgRef = ref();
 
@@ -72,7 +75,6 @@ let is_pause_observe = false; // 是否暂停DOM检测
 let plus_icon_list = [];
 let plus_icon_index = 1;
 let timer_progress = null;
-const targetEle = ref(null);
 /**
  * 遍历脑图树种的每一个节点
  * @param nd - 脑图树的节点
@@ -100,12 +102,13 @@ function loopTreeNode(nd, path) {
 }
 
 const update = async (data) => {
+  console.log(data, 111);
   const result = transformer.transform(data);
   const root = result.root;
 
   // 遍历数据
   loopTreeNode(root, [0]);
-
+  console.log(mark_map, 888);
   await mark_map.setData(root);
   mark_map.fit();
 
@@ -140,7 +143,7 @@ function observeMindMapChanged(root_value) {
 
     let remove_count = 0; // 移除的统计
     let add_count = 0; // 添加的统计
-
+    console.log(111111111);
     mutationsList.forEach((mutation) => {
       if (mutation.type === 'childList') {
         if (mutation.addedNodes.length > 0) {
@@ -171,9 +174,9 @@ function observeMindMapChanged(root_value) {
 
       // setTimeout(() => {
       //   // 解锁
-      //   is_pause_observe = false;
+      is_pause_observe = false;
       // }, 500);
-    });
+    }, 500);
 
     if (remove_count !== 0) {
       // 移除
@@ -201,16 +204,14 @@ function attachPlusIcon(root_value) {
     str = str.replace(/,/g, '].children[');
     str = 'root_value[' + str + ']';
     str = str.replace('root_value[0].', 'root_value.');
-
     let cur_node = null;
     eval(`cur_node=${str}`);
-
     const result = mark_map.findElement(cur_node);
     if (!result) {
-      // console.log('找不到节点', cur_node, root_value, str);
+      console.log('找不到节点', cur_node, root_value, str);
       return;
     } else {
-      // console.log('找到节点');
+      console.log('找到节点');
     }
 
     const node_group = result.g; // svg g 元素
@@ -286,9 +287,8 @@ function generatePlusIconDiv(node_data, root_data) {
           full: root_data, // 全部node的数据
           ele: item // 当前被点击的元素
         });
-        targetEle.value = item;
 
-        // updateIconProgress(item)
+        updateIconProgress(item);
       } else {
         // 其他的
         item.setAttribute('clickable', 0);
@@ -312,69 +312,192 @@ function genProgressSpan(count) {
   return `<span style="font-size:10px;display: inline-block;text-align:center;">${count}%</span>`;
 }
 
-// /**
-//  * 更新进度
-//  * @param item
-//  */
-// function updateIconProgress(item) {
-//   if (timer_progress != null) {
-//     console.log('正在进行中,不用重复点击');
-//     return;
-//   }
-
-//   // 设置子元素为动态的加载进度
-//   let count = 0;
-//   item.innerHTML = genProgressSpan(count);
-
-//   timer_progress = setInterval(() => {
-//     is_pause_observe = true;
-//     count = count + 1;
-
-//     if (count > 100) {
-//       // 进度圆满
-//       clearInterval(timer_progress);
-
-//       is_pause_observe = false;
-//       end_node_list = [];
-//       // mark_map;
-//       is_pause_observe = false; // 是否暂停DOM检测
-//       plus_icon_list = [];
-//       plus_icon_index = 1;
-//       timer_progress = null;
-
-//       mark_map.destroy();
-//       mark_map = null;
-//       initMarkMap(props.content);
-//       return;
-//     }
-
-//     item.innerHTML = genProgressSpan(count);
-//   }, 50);
-// }
+/**
+ * 更新进度
+ * @param item
+ */
+function updateIconProgress(item) {
+  if (timer_progress != null) {
+    console.log('正在进行中,不用重复点击');
+    return;
+  }
+
+  // 设置子元素为动态的加载进度
+  let count = 0;
+  item.innerHTML = genProgressSpan(count);
 
+  timer_progress = setInterval(() => {
+    is_pause_observe = true;
+    count = count + 1;
+
+    // if (count > 100) {
+    //   // 进度圆满
+    //   clearInterval(timer_progress);
+
+    //   is_pause_observe = false;
+    //   end_node_list = [];
+    //   // mark_map;
+    //   is_pause_observe = false; // 是否暂停DOM检测
+    //   plus_icon_list = [];
+    //   plus_icon_index = 1;
+    //   timer_progress = null;
+
+    //   mark_map.destroy();
+    //   mark_map = null;
+    //   initMarkMap(props.content);
+    //   return;
+    // }
+
+    item.innerHTML = genProgressSpan(count);
+    if (count === 99) {
+      clearInterval(timer_progress);
+      return;
+    }
+  }, 100);
+}
+const getCenter = () => {
+  const svg = svgRef.value;
+  console.dir(svg);
+  const width = svg.clientWidth;
+  const height = svg.clientHeight;
+
+  return {
+    x: width / 2,
+    y: height / 2
+  };
+};
+// 获取当前视图缩放倍数
+const getCurrentZoomScale = () => {
+  const svg = svgRef.value; // 获取 SVG 元素
+  console.log(svg.children);
+  for (const key in svg.children) {
+    console.log(svg.children[key], key);
+    if (svg.children[key].tagName === 'g') {
+      console.log(svg.children[key], 11);
+      const transform = svg.children[key].getAttribute('transform');
+      const scaleMatch = transform.match(/scale\(([^)]+)\)/); // 匹配 scale() 中的值
+      if (scaleMatch) {
+        return parseFloat(scaleMatch[1]); // 返回缩放倍数
+      }
+    }
+  }
+
+  return 1; // 默认缩放倍数为 1(没有缩放时)
+};
+
+// 应用缩放
+const applyZoom = (scaleFactor) => {
+  const center = getCenter(); // 获取视图中心点
+  // 计算新的变换矩阵:缩放 + 平移
+  const newTransform = `scale(${scaleFactor}) translate(${center.x * scaleFactor}, ${center.y * scaleFactor})`;
+  console.log(newTransform);
+  // 将新的变换应用到 Markmap
+  mark_map.handleZoom({ transform: newTransform });
+  // mark_map.rescale(scaleFactor);
+};
+//
+const zoomIn = () => {
+  let scale = getCurrentZoomScale();
+  console.log(scale);
+  scale = scale + 0.1;
+  applyZoom(scale);
+};
+const zoomOut = () => {
+  let scale = getCurrentZoomScale();
+  console.log(scale);
+  if (scale <= 0.1) {
+    return;
+  }
+  scale = scale - 0.1;
+  applyZoom(scale);
+};
+const zoomReset = () => {
+  mark_map.fit();
+};
+const downloadSVG = () => {
+  const svgString = mark_map.svg;
+  console.log(svgString);
+  const blob = new Blob([svgString], { type: 'image/png' });
+  const url = URL.createObjectURL(blob);
+
+  const a = document.createElement('a');
+  a.href = url;
+  a.download = 'mindmap.png';
+  a.click();
+  URL.revokeObjectURL(url);
+};
+// 下载为 PNG
+const downloadPNG = () => {
+  const svg = svgRef.value;
+  const svgClone = svg.cloneNode(true); // 克隆原始 SVG
+
+  // 将 SVG 转换为 Canvas
+  const canvas = document.createElement('canvas');
+  const ctx = canvas.getContext('2d');
+
+  const svgData = new XMLSerializer().serializeToString(svgClone);
+  const img = new Image();
+
+  // 如果是跨域加载,设置 crossOrigin
+  img.crossOrigin = 'Anonymous'; // 设置跨域请求,允许加载外部资源
+
+  // 使用 Blob 转换为图片
+  const svgBlob = new Blob([svgData], { type: 'image/svg+xml' });
+  const url = URL.createObjectURL(svgBlob);
+  console.log(url);
+
+  img.onload = () => {
+    // 确保加载完成后进行绘制
+    canvas.width = img.width;
+    canvas.height = img.height;
+
+    ctx.clearRect(0, 0, canvas.width, canvas.height); // 清空 Canvas
+    ctx.drawImage(img, 0, 0);
+    console.log('canvas', canvas);
+    // 转换为 PNG 数据
+    canvas.toBlob((blob) => {
+      // 创建下载链接
+      const link = document.createElement('a');
+      const url = URL.createObjectURL(blob);
+      link.href = url;
+      link.download = 'mindmap.png'; // 设置下载文件名
+      link.click();
+      URL.revokeObjectURL(url); // 清理 URL 对象
+    }, 'image/png');
+  };
+
+  // 设置图片的 source 为生成的 Blob URL
+  img.src = url;
+};
 watch(
   () => props.content,
-  (newVal, oldVal) => {}
-);
-watch(
-  () => props.progress,
   (newVal, oldVal) => {
-    console.log(newVal, oldVal, '--->>>');
-    if (newVal < 100) {
-      is_pause_observe = true;
-      if (targetEle.value) {
-        targetEle.value.innerHTML = genProgressSpan(newVal);
-      }
-    } else {
-      is_pause_observe = false;
-      // 重新加载mindMap
-      updateMarkMap();
-    }
+    clearInterval(timer_progress);
+
+    is_pause_observe = false;
+    end_node_list = [];
+    // mark_map;
+    is_pause_observe = false; // 是否暂停DOM检测
+    plus_icon_list = [];
+    plus_icon_index = 1;
+    timer_progress = null;
+
+    mark_map.destroy();
+    mark_map = null;
+    console.log(props.content, 111);
+    initMarkMap(props.content);
   }
 );
 
 onMounted(() => initMarkMap(props.content));
 onUpdated(updateMarkMap);
+defineExpose({
+  zoomIn,
+  zoomReset,
+  zoomOut,
+  downloadSVG,
+  downloadPNG
+});
 </script>
 
 <style lang="css" scoped>

+ 25 - 0
src/modules/conversation-aside/ChatAsideExpandBtn.vue

@@ -0,0 +1,25 @@
+<template>
+  <a-tooltip content="展开/收起" position="left" mini>
+    <img class="img-btn" src="../../assets/chat-aside/aside-btn-expond.png" alt="展开/收起" @click="handleClick" />
+  </a-tooltip>
+</template>
+
+<script setup>
+const emit = defineEmits(['toggleRight']);
+/**
+ * 控制展开收起
+ * */
+const handleClick = () => {
+  emit('toggleRight');
+};
+</script>
+<style scoped>
+.img-btn {
+  position: absolute;
+  right: 0px;
+  top: 0px;
+  cursor: pointer;
+  width: 30px;
+  height: 30px;
+}
+</style>

+ 64 - 0
src/modules/conversation-aside/Conversation-chat-mind-tools.vue

@@ -0,0 +1,64 @@
+<template>
+  <!-- <div class="mind-map-tools"> -->
+  <a-tooltip :content="$t('conversation.zoom_in')" position="top" mini>
+    <icon-plus class="icon" @click="zoomIn" />
+  </a-tooltip>
+  <a-tooltip :content="$t('conversation.zoom_out')" position="top" mini>
+    <icon-minus class="icon" @click="zoomOut" />
+  </a-tooltip>
+  <a-tooltip :content="$t('conversation.fit')" position="top" mini>
+    <icon-scan class="icon" @click="zoomReset" />
+  </a-tooltip>
+  <a-tooltip :content="$t('conversation.full_screen')" position="top" mini>
+    <icon-fullscreen v-if="!props.isFullScreen" class="icon" @click="fullScreen" />
+    <icon-fullscreen-exit v-else class="icon" @click="exitFullScreen" />
+  </a-tooltip>
+  <a-tooltip :content="$t('conversation.download')" position="top" mini>
+    <icon-download class="icon" @click="download" />
+  </a-tooltip>
+
+  <!-- </div> -->
+</template>
+
+<script setup>
+const props = defineProps({
+  isFullScreen: Boolean
+});
+
+const emit = defineEmits(['toZoomIn', 'toZoomOut', 'toZoomReset', 'toFullScreen', 'toDownload', 'toExitFullScreen']);
+const zoomIn = () => {
+  emit('toZoomIn');
+};
+const zoomOut = () => {
+  emit('toZoomOut');
+};
+const zoomReset = () => {
+  emit('toZoomReset');
+};
+const fullScreen = () => {
+  emit('toFullScreen');
+};
+const exitFullScreen = () => {
+  emit('toExitFullScreen');
+};
+const download = () => {
+  emit('toDownload');
+};
+</script>
+<style scoped>
+/* .mind-map-tools {
+  display: flex;
+} */
+.icon {
+  cursor: pointer;
+  background-color: var(--color-fill-4);
+  color: var(--color-bg-1);
+  margin-right: 10px;
+  padding: 5px;
+  border-radius: 2px;
+}
+.icon:hover {
+  background-color: var(--color-fill-3);
+  color: var(--color-text-2);
+}
+</style>

+ 61 - 0
src/modules/conversation-aside/Conversation-chat-web-page.vue

@@ -0,0 +1,61 @@
+<template>
+  <div class="web-page">
+    <div class="header">
+      <div class="header-left">{{ $t('conversation.web_page_title') }}</div>
+      <div class="header-right">
+        <a-button class="btn" @click="toClose"> <icon-close /></a-button>
+      </div>
+    </div>
+    <WebPageList class="page-list" :search_list="search_list"></WebPageList>
+  </div>
+</template>
+
+<script setup>
+import WebPageList from './web-page/Web-page-list.vue';
+
+const props = defineProps({
+  search_list: {
+    type: Array,
+    default: () => {
+      return [];
+    }
+  }
+});
+const emit = defineEmits(['toClose']);
+
+const toClose = () => {
+  emit('toClose');
+};
+</script>
+<style scoped>
+.web-page {
+  width: 100%;
+  height: 100%;
+}
+.header {
+  height: 40px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  .header-left {
+    /* text-align: left; */
+    line-height: 30px;
+    padding: 0 20px;
+    font-size: 16px;
+    font-weight: 600;
+  }
+  .header-right {
+    .btn {
+      background-color: var(--color-bg-2);
+      padding: 3px 6px;
+      border-radius: 8px;
+    }
+    .btn:hover {
+      background-color: var(--color-fill-3);
+    }
+  }
+}
+.page-list {
+  height: calc(100% - 30px);
+}
+</style>

+ 103 - 0
src/modules/conversation-aside/web-page/Web-page-list.vue

@@ -0,0 +1,103 @@
+<template>
+  <div class="page-list">
+    <div class="page-list-item" v-for="(item, index) in page_list" :key="index">
+      <div class="list-item-title">
+        <span class="item-index">{{ index + 1 }}</span>
+        <span class="item-title" @click="toDetail(item)">{{ item.title }}</span>
+      </div>
+      <div class="item-des">{{ item.content }}</div>
+      <div class="item-bottom">
+        <div class="item-origin">
+          <img src="" />
+          <span>{{ item.engine }}</span>
+        </div>
+        <a-link @click="toDetail(item)">
+          {{ item.url }}
+        </a-link>
+        <div>{{ item.time }}</div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+const props = defineProps({
+  search_list: {
+    type: Array,
+    default: () => []
+  }
+});
+const page_list = computed(() => {
+  return props.search_list;
+});
+// const page_list = ref([
+//   {
+//     title: '什么是大模型?超大模型和 Foundation Model 呢?',
+//     origin: '知乎',
+//     des: '目前Foundation Model或者是大模型,特别地火,接下来介绍什么是大模型,大模型的基本概念;接着看看大模型的实际作用,然后基于这些实际作用,我们简单展开几个应用场景。最后就是介绍支持大模型训练的AI框架',
+//     url: 'https://www.zhihu.com/question/498275802',
+//     time: '2024-01-04'
+//   }
+// ]);
+
+const toDetail = (item) => {
+  window.open(item.url, '__blank');
+};
+</script>
+<style scoped>
+.page-list {
+  text-align: left;
+  padding: 0 20px;
+  overflow-y: auto;
+  overflow-x: hidden;
+}
+.page-list-item {
+  display: flex;
+  flex-direction: column;
+  padding: 5px 0;
+  color: #707379;
+}
+.list-item-title {
+  /*显示两行溢出显示省咯号 */
+  display: flex;
+  cursor: pointer;
+}
+
+.item-index {
+  margin-right: 5px;
+  line-height: 18px;
+}
+.item-title {
+  color: #007aff;
+  line-height: 18px;
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  text-overflow: ellipsis;
+
+  overflow: hidden;
+}
+.item-des,
+.item-bottom {
+  margin-top: 10px;
+  padding-left: 15px;
+}
+.item-des {
+  line-height: 18px;
+  display: -webkit-box;
+  -webkit-line-clamp: 3;
+  -webkit-box-orient: vertical;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+.item-bottom {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding-bottom: 10px;
+}
+.item-origin {
+  font-weight: bold;
+}
+</style>

+ 69 - 0
src/modules/conversation-chat/DialogCommonUsed.vue

@@ -0,0 +1,69 @@
+<template>
+  <div class="dialog-common-used">
+    <div class="dialog-common-used-item" v-for="item in used_list" :key="item.value" @click="handleClick(item)">
+      <img :src="item.icon" alt="" />
+      <div class="dialog-common-used-item-text">{{ item.value }}</div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref } from 'vue';
+import file_1 from '../../assets/chat/file-1.png';
+import file_2 from '../../assets/chat/file-2.png';
+import file_3 from '../../assets/chat/file-3.png';
+import edit_1 from '../../assets/chat/edit-1.png';
+
+const emit = defineEmits(['updateAskTxt']);
+const used_list = ref([
+  {
+    value: '总结文本',
+    icon: file_1
+  },
+  {
+    value: '帮我写',
+    icon: edit_1
+  },
+  {
+    value: '方案构思',
+    icon: file_2
+  },
+  {
+    value: '提供建议',
+    icon: file_3
+  }
+]);
+/**
+ *
+ * */
+const handleClick = (item) => {
+  emit('updateAskTxt', item.value);
+};
+</script>
+<style scoped>
+.dialog-common-used {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 15px;
+  /* justify-content: space-between; */
+  /* align-items: center; */
+  /* align-content: center; */
+  width: 100%;
+  /* height: 100%; */
+  padding: 10px;
+}
+
+.dialog-common-used-item {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+  width: 120px;
+  height: 48px;
+  border-radius: 12px;
+  background-color: var(--color-bg-1);
+  cursor: pointer;
+}
+</style>

+ 34 - 5
src/modules/conversation-chat/DialogInput.vue

@@ -1,8 +1,8 @@
 <template>
   <div ref="input_area_container">
-    <slot name="left"></slot>
+    <slot name="top"></slot>
     <div class="dialog-input-area-container" :style="'width:' + width + 'px'">
-      <slot name="top"></slot>
+      <!-- <slot name="top"></slot> -->
 
       <div class="textarea-container" v-if="enable_text_input">
         <a-textarea
@@ -24,12 +24,13 @@
         :enable_voice_recognition="enable_voice_recognition"
         :file_accept="file_accept"
         :file_upload_limit="file_upload_limit"
+        :input_file_list_len="input_file_list.length"
         @fileUpload="handleFileUpload"
         @questionDefine="handleQuestionDefine"
         @voiceRecognize="handleVoiceRecognize"
+        @actionConfig="actionConfig"
         @submit="handleSubmit"
       />
-
       <slot name="bottom"></slot>
 
       <FileUploadPreviewV1
@@ -45,7 +46,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted, onUnmounted } from 'vue';
+import { ref, onMounted, onUnmounted, watch } from 'vue';
 import InputActions from './dialog-input/InputActions.vue';
 
 import FileUploadPreviewV1 from '../file-preview/FileUploadPreviewV1.vue';
@@ -55,6 +56,11 @@ import useEleSizeV2 from '../../utils/use-ele-size-v2';
 import useTextareaAutoFocus from './use-textarea-auto-focus';
 
 const props = defineProps({
+  // 首页选择推荐输入的文本
+  ask_input_text: {
+    type: String,
+    default: ''
+  },
   /**
    * 是否禁用操作
    * 默认 false
@@ -170,6 +176,15 @@ const input_file_list = ref([]);
 
 const emit = defineEmits(['submit', 'sizeChange']);
 
+watch(
+  () => props.ask_input_text,
+  (new_val) => {
+    if (new_val.length) {
+      input_text_value.value = new_val;
+    }
+  }
+);
+
 /**
  * 文件选择方法
  */
@@ -205,7 +220,8 @@ const handleSubmit = () => {
 
   emit('submit', {
     text: input_text_value.value,
-    files: real_file_list
+    files: real_file_list,
+    config: action_config.value
   });
 };
 
@@ -299,6 +315,19 @@ const handleQuestionDefine = () => {};
  * 语音识别
  */
 const handleVoiceRecognize = () => {};
+/**
+ * 深度思考
+ * 联网
+ * 知识库
+ */
+const action_config = ref({
+  isDeep: Number(1),
+  knowledgeId: [],
+  chatMode: Number(1)
+});
+const actionConfig = (config) => {
+  action_config.value = config;
+};
 
 /**
  * 移除文本中的换行符

+ 5 - 1
src/modules/conversation-chat/DialogItem.vue

@@ -10,6 +10,7 @@
       @stop="handleStopChat"
       @retry="handleRetryChat"
       @select="handleSelectChat"
+      @edit="handleEditChat"
     />
   </div>
 </template>
@@ -31,7 +32,7 @@ const props = defineProps({
   answer_action_list: Array
 });
 
-const emit = defineEmits(['stop', 'retry', 'select']);
+const emit = defineEmits(['stop', 'retry', 'select', 'edit']);
 
 /**
  * 当前这一项的类别,有两个值
@@ -108,6 +109,9 @@ const handleStopChat = () => {
 const handleRetryChat = () => {
   emit('retry');
 };
+const handleEditChat = () => {
+  emit('edit');
+};
 
 const handleSelectChat = () => {
   emit('select');

+ 20 - 2
src/modules/conversation-chat/DialogList.vue

@@ -1,5 +1,11 @@
 <template>
-  <a-scrollbar type="track" :style="container_style" ref="scroll_container" data-flat="list-container">
+  <a-scrollbar
+    type="track"
+    :style="container_style"
+    ref="scroll_container"
+    data-flat="list-container"
+    :class="is_show_right_page ? 'scrollbar_klass' : ''"
+  >
     <slot name="left"></slot>
 
     <div class="dialog-list dialog-markdown-container">
@@ -11,6 +17,7 @@
         :key="item.client_custom_unique_id"
         :answer_action_list="answer_action_list"
         @select="handleSelectChat(item)"
+        @edit="handleEditChat(item)"
       />
 
       <DialogSuggestion v-if="suggestion.length !== 0" :suggestion="suggestion" />
@@ -53,8 +60,13 @@ const props = defineProps({
    */
   suggestion: Array,
 
-  answer_action_list: Array
+  answer_action_list: Array,
+  /**
+   * 控制滚动条展示
+   * */
+  is_show_right_page: Boolean
 });
+const emit = defineEmits(['edit']);
 
 const scroll2Bottom = useScrollBottom('scroll_container');
 
@@ -72,6 +84,9 @@ const container_style = computed(() => {
  */
 const handleSelectChat = (item) => {};
 
+const handleEditChat = (item) => {
+  emit('edit', item);
+};
 defineExpose({
   scroll2Bottom: () => {
     scroll2Bottom(1000000);
@@ -85,6 +100,9 @@ defineExpose({
   color: var(--color-text-1);
   flex-grow: 1;
 }
+:deep(.scrollbar_klass ~ .arco-scrollbar-track.arco-scrollbar-track-direction-vertical) {
+  display: none;
+}
 </style>
 
 <style>

+ 30 - 0
src/modules/conversation-chat/DialogStopSse.vue

@@ -0,0 +1,30 @@
+<template>
+  <div class="stop-sse">
+    <a-button class="stop-btn" @click="stopSse">
+      <img class="icon" src="../../assets/chat/stop-1.png" alt="" />
+      停止响应
+    </a-button>
+  </div>
+</template>
+
+<script setup>
+const emit = defineEmits(['stopResponse']);
+const stopSse = () => {
+  emit('stopResponse');
+};
+</script>
+<style scoped>
+.stop-sse {
+  text-align: center;
+}
+.stop-btn {
+  width: 113px;
+  height: 36px;
+  border-radius: 12px;
+  background-color: var(--color-bg-1);
+  margin-bottom: 10px;
+  .icon {
+    margin-right: 3px;
+  }
+}
+</style>

+ 11 - 6
src/modules/conversation-chat/DialogWelcome.vue

@@ -1,8 +1,9 @@
 <template>
   <div class="dialog-welcome-content">
-    <div>
-      <!-- <span>对话欢迎内容:欢迎内容应该按照agent进行配置,最好是从后端进行返回</span> -->
-    </div>
+    <!-- <div class=""> -->
+    <!-- <span>对话欢迎内容:欢迎内容应该按照agent进行配置,最好是从后端进行返回</span> -->
+    <p class="dialog-welcome-content-header">{{ $t('conversation.welcome') }}</p>
+    <!-- </div> -->
   </div>
 </template>
 
@@ -19,9 +20,13 @@
   margin-right: auto;
 
   color: var(--color-text-1);
-
+  text-align: center;
   /* background-color: #aaa; */
-  width: 300px;
-  height: 300px;
+  /* width: 300px;
+  height: 300px; */
+}
+.dialog-welcome-content-header {
+  font-weight: bolder;
+  font-size: 36px;
 }
 </style>

+ 148 - 24
src/modules/conversation-chat/dialog-input/InputActions.vue

@@ -1,11 +1,34 @@
 <template>
   <div class="dialog-input-actions">
     <div class="dialog-input-action-left">
-      <!-- <div class="action-left-item-btn connect-internet" @click="handleSwitchChange">
-        <a-switch class="chat-connect-network-switch" :default-checked="default_switch" :model-value="cur_switch" />
-
-        <span class="search-from-network" :class="search_text_klass">{{ $t('conversation.online_search') }}</span>
-      </div> -->
+      <a-button
+        class="action-left-item-btn"
+        :class="is_deep_think ? 'action-left-item-btn-active' : ''"
+        @click="handleThinkChange"
+        ><icon-ear />
+        {{ $t('conversation.deep_think') }}
+      </a-button>
+      <a-button
+        class="action-left-item-btn"
+        :class="is_connect_internet ? 'action-left-item-btn-active' : ''"
+        :disabled="disabled_internet"
+        @click="handleInternetChange"
+      >
+        <img class="icon" v-if="!is_connect_internet" src="../../../assets/chat/internet.png" alt="" />
+        <img class="icon" v-else src="../../../assets/chat/internet-active.png" alt="" />
+        {{ $t('conversation.internet_search') }}
+      </a-button>
+      <a-select
+        :disabled="disabled_knowledge"
+        :placeholder="$t('conversation.knowledge_base')"
+        :trigger-props="{ autoFitPopupMinWidth: true }"
+        v-model="base_list"
+        multiple
+        allow-clear
+        :max-tag-count="1"
+      >
+        <a-option v-for="item in knowledge_list" :key="item.id" :label="item.name" :value="item.id"></a-option>
+      </a-select>
     </div>
 
     <div class="dialog-input-action-right">
@@ -30,7 +53,7 @@
       </a-tooltip> -->
 
       <a-tooltip :content="$t('conversation.file_upload_message')" position="top">
-        <div v-if="enable_file_upload" class="action-right-item-btn" @click="handleFileUpload">
+        <div v-if="enable_file_upload && !disabled_upload" class="action-right-item-btn" @click="handleFileUpload">
           <img src="../../../assets/file-upload.png" />
         </div>
       </a-tooltip>
@@ -45,7 +68,8 @@
 </template>
 
 <script setup>
-import { ref, computed } from 'vue';
+import { ref, computed, onMounted, watch } from 'vue';
+import { getKnowledgeList } from '../../../apis/knowledge.js';
 
 const props = defineProps({
   /**
@@ -111,28 +135,96 @@ const props = defineProps({
     type: Number,
     default: 0,
     required: false
+  },
+  input_file_list_len: {
+    type: Number,
+    default: 0
   }
 });
 
-const emit = defineEmits(['fileUpload', 'questionDefine', 'voiceRecognize', 'submit']);
+const emit = defineEmits(['fileUpload', 'questionDefine', 'voiceRecognize', 'submit', 'actionConfig']);
 
-// 联网搜索开关
-const default_switch = ref(true);
-const cur_switch = ref(true);
-const search_text_klass = computed(() => {
-  return {
-    'is-connnected': cur_switch.value,
-    'not-connnected': !cur_switch.value
-  };
-});
-const handleSwitchChange = (val) => {
-  if (cur_switch.value) {
-    cur_switch.value = false;
+/**
+ *
+ * 深度思考
+ * 联网搜索
+ * */
+const is_deep_think = ref(false);
+const is_connect_internet = ref(false);
+
+//
+const disabled_internet = ref(false);
+const disabled_knowledge = ref(false);
+const disabled_upload = ref(false);
+
+// 深度思考
+const handleThinkChange = () => {
+  is_deep_think.value = !is_deep_think.value;
+  emit('actionConfig', {
+    isDeep: is_deep_think.value ? 2 : 1,
+    knowledgeId: base_list.value,
+    chatMode: is_connect_internet.value ? 2 : 1
+  });
+};
+// 联网搜索
+const handleInternetChange = () => {
+  if (disabled_internet.value) {
+    is_connect_internet.value = false;
+    return;
+  }
+  is_connect_internet.value = !is_connect_internet.value;
+  if (is_connect_internet.value) {
+    disabled_knowledge.value = true;
+    disabled_upload.value = true;
   } else {
-    cur_switch.value = true;
+    disabled_knowledge.value = false;
+    disabled_upload.value = false;
   }
+  emit('actionConfig', {
+    isDeep: is_deep_think.value ? 2 : 1,
+    knowledgeId: base_list.value,
+    chatMode: is_connect_internet.value ? 2 : 1
+  });
 };
 
+const base_list = ref([]);
+/**
+ * 知识库
+ * */
+const knowledge_list = ref([]);
+watch(
+  () => base_list.value.length,
+  (len) => {
+    if (len) {
+      disabled_internet.value = true;
+      disabled_upload.value = true;
+    } else {
+      disabled_internet.value = false;
+      disabled_upload.value = false;
+    }
+    emit('actionConfig', {
+      isDeep: is_deep_think.value ? 2 : 1,
+      knowledgeId: base_list.value,
+      chatMode: is_connect_internet.value ? 2 : 1
+    });
+  },
+  {
+    immediate: true
+  }
+);
+watch(
+  () => props.input_file_list_len,
+  (len) => {
+    if (len) {
+      disabled_internet.value = true;
+      disabled_knowledge.value = true;
+    } else {
+      disabled_internet.value = false;
+      disabled_knowledge.value = false;
+    }
+  }
+);
+
 const handleQuestionDefine = () => {
   emit('questionDefine');
 };
@@ -148,6 +240,20 @@ const handleVoiceRecognize = () => {
 const handleSubmit = () => {
   emit('submit');
 };
+
+const getKnowledgeListFun = async () => {
+  try {
+    const res = await getKnowledgeList(1, 999, '');
+    if (res.code / 1 === 200) {
+      knowledge_list.value = res.data.rows;
+    }
+  } catch (e) {
+    console.log(e);
+  }
+};
+onMounted(() => {
+  getKnowledgeListFun();
+});
 </script>
 
 <style lang="css" scoped>
@@ -155,12 +261,31 @@ const handleSubmit = () => {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  height: 30px;
 }
 
 .dialog-input-action-left {
+  display: flex;
+}
+.action-left-item-btn {
+  height: 30px;
+  line-height: 30px;
+  cursor: pointer;
+  padding: 0 10px;
+  border: 1px solid var(--color-border-2);
+  color: var(--color-text-2);
+  border-radius: 8px;
+  background: var(color-bg-1);
+  .icon {
+    width: 14px;
+    margin-right: 3px;
+  }
+}
+.action-left-item-btn:hover {
+}
+.action-left-item-btn-active {
+  border-color: var(--color-bolder-11);
+  color: var(--color-bolder-11);
 }
-
 .dialog-input-action-right {
   display: flex;
   justify-content: space-between;
@@ -173,7 +298,6 @@ const handleSubmit = () => {
 }
 
 .connect-internet {
-  /* color: #707379; */
   color: var(--color-text-3);
 }
 .search-from-network {

+ 13 - 0
src/modules/conversation-chat/use-conversation-chat-parameters.js

@@ -1,6 +1,7 @@
 import { ref } from 'vue';
 
 import { conversationQAParameter } from './api-conversation-chat-qa';
+import { compact } from 'lodash-es';
 
 export default function useConversationChatParameters() {
   // api请求的状态
@@ -17,6 +18,18 @@ export default function useConversationChatParameters() {
     // 进入加载状态
     api_status.value = 2;
     api_message.value = '';
+    //
+    if (!agent_id) {
+      conversation_params.value = {
+        retriever_resource: true,
+        file_upload: {
+          enable: true
+        }
+      };
+      api_status.value = 3;
+      api_message.value = 'success';
+      return;
+    }
     conversationQAParameter(agent_id)
       .then((res) => {
         if (res.code / 1 === 200) {

+ 33 - 1
src/modules/conversation-content/DialogContent.vue

@@ -1,5 +1,11 @@
 <template>
   <div class="dialog-chat-answer-content" :style="custom_style">
+    <!-- <div class="think-select" v-if="show_think">
+      <icon-robot class="robot" @click="handleThinkClick" />
+      <span>思考中...</span>
+      <icon-down v-if="true" class="down" />
+      <icon-up v-else class="down" />
+    </div> -->
     <div v-if="is_loading">
       <a-spin></a-spin>
     </div>
@@ -32,7 +38,11 @@ const props = defineProps({
   /**
    * 内容区域最大宽度
    */
-  max_width: Number
+  max_width: Number,
+  /**
+   *  是否展示思考
+   * */
+  show_think: Boolean
 });
 
 /**
@@ -46,6 +56,10 @@ const custom_style = computed(() => {
     return '';
   }
 });
+
+const handleThinkClick = () => {
+  // 展开/收起
+};
 </script>
 
 <style lang="css" scoped>
@@ -67,4 +81,22 @@ const custom_style = computed(() => {
 .answer-content-text {
   line-height: 20px;
 }
+.think-select {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 5px;
+  height: 40px;
+  width: 130px;
+  border-radius: 12px;
+  background: var(--color-fill-2);
+  cursor: pointer;
+  .robot {
+    color: #668ff3;
+    font-size: 16px;
+  }
+  .down {
+    margin-left: 10px;
+  }
+}
 </style>

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

@@ -61,6 +61,28 @@ export const uploadFilesV2 = (agent_id, chat_id, file) => {
     }
   });
 };
+/**
+ *上传文件v3
+ */
+export const uploadFilesV3 = (chatMode, file) => {
+  /**
+   * 响应数据示例
+   * 单文件
+   */
+  const res_example1 = {
+    code: 200,
+    msg: '',
+    data: {
+      id: 'faf0d7f5-ba4d-4dcc-81bd-be826accfeb9',
+      name: 'juejin.pdf'
+    }
+  };
+  return ajax.post(`/api/v1/complex/upload/${Number(chatMode)}`, file, {
+    headers: {
+      'Content-Type': 'multipart/form-data'
+    }
+  });
+};
 
 /**
  * 报表合并上传文件
@@ -165,6 +187,22 @@ export const uploadFileByAgentInfo = async (agent_id, chat_id, client_file, menu
     }
   }
 };
+/**
+ * 首页文档上传
+ * */
+export const uploadNewChatInfo = async (chatMode, client_file) => {
+  const file_params = files2Formdata(client_file, 'file');
+  const uploadResult = await uploadFilesV3(chatMode, file_params);
+  if (uploadResult.id) {
+    const file_uploaded = uploadResult.data;
+    return {
+      code: 200,
+      data: file_uploaded
+    };
+  } else {
+    return uploadResult;
+  }
+};
 
 /**
  * 新建对话,根据agent获取chat_id
@@ -201,3 +239,20 @@ export const getHistoryLogs = (conversation_id) => {
       });
   });
 };
+
+// 生成思维导图
+export const getMindMapData = ({ messageId, query }) => {
+  return new Promise((resolve, reject) => {
+    ajax
+      .post(`/api/mindmap/create`, { messageId, query })
+      .then((res) => {
+        resolve({
+          code: 200,
+          data: res.data
+        });
+      })
+      .catch((err) => {
+        reject(err);
+      });
+  });
+};

+ 30 - 3
src/modules/conversation-dialog/format-dialog-answer-from-sse_receive_message.js

@@ -25,7 +25,8 @@ import formatDialogAnswerAttachedHtmlBeforeRender from './format-dialog-answer-a
  * @param {object} preMsg - 前一次的消息,如果是流式消息,则需要上一次的Content进行拼接,从而形成新的消息
  * @returns
  */
-export default function formatDialogAnswerFromSseReceiveMessage(message_obj, preMsg, agentInfo) {
+export default function formatDialogAnswerFromSseReceiveMessage(message_obj, preMsg, agentInfo, mixAnswer) {
+  console.log(message_obj, 2222);
   const res_content = message_obj.data;
 
   // 复制一份原消息内容
@@ -98,14 +99,39 @@ export default function formatDialogAnswerFromSseReceiveMessage(message_obj, pre
     // system: '', // 系统消息
 
     // 进行中的消息: 系统消息
-    client_custom_running_msg: ''
+    client_custom_running_msg: '',
 
     // file: '',
     // image_url: '',
     // code: '',
     // sql: '',
     // reference: ''
+    // 会话的message_id
+    client_custom_chat_message_id: ''
+    //   client_custom_item_think: false, // 是否有思考
+    //   client_custom_item_think_content: '', //思考内容
+    //   client_custom_item_internet: false, //是否联网
+    //   client_custom_item_internet_list: [], //联网内容
+    //   client_custom_chat_message_id: '' //会话的message_id
   };
+  // if (Object.keys(message_obj).includes('message_id')) {
+
+  // }
+  if (Object.keys(message_obj).includes('isDeep')) {
+    currentMsg.client_custom_item_think = message_obj.isDeep;
+  }
+  if (Object.keys(message_obj).includes('isInternet')) {
+    currentMsg.client_custom_item_internet = message_obj.isInternet;
+    if (res_content.title === 'SearXNG 搜索') {
+      currentMsg.client_custom_item_internet_list = res_content.outputs.json;
+    }
+  }
+  console.log(message_obj.message_id, 3333);
+  if (message_obj.message_id) {
+    currentMsg.client_custom_chat_message_id = message_obj.message_id;
+  }
+  currentMsg.client_custom_chat_message_id = message_obj.message_id || '';
+  console.log(currentMsg.client_custom_chat_message_id, 444);
 
   /**
    * 将前面websocket发来的数据,合并到当前的消息中
@@ -421,8 +447,9 @@ export default function formatDialogAnswerFromSseReceiveMessage(message_obj, pre
   //  * 在answer字符串渲染到界面之前,在answer后拼接的html字符串
   //  */
   // currentMsg.client_custom_attached_html_before_render = temp_html_str;
-
+  console.log(currentMsg.client_custom_chat_message_id, 6666);
   formatDialogAnswerAttachedHtmlBeforeRender(currentMsg);
+  console.log(currentMsg.client_custom_chat_message_id, 8888);
   return {
     status: chat_status, // 本轮会话的状态:0 未开始 1 进行中 2 已结束 3 错误
     message: chat_msg,

+ 16 - 4
src/modules/conversation-dialog/use-sse-chat.js

@@ -48,6 +48,10 @@ function getSseUrl(agent_id, mode) {
   if (['agent-basic'].includes(mode)) {
     return base_url + `/api/v1/complex/${chat_id}/completions`;
   }
+  // 无agent_id默认为首页对话
+  if (!agent_id) {
+    return base_url + `/api/v1/complex/chat/completions`;
+  }
 
   console.log(base_url);
 }
@@ -64,8 +68,7 @@ export default function useFetchEventSource() {
   const isLoading = ref(false); // 是否正在连接
   const error = ref(null); // 错误信息
   const isConnected = ref(false); // 是否已连接
-  const abortController = new AbortController(); // 用于取消请求
-
+  let abortController = null;
   /**
    * queryParams: sse接口需要传递的参数
    * */
@@ -73,12 +76,21 @@ export default function useFetchEventSource() {
     isLoading.value = true;
     isConnected.value = false;
     error.value = null;
-    const { agent_id, session_id, mode, query, files, inputs } = queryParams;
+    const { agent_id, session_id, mode, query, files, inputs, knowledgeId, chatMode, isDeep } = queryParams;
+    console.log('sse_url');
+    let params;
+    if (agent_id) {
+      params = { sessionId: session_id, query, files, inputs };
+    } else {
+      params = { sessionId: session_id, query, files, knowledgeId, chatMode, isDeep };
+    }
 
     const sse_url = getSseUrl(agent_id, mode);
     if (!sse_url) {
       return;
     }
+    console.log('sse_url', sse_url);
+    abortController = new AbortController();
     fetchEventSource(sse_url, {
       method: 'POST',
       headers: {
@@ -86,7 +98,7 @@ export default function useFetchEventSource() {
         'Content-Type': 'application/json',
         Accept: 'text/event-stream'
       },
-      body: JSON.stringify({ sessionId: session_id, query, files, inputs }),
+      body: JSON.stringify(params),
       signal: abortController.signal, // 支持取消请求messages
       // openWhenHidden: true, //页面退至后台后保持连接
       onmessage(ev) {

+ 25 - 2
src/modules/conversation-item/DialogActions.vue

@@ -7,7 +7,27 @@
         </div>
       </a-tooltip>
 
-      <!-- <div class="action-btn-item" @click="handleRetry">新对话,最后一条会话有此按钮 再试一次</div> -->
+      <div class="action-btn-item" @click="handleRetry">
+        <a-tooltip :content="$t('conversation.retry')" position="right" v-if="isEnableBtn('retry')">
+          <div class="action-btn-item" @click="handleRetry">
+            <icon-refresh size="18" />
+          </div>
+        </a-tooltip>
+      </div>
+      <div class="action-btn-item">
+        <a-tooltip :content="$t('conversation.edit')" position="right" v-if="isEnableBtn('edit')">
+          <div class="action-btn-item" @click="handleEdit">
+            <icon-edit size="18" />
+          </div>
+        </a-tooltip>
+      </div>
+      <div class="action-btn-item" @click="handleDownload">
+        <a-tooltip :content="$t('conversation.download')" position="right" v-if="isEnableBtn('download')">
+          <div class="action-btn-item" @click="handleDownload">
+            <icon-download size="18" />
+          </div>
+        </a-tooltip>
+      </div>
 
       <!-- <div class="action-btn-item" @click="handleShare">分享</div> -->
     </div>
@@ -39,7 +59,7 @@ const props = defineProps({
   action_list: Array
 });
 
-const emit = defineEmits(['copy', 'retry', 'share']);
+const emit = defineEmits(['copy', 'retry', 'share', 'edit']);
 
 /**
  * 检查一个按钮是否可用
@@ -79,6 +99,9 @@ const handleCopy = () => {
 const handleRetry = () => {
   emit('retry');
 };
+const handleEdit = () => {
+  emit('edit');
+};
 /**
  * 分享
  */

+ 26 - 2
src/modules/conversation-item/DialogAnswer.vue

@@ -14,7 +14,12 @@
 
       <div class="dialog-content answer-content">
         <div>
-          <DialogContent :content="answer_html" :is_loading="is_content_loading" :max_width="content_max_width">
+          <DialogContent
+            :content="answer_html"
+            :is_loading="is_content_loading"
+            :max_width="content_max_width"
+            :show_think="show_think"
+          >
             <DialogActions
               v-if="answer_status === 2"
               class="answer-actions-btns"
@@ -22,6 +27,7 @@
               @copy="handleAnswerCopy"
               @share="handleAnswerShare"
               @retry="handleAnswerRetry"
+              @edit="handleAnswerEdit"
             />
           </DialogContent>
 
@@ -94,7 +100,7 @@ const props = defineProps({
   answer_action_list: Array
 });
 
-const emit = defineEmits(['stop', 'retry', 'select']);
+const emit = defineEmits(['stop', 'retry', 'select', 'edit']);
 
 const select_btn_visible = computed(() => {
   return props.show_select === true;
@@ -231,6 +237,12 @@ const full_refference = computed(() => {
 
   if (Object.keys(answer.client_custom_linked_refference_full_content).length === 0) {
     return null;
+  } else {
+    for (let key in answer.client_custom_linked_refference_full_content) {
+      if (answer.client_custom_linked_refference_full_content[key].length === -0) {
+        return null;
+      }
+    }
   }
 
   return answer.client_custom_linked_refference_full_content;
@@ -246,6 +258,12 @@ const has_refference = computed(() => {
 
   return true;
 });
+/**
+ * 是否展示思考过程
+ * */
+const show_think = computed(() => {
+  return true;
+});
 
 const handleSelectChange = () => {
   emit('select');
@@ -277,6 +295,12 @@ const handleAnswerShare = () => {};
 const handleAnswerRetry = () => {
   emit('retry');
 };
+/**
+ * 编辑文本
+ * */
+const handleAnswerEdit = () => {
+  emit('edit');
+};
 
 onMounted(() => {
   const container_klass = anwser_klass.value;

+ 2 - 1
src/modules/conversation-item/DialogQuestion.vue

@@ -5,7 +5,8 @@
     <div class="dialog-main question-main">
       <div class="dialog-content question-content">
         <div class="question-main-content">
-          <DialogContent :content="question.content" class="question-content-core-text"> </DialogContent>
+          <DialogContent :content="question.content" :show_think="false" class="question-content-core-text">
+          </DialogContent>
 
           <FileUploadPreviewV2
             v-if="question_attached_files.length !== 0"

+ 5 - 2
src/modules/intelligent/MessageBody.vue

@@ -335,9 +335,12 @@ const editIconHandle = (item) => {
 };
 
 bus_intelligentSearch.on((search_data) => {
-  if (search_data.searchText) {
-    search_text.value = search_data.searchText;
+  for (const key in search_data) {
+    if (key === 'searchText') {
+      search_text.value = search_data.searchText;
+    }
   }
+
   if (search_data.target_label_list) {
     search_labelId_list.value = search_data.target_label_list;
   }

+ 22 - 3
src/modules/intelligent/TopSetting.vue

@@ -94,7 +94,7 @@ import ModelSelectIcon from './ModalSelectIcon.vue';
 import AgentConfig from './rgflow/agentConfig.vue';
 import { funcDebounce } from '../../utils/utils';
 import { getRagFlowData } from '../../apis/rgflow-dify';
-import { updateOwnIntelligentIcon } from '../../apis/intelligent';
+import { updateOwnIntelligentIcon, getDifyProfile } from '../../apis/intelligent';
 import { getDefaultIntelligent } from '../../views/manage/common';
 import {
   bus_intelligentSearch,
@@ -200,6 +200,7 @@ const getRagFlowDataFunc = async (item) => {
   target_icon.value = item.icon;
   try {
     const res = await getRagFlowData(item.id);
+    // res.data.vector_similarity_weight = 1 - Number(res.data.vector_similarity_weight);
     Object.assign(itemObj, res.data);
     editAgentKuai.value.editClick();
   } catch (err) {
@@ -217,6 +218,20 @@ const clearSelectIcon = () => {
   add_type.value = 'add';
   edit_icon_data.value = null;
 };
+
+const getDifyProfileFun = async () => {
+  try {
+    const res = await getDifyProfile();
+    if (res.id && res.id.length) {
+      return true;
+    } else {
+      return false;
+    }
+  } catch (e) {
+    console.log(e);
+  }
+};
+
 /**
  * 监听编辑智能体事件
  * @param {Object} item 编辑智能体对象
@@ -225,7 +240,7 @@ const clearSelectIcon = () => {
  * case 3: 编辑basic智能体
  * case 4: 编辑Dify智能体
  * */
-bus_intelligentEdit.on((item) => {
+bus_intelligentEdit.on(async (item) => {
   switch (item.agentType) {
     case '1':
       // TODO: 编辑rgflow智能体
@@ -237,12 +252,16 @@ bus_intelligentEdit.on((item) => {
       break;
     case '4':
       // TODO: 进入Dify智能体配置
-      router.push({ path: '/intelligent/detail', query: { id: item.id, name: item.name, mode: item.mode } });
+      const res = await getDifyProfile();
+      if (res.id && res.id.length) {
+        router.push({ path: '/intelligent/detail', query: { id: item.id, name: item.name, mode: item.mode } });
+      }
       break;
     default:
       console.log('暂不支持该智能体类型');
   }
 });
+
 bus_intelligentIconEdit.on((item) => {
   show_select_icon.value = true;
   edit_icon_data.value = item;

+ 2 - 1
src/modules/intelligent/rgflow/AgentConfig.vue

@@ -537,7 +537,8 @@ const handleBeforeOk = async (done) => {
       //formNew.llm_setting.max_tokens = '';
       delete formNew.llm_setting.max_tokens;
     }
-
+    // formMew.vector_similarity_weight = 1 - Number(formMew.vector_similarity_weight);
+    // console.log(formNew, 'formNew');
     setLoading(true);
     try {
       const res = await createRgFlow(formNew);

+ 1 - 1
src/router/index.js

@@ -17,7 +17,7 @@ const router = createRouter({
  */
 
 /** 免验证白名单 */
-const whiteList = ['/login', '/reg', '/denied', '/', '/chat/voice'];
+const whiteList = ['/login', '/reg', '/denied', '/chat/voice'];
 
 /**
  * 需要登录的链接

+ 91 - 58
src/views/ConversationQA.vue

@@ -1,10 +1,11 @@
 <template>
   <LayoutExperience>
     <ContentContainer :containerStyle="containerStyle">
-      <ChatAsideMenu />
+      <ChatAsideMenu @openWelcome="openWelcome" @toNewChat="newChat" />
 
       <template v-if="query_value_status === 2">
         <ConversationChatContainer
+          ref="conversation_chat_container"
           :agent_id="chat_agent"
           :CHAT_AGENT_INFOMATION="CHAT_AGENT_INFOMATION"
           :session_id="chat_session"
@@ -37,8 +38,6 @@ import ChatAsideMenu from './conversation/ChatAsideMenu.vue';
 
 import ConversationChatContainer from './conversation/ConversationChatContainer.vue';
 
-import ChatAsideBtn from './conversation/ChatAsideBtn.vue';
-
 import { useAllAgentStore } from '../base/store/use-all-agents';
 import getChatAgentInfomation from '../modules/conversation-board/get-chat-agent-infomation';
 
@@ -75,6 +74,8 @@ const allAgent = useAllAgentStore();
 const chat_agent = ref('');
 const chat_session = ref('');
 
+const conversation_chat_container = ref(null);
+
 /**
  * 对话区域的唯一ID
  * 智能体的ID + 当前路由地址
@@ -147,32 +148,32 @@ async function getDefaultAgentId() {
   const default_id = getConfigField('DEFAULT_CHAT_AGENT_ID');
   const default_name = getConfigField('DEFAULT_CHAT_AGENT_NAME');
 
-  const agent_list = allAgent.getList();
-  const len = agent_list.length;
-
-  if (len > 1) {
-    for (let i = 0; i < len; i++) {
-      const item = agent_list[i];
-      if (item.name === default_name) {
-        default_agent_id = item.id;
-        break;
-      }
-    }
-    if (!default_agent_id) {
-      // 未搜索到: 使用第一个
-      default_agent_id = agent_list[0].id;
-    }
-  } else if (len === 1) {
-    // 仅有一个
-    default_agent_id = agent_list[0].id;
-  } else {
-    // 为0: 使用配置的ID
-    default_agent_id = default_id;
-  }
-  if (!default_agent_id) {
-    // 如果ID依然不生效,则需要请求api
-    // TODO: 请求API
-  }
+  // const agent_list = allAgent.getList();
+  // const len = agent_list.length;
+
+  // if (len > 1) {
+  //   for (let i = 0; i < len; i++) {
+  //     const item = agent_list[i];
+  //     if (item.name === default_name) {
+  //       default_agent_id = item.id;
+  //       break;
+  //     }
+  //   }
+  //   if (!default_agent_id) {
+  //     // 未搜索到: 使用第一个
+  //     default_agent_id = agent_list[0].id;
+  //   }
+  // } else if (len === 1) {
+  //   // 仅有一个
+  //   default_agent_id = agent_list[0].id;
+  // } else {
+  //   // 为0: 使用配置的ID
+  //   default_agent_id = default_id;
+  // }
+  // if (!default_agent_id) {
+  //   // 如果ID依然不生效,则需要请求api
+  //   // TODO: 请求API
+  // }
 
   return default_agent_id;
 }
@@ -182,6 +183,10 @@ async function getDefaultAgentId() {
  * @param agent_id
  */
 function comfirmAgent(agent_id) {
+  if (!agent_id) {
+    // 根据agent_id 查询到动态表单数据
+    getConversationParams(undefined);
+  }
   /**
    * 根据agent_id 查询到agent的基本信息
    * 从而确定agent的相关功能支持,例如:是否需要上传文件,支持上传什么类型的文件、支持上传多少个文件?
@@ -197,34 +202,62 @@ function comfirmAgent(agent_id) {
 const updateSessionId = (session_id) => {
   chat_session.value = session_id;
 };
+
+const openWelcome = () => {
+  if (conversation_chat_container.value) {
+    conversation_chat_container.value.toShowWelcome();
+  }
+};
+
+const newChat = async () => {
+  await init();
+  if (conversation_chat_container.value) {
+    conversation_chat_container.value.toNewChat();
+  }
+};
+watch(
+  () => route,
+  (val) => {
+    if (route.query.chat) {
+      newChat();
+    }
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+);
 async function init() {
   const route = useRoute();
 
-  const query = route.query;
-
-  const agent_id = query.agent;
-  const session_id = query.session;
+  const query = route?.query;
+  let agent_id, session_id;
+  if (query && query.agent) {
+    agent_id = query.agent;
+  }
+  if (query && query.session) {
+    session_id = query.session;
+  }
 
   if (!agent_id) {
+    query_value_status.value = 2;
+    query_value_message.value = 'success';
     // 路由中不存在智能体ID参数
     // 获取到默认的智能体ID
     const default_agent_id = await getDefaultAgentId();
-
-    // 将路由重置为包含agent参数的地址
-    router.replace({
-      path: '/chat/qa',
-      query: {
-        agent: default_agent_id
-      }
-    });
-
-    // // chat_agent.value = default_agent_id;
-    // comfirmAgent(default_agent_id);
+    // // 将路由重置为包含agent参数的地址
+    // router.replace({
+    //   path: '/chat/qa',
+    //   query: {
+    //     agent: default_agent_id
+    //   }
+    // });
+    chat_agent.value = default_agent_id;
+    comfirmAgent(default_agent_id);
     // // ------------------------------------------------------
     // // ------------------------------------------------------
     // // ------------------------------------------------------
     // // ------------------------------------------------------
-
     // query_value_status.value = 2;
     // query_value_message.value = 'success';
   } else {
@@ -267,19 +300,19 @@ onBeforeRouteUpdate(async (to, from) => {
     if (!to.query.session) {
       chat_session.value = null;
     }
-
-    if (!to.query.agent) {
-      const default_agent_id = await getDefaultAgentId();
-
-      chat_agent.value = default_agent_id;
-
-      router.replace({
-        path: '/chat/qa',
-        query: {
-          agent: default_agent_id
-        }
-      });
-    }
+    // if (!to.query.agent) {
+    //   const default_agent_id = await getDefaultAgentId();
+
+    //   chat_agent.value = default_agent_id;
+    //   console.log(default_agent_id);
+
+    //   router.replace({
+    //     path: '/chat/qa',
+    //     query: {
+    //       agent: default_agent_id
+    //     }
+    //   });
+    // }
   }
 });
 

+ 58 - 78
src/views/Home.vue

@@ -1,94 +1,74 @@
 <template>
   <div>SmartAI</div>
-  <!-- <div class="box">
-    <MindMap
-      :content="test_mk_str"
-      :box_style="box_style"
-      :svg_style="svg_style"
-      :progress="progress"
-      @extend="extend"
-    ></MindMap>
-  </div> -->
+
+  <div class="box">
+    <Editor></Editor>
+    <!-- <MindMap :content="mindMap_content" :box_style="box_style" :svg_style="svg_style" @extend="extend"></MindMap> -->
+  </div>
 </template>
 
 <script setup>
 import { ref, onMounted } from 'vue';
+import Editor from '../components/Editor.vue';
 // import MindMap from '../components/MindMap.vue';
 // // 系统第一页
 
-// const mindMap_content = ref();
-// const test_mk_str = `
-// # markmap
+const isFullScreen = ref(false);
+const toFullScreen = () => {
+  isFullScreen.value = !isFullScreen.value;
+};
 
-// - beautiful
-// - useful
-// - easy
-//   - hard
-//   - code
-// - interactive
-//   - age
-//   - name
-//   - job
-//     - shaa
-//     - hhe
-//     - nihao
-//       - jfksaj
-//     - zheg
-// `;
-// const test_mk_str_extend = `
-// # markmap
+const mindMap_content = ref('');
+const test_mk_str = `
+# 一体机 (AIO)\n## 定义\n- 集成系统\n  - 显示器\n    - 基本特点\n      - 显示技术\n        - 液晶显示器(LCD)\n          - TN\n          - IPS\n          - VA\n        - 有机发光二极管显示器(OLED)\n        - 微型LED显示器(MicroLED)\n      - 分辨率\n        - 1080p (1920x1080)\n        - 2K (2560x1440)\n        - 4K (3840x2160)\n      - 刷新率\n        - 60Hz\n        - 75Hz\n        - 120Hz\n        - 144Hz\n      - 响应时间\n        - 减少图像拖影\n        - 提高动态画面清晰度\n      - 接口类型\n        - VGA\n        - DVI\n        - HDMI\n        - DisplayPort\n    - 分类\n      - 按用途\n        - 普通显示器\n        - 专业显示器\n        - 游戏显示器\n      - 按尺寸\n        - 21.5英寸\n        - 24英寸\n        - 27英寸\n        - 32英寸\n      - 按曲率\n        - 平面显示器\n        - 曲面显示器\n    - 适用范围\n      - 个人电脑\n      - 笔记本电脑\n      - 电视\n      - 手机\n      - 平板电脑\n  - 主板\n  - 处理器\n  - 内存\n  - 存储设备\n  - 电源\n## 特点\n- 紧凑设计\n  - 节省空间\n  - 整洁环境\n- 易于安装和维护\n  - 快速连接\n  - 集成组件\n- 外观美观\n  - 现代化设计\n  - 适合装饰\n- 性能多样\n  - 办公应用\n  - 图形处理\n  - 游戏\n- 扩展性有限\n  - 内部空间小\n  - 升级能力低\n## 适用范围\n- 办公\n- 家庭娱乐\n- 教育\n
+`;
+const test_mk_str_extend = `
+# markmap
 
-// - beautiful
-// - useful
-// - easy
-//   - hard
-//   - code
-// - interactive
-//   - age
-//   - name
-//   - job
-//     - shaa
-//     - hhe
-//     - nihao
-//       - jfksaj
-//     - zheg
-//       - 张三
-//       - 李四
-//       - 王五
-//       - 赵六
-//       - 孙七
-// `;
+- beautiful
+- useful
+- easy
+  - hard
+  - code
+- interactive
+  - age
+  - name
+  - job
+    - shaa
+    - hhe
+    - nihao
+      - jfksaj
+    - zheg
+      - 张三
+      - 李四
+      - 王五
+      - 赵六
+      - 孙七
+`;
 
-// const box_style = ref({
-//   width: '100%',
-//   height: '100%'
-// });
-// const svg_style = ref({
-//   width: '100%',
-//   height: '100%'
-// });
-// const progress = ref(0);
-// const target_node = ref(null);
-// const root_full = ref(null);
-// let timer_progress = null;
+const box_style = ref({
+  width: '100%',
+  height: '100%'
+});
+const svg_style = ref({
+  width: '100%',
+  height: '100%'
+});
+const target_node = ref(null);
+const root_full = ref(null);
 
-// const extend = (item) => {
-//   console.log(item);
-//   target_node.value = item.node;
-//   root_full.value = item.full;
-//   progress.value = 0;
-//   timer_progress = setInterval(() => {
-//     if (progress.value < 100) {
-//       progress.value += 1;
-//     }
-//     if (progress.value == 100) {
-//       mindMap_content.value = test_mk_str_extend;
-//     }
-//   }, 30);
-// };
-// onMounted(() => {
-//   mindMap_content.value = test_mk_str;
-// });
+const extend = (item) => {
+  console.log(item);
+  target_node.value = item.node;
+  root_full.value = item.full;
+  setTimeout(() => {
+    // 调用接口返回数据传递过去
+    mindMap_content.value = test_mk_str_extend;
+  }, 1000);
+};
+onMounted(() => {
+  mindMap_content.value = test_mk_str;
+});
 </script>
 <style scoped>
 .box {

+ 4 - 3
src/views/SettingsPassword.vue

@@ -110,11 +110,12 @@ const handleUpdatePassword = () => {
         form.password_current = '';
         form.password_new_1 = '';
         form.password_new_2 = '';
-
-        Message.success(res.msg);
+        const msg = i18n.t('settings.update_success');
+        Message.success(msg);
       } else {
         // 失败
-        Message.warning(res.msg);
+        const msg = i18n.t('settings.update_fail');
+        Message.warning(msg);
       }
 
       loading.value = false;

+ 0 - 0
src/views/conversation/ChatAsideExpand.vue


+ 0 - 12
src/views/conversation/ChatAsideExpandBtn.vue

@@ -1,12 +0,0 @@
-<template>
-  <!-- <img class="img-btn" src="../../assets/chat-aside/aside-btn-expond.png" alt="展开/收起" /> -->
-</template>
-
-<script setup></script>
-<style scoped lang="scss">
-.img-btn {
-  position: absolute;
-  right: 10px;
-  top: 30px;
-}
-</style>

+ 24 - 2
src/views/conversation/ChatAsideMenu.vue

@@ -3,7 +3,7 @@
     <div class="chat-aside-menu" :class="current_theme_klass">
       <div class="menu-content">
         <div class="menu-actions-top">
-          <img class="header-logo-img" :key="logo_key" src="/logo.png" alt="SmartAI Logo" />
+          <img class="header-logo-img" :key="logo_key" src="/logo.png" alt="SmartAI Logo" @click="openWWelcome" />
         </div>
 
         <div class="aside-menu-h-line"></div>
@@ -58,6 +58,8 @@ import ChatAsideAgent from './ChatAsideAgent.vue';
 
 import useOpenWebLink from './use-open-mode-web-link';
 
+const emit = defineEmits(['openWelcome', 'toNewChat']);
+
 const openWebLink = useOpenWebLink();
 
 const should_render = ref(false);
@@ -110,7 +112,16 @@ const agents_list = computed(() => {
 const logo_key = computed(() => platformInformation.logo_key);
 
 const handleMenuClick = (path) => {
-  router.push(path);
+  if (path === '/chat/qa') {
+    router.push({
+      path: '/chat/qa',
+      query: {
+        chat: 'new_chat'
+      }
+    });
+  } else {
+    router.push(path);
+  }
 };
 
 /**
@@ -139,6 +150,16 @@ const handleAgentClick = (item) => {
 const handleAgentDelete = (agent_id) => {
   recentAgents.removeItem(agent_id);
 };
+
+/**
+ * 打开欢迎页面
+ * */
+const openWWelcome = () => {
+  router.push({
+    path: '/chat/qa'
+  });
+  emit('openWelcome');
+};
 </script>
 
 <style lang="css" scoped>
@@ -188,6 +209,7 @@ const handleAgentDelete = (agent_id) => {
 }
 .menu-content .menu-actions-top .header-logo-img {
   width: 30px;
+  cursor: pointer;
 }
 
 .aside-menu-h-line {

+ 1133 - 0
src/views/conversation/ConversationChatContainer copy.vue

@@ -0,0 +1,1133 @@
+<template>
+  <div class="conversation-chat-container">
+    <div class="conversation-chat-main" ref="chat_scrren_container">
+      <DialogHeader class="conversation-chat-header" :agent_name="current_agent_name" :style="style_header" />
+
+      <div class="conversation-chat-content" :style="style_body">
+        <div class="conversation-chat-main-container">
+          <template v-if="show_welcome">
+            <!-- 有会话ID 获取历史记录,显示加载中状态 -->
+            <div v-if="session_id" class="conversation-chat-area history-list-loading">
+              <div class="scroll-left"></div>
+              <div class="history-list-loading-container"><a-spin /></div>
+              <div class="scroll-right"></div>
+            </div>
+
+            <!-- 无会话ID,直接对话,此时可现实欢迎消息:欢迎使用报表合并、欢迎使用文档智能、欢迎使用小数绘图 -->
+            <DialogWelcome v-if="!session_id" class="conversation-chat-area" />
+          </template>
+
+          <DialogList
+            v-if="!show_welcome"
+            ref="list_scroller"
+            class="conversation-chat-area"
+            :height="list_height"
+            :width="chat_width / 2"
+            :agent_id="agent_id"
+            :list="chat_display_list"
+            :suggestion="chat_suggestion"
+            :answer_action_list="answer_action_list"
+            @share="handleChatShare"
+            @submit="handleSuggestSubmit"
+          >
+            <template v-slot:left>
+              <div class="scroll-left"></div>
+            </template>
+            <template v-slot:right>
+              <div class="scroll-right"></div>
+            </template>
+          </DialogList>
+        </div>
+
+        <DialogInput
+          ref="dialog_input"
+          class="conversation-chat-input"
+          :style="input_style"
+          :disabled="input_disabled"
+          :enable_text_input="enable_text_input"
+          :enable_file_upload="enable_file_upload"
+          :file_accept="file_accept"
+          :width="chat_width / 2"
+          @sizeChange="handleInputSizeChange"
+          @submit="handleInputSubmit"
+        >
+          <template v-if="show_input_others_opts" v-slot:top>
+            <DialogInputTop :user_input_form="conversation_params.user_input_form" ref="dialog_input_top" />
+          </template>
+
+          <template v-slot:left>
+            <div class="scroll-left"></div>
+          </template>
+          <template v-slot:right>
+            <div class="scroll-right"></div>
+          </template>
+        </DialogInput>
+      </div>
+
+      <DialogFooter class="conversation-chat-footer" :style="style_footer" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, onUnmounted, computed, nextTick } from 'vue';
+import { useI18n } from 'vue-i18n';
+// import { Message } from '@arco-design/web-vue';
+import { Message } from '../../3rd-libs/arco-vue-libs';
+
+import { jsonStringify } from '../../utils/utils';
+
+import DialogHeader from '../../modules/conversation-chat/DialogHeader.vue';
+import DialogWelcome from '../../modules/conversation-chat/DialogWelcome.vue';
+import DialogList from '../../modules/conversation-chat/DialogList.vue';
+import DialogInput from '../../modules/conversation-chat/DialogInput.vue';
+// import DialogInputAttachedAttrs from '../../modules/conversation-chat/DialogInputAttachedAttrs.vue';
+import DialogInputTop from '../../modules/conversation-chat/DialogInputTop.vue';
+import DialogFooter from '../../modules/conversation-chat/DialogFooter.vue';
+
+import useWebSocketChat from '../../modules/conversation-dialog/use-websocket-chat';
+
+import useFetchEventSource from '../../modules/conversation-dialog/use-sse-chat';
+import { getHistoryLogs, getDialogChatId, uploadFileByAgentInfo } from '../../modules/conversation-dialog/api-dialog';
+
+import formatDialogSendOptions from '../../modules/conversation-dialog/format-dialog-send-options';
+import formatDialogSendSseOptions from '../../modules/conversation-dialog/format-dialog-send-sse-options';
+
+import formatDialogHistory from '../../modules/conversation-dialog/format-dialog-history';
+
+import formatDialogQuestionByClient from '../../modules/conversation-dialog/format-dialog-question-by-client';
+import formatDialogAnswerForLoading from '../../modules/conversation-dialog/format-dialog-answer-for-loading';
+import formatDialogAnswerForException from '../../modules/conversation-dialog/format-dialog-answer-for-exception';
+import formatDialogAnswerFromWebsocketReceiveMessage from '../../modules/conversation-dialog/format-dialog-answer-from-websocket_receive_message';
+import formatDialogAnswerFromSseReceiveMessage from '../../modules/conversation-dialog/format-dialog-answer-from-Sse_receive_message';
+
+import useEleSizeV1 from '../../utils/use-ele-size-v1';
+import useInputClear from './use-input-clear';
+import useInputGetOptions from './use-input-get-options';
+import useScroll2Bottom from './use-scroll-bottom';
+import { IconEye } from '@arco-design/web-vue/es/icon';
+import { file, files } from 'jszip';
+import { getFileType } from '../../modules/form-dynamic/file-accept';
+
+const props = defineProps({
+  /**
+   * 智能体的ID
+   * 此参数必须传递
+   */
+  agent_id: String,
+  /**
+   * 智能体的一些关键信息
+   */
+  CHAT_AGENT_INFOMATION: Object,
+
+  /**
+   * 会话ID
+   * 如果有此参数,则意味着获取该对话的记录内容
+   */
+  session_id: String,
+  /**
+   * 动态表单数据
+   * 根据智能体的agent_id 获取到动态表单数据
+   * */
+  conversation_params: Object
+});
+const emit = defineEmits(['updateSessionId']);
+
+const i18n = useI18n();
+
+/**
+ * input的一些操作方法
+ */
+const inputClearFn1 = useInputClear('dialog_input'); // 主表单清空
+const inputClearFn2 = useInputClear('dialog_input_top'); // 顶部附加表单的数据
+const getInputTopOptions = useInputGetOptions('dialog_input_top'); // 获取附加表单参数的方法
+
+/**
+ * 对话区域滚动到底部
+ */
+const scroll2Bottom = useScroll2Bottom('list_scroller');
+const scroll2ListBottom = () => {
+  nextTick(() => {
+    scroll2Bottom();
+  });
+};
+
+/**
+ * 整个对话区域的一些尺寸定义
+ */
+const header_height = 30;
+const footer_height = 30;
+const input_height = ref(100);
+
+/**
+ * 获取到对话区域的宽度和高度
+ */
+const { width: chat_width, height: chat_height } = useEleSizeV1('chat_scrren_container');
+
+/**
+ * 对话列表区域的高度
+ */
+const list_height = computed(() => {
+  return chat_height.value - header_height - footer_height - input_height.value - 20;
+});
+
+const input_style = computed(() => {
+  return {
+    // height: input_height + 'px'
+  };
+});
+
+const style_header = computed(() => {
+  return { height: header_height + 'px' };
+});
+const style_body = computed(() => {
+  return { height: chat_height - 30 - 30 + 'px' };
+});
+const style_footer = computed(() => {
+  return { height: footer_height + 'px' };
+});
+
+// ------------------------------------------------------------------
+// ------------------------------------------------------------------
+// ------------------------------------------------------------------
+
+/**
+ * 对话数据列表
+ */
+const chat_log_list = ref([]); // 历史对话的数据列表
+const chat_new_list = ref([]); // 新对话的数据列表
+const chat_suggestion = ref([]); // 对话推荐项目
+
+/**
+ * 当前对话使用的ID
+ * 当用户有输入后,点击提交,此时创建新对话,新对话的ID为此ID
+ * 之后用户可以一直使用此ID进行对话。
+ */
+const chat_round_id = ref(null);
+
+/**
+ * 对话发起的次数记录
+ * 用户每发起一次会话,此值会加1
+ */
+const chat_round_sent_count = ref(0);
+
+/**
+ * 新对话,客户端的提问数据
+ * 用以临时保存客户端的提问数据
+ * 每当用户确认进行提问时,将新提问赋给此值
+ * 一旦对话消息通过websocket发送到服务器,此值可清空
+ */
+const chat_new_question = ref({});
+
+/**
+ * 界面展示的对话列表
+ * 当用户从历史会话进入时,历史会话记录有数据,当用户基于历史会话进行对话时,新会话也有数据,
+ * 当用户从agent进入时,仅有新会话,没有历史会话
+ *
+ * 页面渲染的会话列表是 历史会话 + 新会话数据
+ */
+const chat_display_list = computed(() => {
+  return [].concat(chat_log_list.value, chat_new_list.value);
+});
+
+/**
+ * 是否禁用输入组件
+ */
+const input_disabled = ref(false);
+
+/**
+ * 菜单ID编号
+ * 1 报表合并
+ * 2 知识问答
+ * 3 文档智能
+ * 4 智能问答
+ * 5 智能数据
+ * 6 小数绘图
+ * 7 文档出卷
+ * 8 出题组卷
+ * 9 文档报告
+ */
+const current_menu_id = computed(() => {
+  const agent = props.CHAT_AGENT_INFOMATION.agent;
+  const menu_id = agent.menuId / 1;
+  return menu_id;
+});
+const current_agent_name = computed(() => {
+  const agent = props.CHAT_AGENT_INFOMATION.agent;
+  const name = agent.name;
+  return name;
+});
+const current_agent_mode = computed(() => {
+  const agent = props.CHAT_AGENT_INFOMATION.agent;
+  const mode = agent.mode;
+  return mode;
+});
+
+/**
+ * 是否显示欢迎页
+ * 当用户进入对话页面,还未正式开始对话时
+ * 对话区域会显示欢迎内容
+ * 当用户开始对话时,欢迎内容消失,对话列表开始展示
+ */
+const show_welcome = ref(true);
+
+/**
+ * 表单区域:是否显示附加参数表单
+ *
+ * 根据动态表单返回的数据中user_input_form中是否有数据来控制是否显示附加参数表单
+ */
+const show_input_others_opts = computed(() => {
+  if (
+    props.conversation_params &&
+    props.conversation_params.user_input_form &&
+    props.conversation_params.user_input_form.length
+  ) {
+    return true;
+  }
+  return false;
+
+  // const menu_id = current_menu_id.value;
+
+  // /**
+  //  * 7 文档出卷
+  //  * 9 文档报告
+  //  */
+  // if (menu_id === 7 || menu_id === 9) {
+  //   return true;
+  // }
+
+  // return false;
+});
+
+/**
+ * 是否支持文本输入
+ * 返回 true,输入区域将不存在文本输入框
+ * 返回 false,输入区域将可以进行文本输入
+ *
+ *通过conversation_params.retriever_resource.enabled来控制是否支持文本输入
+ */
+const enable_text_input = computed(() => {
+  let enabled = false;
+  if (props.conversation_params && props.conversation_params.retriever_resource) {
+    enabled = props.conversation_params.retriever_resource.enabled;
+  }
+  return enabled;
+});
+
+/**
+ * 对话文本是否必须
+ * 此值如果为 true,则在发起对话之前,会进行 文本的存在性检查
+ * 此值如果为 false,则在发起对话之前,不会进行 文件存在性检查
+ */
+const is_chat_text_required = computed(() => {
+  const is_enable = enable_text_input.value;
+  if (!is_enable) {
+    return false;
+  }
+
+  return true;
+});
+/**
+ * 默认的文本值
+ */
+const default_text_value = computed(() => {
+  const menu_id = current_menu_id.value;
+
+  /**
+   * 某些情况下,无需输入文本
+   * 但是在对话部分,需要展示用户的提问,此时可以给一个默认的文本值
+   * 例如在报表合并情况下,仅需要上传文件,不需要输入文字
+   */
+  if (menu_id === 1) {
+    return '合并Excel';
+  }
+
+  return '';
+});
+
+/**
+ * 表单区域:是否允许文件上传
+ * 返回 ture,输入区域将会有文件上传按钮
+ * 返回 false,输入区域将没有文件上传按钮
+ *
+ * 根据 conversation_params.file_upload.enabled来控制是否允许文件上传
+ */
+const enable_file_upload = computed(() => {
+  let enabled = false;
+  if (props.conversation_params && props.conversation_params.file_upload) {
+    enabled = props.conversation_params.file_upload.enabled;
+  }
+  return enabled;
+});
+
+/**
+ * 对话文件是否必须上传
+ * 此值如果返回 true,则 在对话时,会进行文件检查
+ * 此值如果返回 false,则 在对话时,不会检查文件的存在性
+ */
+const is_chat_file_required = computed(() => {
+  const is_enable = enable_file_upload.value;
+  if (!is_enable) {
+    return false;
+  }
+
+  const menu_id = current_menu_id.value;
+
+  // 报表合并: 文件必须
+  if (menu_id === 1) {
+    // 当第一次会话时,要求必须上传文件
+    // 非第一次,不做要求
+    if (chat_round_sent_count.value === 0) {
+      return true;
+    }
+  }
+
+  // 文档出卷:文件必须上传
+  if (menu_id === 7) {
+    // 当第一次对话时,必须上传文件,
+    // 非第一次,不做要求
+    if (chat_round_sent_count.value === 0) {
+      return true;
+    }
+  }
+
+  return false;
+});
+/**
+ * 此值在 enable_file_upload === true 的前提下有效
+ * 表单区域:文件上传允许的文件类型
+ * 对应表单参数的 accept
+ */
+const file_accept = computed(() => {
+  const menu_id = current_menu_id.value;
+
+  /**
+   * 1 报表合并:上传几个文档,进行合并  允许 .xlsx
+   * 3 文档智能:上传文档,让智能体进行解析 允许: .pdf/.docx
+   * 5 智能数据:上传Excel,生成饼状图 允许 .xlsx
+   * 6 小数绘图:上传图片,描述图片的内容 允许 .png/jpg/jpeg
+   * 9 文档报告: 允许 .docx
+   */
+  if (menu_id === 1) {
+    return '.xlsx';
+  }
+
+  if (menu_id === 3) {
+    /**
+     * 2025-2-8 16:50
+     * 王永新和赵源在测试deepseek时发现 文档智能 能上传的文件类型有限
+     * 要求将accept调整为和dify的 文档 类型一致
+     */
+    // return '.pdf,.docx';
+    return '.txt,.md,.markdown,.pdf,.html,.xlsx,.xls,.docx,.csv,.eml,.msg,.pptx,.ppt,.xml,.epub';
+  }
+
+  // 智能数据
+  if (menu_id === 5) {
+    return '.xlsx,.xls,.csv';
+  }
+
+  if (menu_id === 6) {
+    return '.jpg,.jpeg,.png';
+  }
+
+  if (menu_id === 9) {
+    return '.docx';
+  }
+
+  return '*';
+});
+
+/**
+ * 答案可用的操作按钮
+ * 可用的值有 [] 或者 ['copy']
+ * 后续可能有新值添加
+ */
+const answer_action_list = computed(() => {
+  const menu_id = current_menu_id.value;
+
+  // 报表合并无操作按钮
+  if (menu_id === 1) {
+    return [];
+  }
+
+  return ['copy'];
+});
+
+/**
+ * 内容分享操作
+ * @param chat_id
+ */
+const handleChatShare = (chat_id) => {};
+/**
+ * 在聊天界面点击推荐项
+ * 继续对话
+ */
+const handleSuggestSubmit = (suggest) => {};
+
+/**
+ * 处理页面中的错误消息
+ * @param msg
+ */
+const handleErrorMessage = (msg) => {
+  Message.error({ content: msg, position: 'top' });
+};
+// ---------------------------------------------
+
+/**
+ * 异常处理
+ * 异常处理过程是一个会话模拟的过程
+ * 先在界面上显示加载动画,然后紧接着显示错误提示
+ * 让用户感知到智能体有处理数据的过程
+ * 所以在异常处理阶段,有一些需要处理的关键点,比如欢迎界面的显示(如果在显示,则需要隐藏,否则对话显示不了)
+ * @param exceptionInfo
+ */
+const handleChatRequestionException = (exceptionInfo) => {
+  // 已结束
+  const answerData = formatDialogAnswerForException(props.CHAT_AGENT_INFOMATION, exceptionInfo);
+
+  // 原来的最后一项弹出
+  chat_new_list.value.pop();
+  // 添加新的最后一项
+  chat_new_list.value.push(answerData);
+
+  // 对话已结束:文件上传、文本输入区域放开使用
+  input_disabled.value = false;
+  // 对话区域滚动到底部
+  scroll2ListBottom();
+};
+
+/**
+ * 输入区域的尺寸发生了变化
+ * 整个对话区域的尺寸也需要依据此变化而变化
+ * @param size
+ */
+const handleInputSizeChange = (size) => {
+  const input_width = size.width;
+  // const input_height = size.height;
+
+  setTimeout(() => {
+    input_height.value = size.height;
+  });
+};
+
+/**
+ * 初始化sse相关方法
+ * */
+const { startStream, stopStream } = useFetchEventSource();
+
+/**
+ * 输入已经确认要提交了
+ *
+ * 1. 创建会话 get-chat-id
+ * 2. 文件上传 如果有文件,则需要文件上传,没有文件,则不需要文件上传
+ * 3. 连接socket,socket 发送文本与文件数据内容
+ * 4. 接收 socket 返回的数据项
+ */
+const handleInputSubmit = async (options) => {
+  let ask_text = options.text; // 文本
+  const ask_files = options.files; // 文件列表
+
+  // 用户选定的文件数量
+  const file_total = ask_files.length;
+
+  const is_text_required = is_chat_text_required.value;
+  const is_file_required = is_chat_file_required.value;
+  // 检查对话文字是否必须
+  if (is_text_required) {
+    if (!ask_text) {
+      const msg = i18n.t('conversation.please_input_text');
+
+      handleErrorMessage(msg);
+      return;
+    }
+  } else {
+    ask_text = default_text_value.value;
+  }
+
+  // 检查对话文件是否必须
+  if (is_file_required) {
+    if (file_total === 0) {
+      const msg = i18n.t('conversation.please_upload_file');
+
+      handleErrorMessage(msg);
+      return;
+    }
+  }
+
+  // --------------------------------------------
+  // --------------------------------------------
+  // --------------------------------------------
+  // --------------------------------------------
+  // --------------------------------------------消息发送前的准备工作
+  // 保存附加参数数据,由于下方有一个 inputClearFn2() 方法的执行
+  // 会将所有数据清空,故附件参数需要在清理之前保存
+  const other_options = getInputTopOptions();
+  /**
+   * 当新消息发送时
+   * 意味着新一轮对话的开始
+   * 此时应该将之前一轮对话的信息进行清空
+   *
+   * 整理客户端提问的数据
+   * 组装Question数据
+   */
+  const client_question = formatDialogQuestionByClient(ask_text, ask_files);
+  // 保存用户的提问信息
+  // 当消息正式发出后,将提问数据放入列表中
+  chat_new_question.value = client_question;
+  // 消息即将发送,记录用户的提问数据
+  chat_new_list.value.push(chat_new_question.value);
+  // 客户端答案数据(loading状态),客户端模拟的答案数据,依据此数据展示加载中的动画
+  const answerLoadingMsg = formatDialogAnswerForLoading(props.CHAT_AGENT_INFOMATION);
+  chat_new_list.value.push(answerLoadingMsg);
+  /**
+   * 会话已确认发送
+   * 隐藏欢迎内容,显示对话内容
+   */
+  show_welcome.value = false;
+  // 清空文本数据和文件数据,避免下一次会话冲突
+  // 清空表单里的数据
+  inputClearFn1();
+  inputClearFn2();
+  // 清空可能存在的推荐项目
+  chat_suggestion.value = [];
+  // 清空已保存的提问数据(提问数据已经推入数组,此处无需继续保存)
+  chat_new_question.value = null;
+  // 文件上传、文本输入区域禁用
+  input_disabled.value = true;
+  // 对话区域滚动到底部
+  scroll2ListBottom();
+  // --------------------------------------------
+  // --------------------------------------------
+  // --------------------------------------------
+  // --------------------------------------------
+  // --------------------------------------------
+
+  const agent_id = props.agent_id;
+
+  // 对话分为三个步骤
+  // 1. 创建会话,如果已经创建,则不需要再次创建
+  // 2. 上传文件,如果不存在文件,则无需文件上传
+  // 3. 连接websocket
+
+  /**
+   * 创建会话 get chat id
+   * 判断对话是否已经创建
+   * 如果对话已经创建,则使用已经创建的对话
+   * 否则,创建新对话
+   */
+  if (chat_round_id.value === null) {
+    const session_id = props.session_id;
+    if (session_id) {
+      // 已有会话ID,使用此会话ID
+      chat_round_id.value = session_id;
+    } else {
+      // 无会话ID,创建新会话
+      // 未创建chat,开始创建
+      try {
+        const chatResult = await getDialogChatId(agent_id);
+
+        if (chatResult.code / 1 === 200) {
+          // 成功
+          const res_data = chatResult.data;
+
+          chat_round_id.value = res_data.chat_id;
+        } else {
+          // 失败
+          // 处理异常:直接在界面上显示
+          handleChatRequestionException({
+            message: `code: ${chatResult.code} message: ${chatResult.msg}`
+          });
+
+          return;
+        }
+      } catch (err) {
+        console.log('err-nfsdnakiuwirea7', err);
+
+        // 处理错误
+        handleChatRequestionException({
+          message: err.message
+        });
+
+        return;
+      }
+    }
+  } else {
+    // 已创建chat,使用已创建的会话ID,进入下一步流程
+  }
+
+  /**
+   * 查看动态表单中是否有文件需要上传,如果有,则需要先上传文件
+   * */
+  if (other_options && other_options.files) {
+    const files_obj = other_options.files;
+    const chat_id = chat_round_id.value;
+    const menu_id = current_menu_id.value;
+    for (const key in files_obj) {
+      const upload_files = files_obj[key].map((item) => item.file);
+      const uploadResult = await uploadFileByAgentInfo(agent_id, chat_id, upload_files, menu_id);
+      if (uploadResult && uploadResult.length) {
+        files_obj[key] = files_obj[key].map((file) => {
+          const uploadedFile = uploadResult.find((uploaded) => uploaded.name === file.name);
+          // 如果找到了匹配的文件,则回填 id
+          if (uploadedFile) {
+            file.upload_file_id = uploadedFile.id;
+            file.url = '';
+          }
+          return file;
+        });
+        // 判断如果对应的文件是单文件上传,则显示为对象
+        if (files_obj[key].some((item) => item.limit_1)) {
+          files_obj[key] = files_obj[key][0];
+        }
+      }
+    }
+    other_options.options = {
+      ...other_options.options,
+      ...files_obj
+    };
+  }
+
+  // 文件上传后的信息
+  let files_response = null;
+  /**
+   * 如果存在文件数据,则进行文件上传
+   */
+  if (file_total !== 0) {
+    // 进行文件上传,获取文件信息
+    const chat_id = chat_round_id.value;
+
+    try {
+      const menu_id = current_menu_id.value;
+      const uploadResult = await uploadFileByAgentInfo(agent_id, chat_id, ask_files, menu_id);
+      if (uploadResult.length) {
+        files_response = uploadResult.map((item) => {
+          return {
+            ...item,
+            url: '',
+            type: getFileType(item),
+            transfer_method: 'local_file',
+            upload_file_id: item.id
+          };
+        });
+      }
+      // if (uploadResult.code / 1 === 200) {
+      //   // 文件上传成功
+      //   files_response = uploadResult.data;
+      // } else {
+      //   // 文件上传异常
+      //   handleChatRequestionException({
+      //     message: `code: ${uploadResult.code} \n\n message: ${uploadResult.msg}`
+      //   });
+
+      //   return;
+      // }
+    } catch (err) {
+      console.log('err-fhskafhakfjslkfjl', err);
+
+      handleChatRequestionException({
+        message: err.message
+      });
+
+      return;
+    }
+  }
+
+  // TODO: 重新组装sse参数,调用sse接口问答
+  const query_params = {
+    agent_id,
+    mode: current_agent_mode.value, //智能体类型
+    query: ask_text,
+    files: files_response || [],
+    inputs: other_options ? other_options.options : [],
+    session_id: props.session_id || ''
+  };
+  startStream(query_params, sseOnMessage);
+  return;
+};
+
+/**
+ * sseOnMessage处理函数
+ * */
+const sseOnMessage = (ev) => {
+  // return;
+  // sse流返回的数据
+  const res_data = JSON.parse(ev.data);
+  if (res_data.session_id) {
+    emit('updateSessionId', res_data.session_id);
+  }
+  //sse流式结束
+  if (res_data.event === 'message_end') {
+    stopStream();
+  }
+  //sse错误,显示错误信息
+  if (res_data.event === 'error' || res_data.message === 'error') {
+    handleChatRequestionException({
+      message: res_data.error
+    });
+    return;
+  }
+  // TODO: 将返回数据填充到页面
+
+  // 获取当前对话中最后一个对话数据
+  // 最后一个对话数据为当前正在进行的对话
+  const last_msg = chat_new_list.value[chat_new_list.value.length - 1];
+
+  /**
+   * 整理返回的数据
+   */
+  const currentChat = formatDialogAnswerFromSseReceiveMessage(res_data, last_msg, props.CHAT_AGENT_INFOMATION);
+  const chat_status = currentChat.status;
+  // 处理对话数据
+  const handleChatData = () => {
+    const chat_data = currentChat.data; // 对话相关的数据项
+
+    // 原来的最后一项弹出
+    chat_new_list.value.pop();
+    // 添加新的最后一项
+    chat_new_list.value.push(chat_data);
+
+    // 滚动到底部
+    scroll2ListBottom();
+  };
+
+  if (chat_status === 0) {
+    // 未开始: 此状态不存在,无需处理
+  }
+
+  if (chat_status === 1) {
+    // 此轮对话仍然在进行中
+    handleChatData();
+  }
+
+  if (chat_status === 2) {
+    // 此轮对话已结束
+    handleChatData();
+
+    // 对话已结束:检查是否存在推荐数据
+    const chat_suggest = currentChat.suggest; // 对话相关的推荐数据
+    if (chat_suggest) {
+      chat_suggestion.value = chat_suggest;
+    }
+
+    // 对话已结束:文件上传、文本输入区域放开使用
+    input_disabled.value = false;
+  }
+
+  if (chat_status === 3) {
+    // 此轮对话出错了
+    // TODO: 错误处理
+
+    // 此轮对话已结束
+    handleChatData();
+    // 对话已结束:文件上传、文本输入区域放开使用
+    input_disabled.value = false;
+  }
+};
+
+// /**
+//  * ------------------------------------------------------------
+//  * 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消息正式发送,发送步骤:
+//    * 1. 初始化链接
+//    * 2. 发送消息
+//    */
+
+//   //  初始化websocket连接
+//   const menu_id = current_menu_id.value;
+//   initWSConnection(props.agent_id, chat_id, menu_id);
+//   // 发送socket连接
+//   sendWSMessage(value);
+//   // 统计会话发起的次数
+//   chat_round_sent_count.value = chat_round_sent_count.value + 1;
+//   /**
+//    * websocket消息发送流程完成
+//    * ------------------------------------------------------------
+//    * ------------------------------------------------------------
+//    * ------------------------------------------------------------
+//    */
+// }
+
+// /**
+//  * 接收websocket消息
+//  * @param e
+//  */
+// function handleWSMessage(e) {
+//   const res_data = e.data;
+
+//   // 获取当前对话中最后一个对话数据
+//   // 最后一个对话数据为当前正在进行的对话
+//   const last_msg = chat_new_list.value[chat_new_list.value.length - 1];
+
+//   /**
+//    * 整理返回的数据
+//    */
+//   const currentChat = formatDialogAnswerFromWebsocketReceiveMessage(res_data, last_msg, props.CHAT_AGENT_INFOMATION);
+//   const chat_status = currentChat.status;
+//   const chat_message = currentChat.message;
+
+//   // 处理对话数据
+//   const handleChatData = () => {
+//     const chat_data = currentChat.data; // 对话相关的数据项
+
+//     // 原来的最后一项弹出
+//     chat_new_list.value.pop();
+//     // 添加新的最后一项
+//     chat_new_list.value.push(chat_data);
+
+//     // 滚动到底部
+//     scroll2ListBottom();
+//   };
+
+//   if (chat_status === 0) {
+//     // 未开始: 此状态不存在,无需处理
+//   }
+
+//   if (chat_status === 1) {
+//     // 此轮对话仍然在进行中
+//     handleChatData();
+//   }
+
+//   if (chat_status === 2) {
+//     // 此轮对话已结束
+//     handleChatData();
+
+//     // 对话已结束:检查是否存在推荐数据
+//     const chat_suggest = currentChat.suggest; // 对话相关的推荐数据
+//     if (chat_suggest) {
+//       chat_suggestion.value = chat_suggest;
+//     }
+
+//     // 对话已结束:文件上传、文本输入区域放开使用
+//     input_disabled.value = false;
+//   }
+
+//   if (chat_status === 3) {
+//     // 此轮对话出错了
+//     // TODO: 错误处理
+
+//     // 此轮对话已结束
+//     handleChatData();
+//     // 对话已结束:文件上传、文本输入区域放开使用
+//     input_disabled.value = false;
+//   }
+// }
+
+// /**
+//  * 处理websocket错误
+//  * @param e
+//  */
+// function handleWSError(e) {
+//   console.log('socket-error-8jfdksagjsfdjaklnfsdajkl', e);
+//   handleChatRequestionException({
+//     message: `对话无法继续:WebSocket Error: ${e.target.url}`
+//   });
+// }
+
+// /**
+//  * 处理websocket关闭事件
+//  * @param e
+//  */
+// function handleWSClose(e) {}
+
+onMounted(() => {
+  const agent_id = props.agent_id;
+  const session_id = props.session_id;
+
+  // 组件挂载
+  if (session_id) {
+    // 有历史记录:获取历史记录内容进行展示
+    getHistoryLogs(session_id)
+      .then((res) => {
+        if (res.code / 1 === 200) {
+          const res_data = res.data;
+          // 成功
+          const session_list = res_data.message;
+
+          // 将列表数据进行转化
+          const list = formatDialogHistory(session_list, props.CHAT_AGENT_INFOMATION);
+
+          // 如果历史记录不为空
+          // 则需要展示对话内容
+          // 此时欢迎内容就不需要展示
+          if (list.length !== 0) {
+            show_welcome.value = false;
+            // 滚动到底部
+            scroll2ListBottom();
+          }
+
+          chat_log_list.value = list;
+        } else {
+          // 异常
+          // TODO:处理异常
+          show_welcome.value = false;
+          // handleChatRequestionException({
+          //   message: res.msg
+          // });
+
+          // 使用消息提示,而非界面错误提示
+          handleErrorMessage('历史会话异常:' + res.msg);
+        }
+      })
+      .catch((err) => {
+        console.log('err-mfsjakfjsdkalfjs', err);
+
+        show_welcome.value = false;
+        // handleChatRequestionException({
+        //   message: err.message
+        // });
+
+        handleErrorMessage('历史会话错误:' + err.message);
+      });
+  } else {
+    // 无历史记录:等待创建会话内容(用户开始输入、提交完成),列表区域显示欢迎界面
+  }
+});
+
+// ---------------------------------------------------------
+onUnmounted(() => {
+  // 组件卸载,关闭组件内的websocket连接
+  // closeWSConnect();
+});
+</script>
+
+<style lang="css" scoped>
+.conversation-chat-container {
+  display: flex;
+  justify-content: center;
+
+  margin-left: auto;
+  margin-right: auto;
+
+  height: calc(100% - 52px);
+
+  padding-top: 52px;
+}
+
+.conversation-chat-left {
+  width: 25%;
+  flex-shrink: 0;
+}
+.conversation-chat-right {
+  width: 25%;
+  flex-shrink: 0;
+}
+.conversation-chat-main {
+  flex-grow: 1;
+  height: 100%;
+  /* max-width: 960px; */
+  margin-left: auto;
+  margin-right: auto;
+
+  display: flex;
+  flex-direction: column;
+
+  /* background-color: #aaa; */
+}
+.conversation-chat-content {
+  flex-grow: 1;
+
+  display: flex;
+  flex-direction: column;
+
+  overflow: hidden;
+
+  padding-top: 8px;
+}
+.conversation-chat-main-container {
+  /* height: 80%; */
+  flex-grow: 1;
+}
+.conversation-chat-area {
+  flex-grow: 1;
+  overflow: auto;
+  /* max-width: 960px; */
+}
+
+.history-list-loading {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  min-height: 300px;
+  width: 100%;
+}
+.history-list-loading-container {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.conversation-input-attached {
+  flex-shrink: 0;
+
+  width: 100%;
+
+  margin-left: auto;
+  margin-right: auto;
+
+  /* margin-top: 5px; */
+  margin-bottom: -10px;
+  padding: 5px;
+  border-radius: 10px;
+
+  display: flex;
+}
+.conversation-chat-input {
+  flex-shrink: 0;
+
+  width: 100%;
+
+  margin-left: auto;
+  margin-right: auto;
+
+  margin-top: 5px;
+  margin-bottom: 5px;
+  padding: 5px;
+  border-radius: 10px;
+
+  display: flex;
+}
+
+.scroll-left {
+  width: 25%;
+  flex-shrink: 0;
+}
+
+.scroll-right {
+  width: 25%;
+  flex-shrink: 0;
+}
+</style>

ファイルの差分が大きいため隠しています
+ 654 - 244
src/views/conversation/ConversationChatContainer.vue


+ 65 - 0
src/views/conversation/ConversationChatWordViewer.vue

@@ -0,0 +1,65 @@
+<template>
+  <div class="word-viewer">
+    <div class="word-viewer-header">
+      <div class="word-viewer-header-btns">
+        <a-button class="btn"
+          ><template #icon><icon-copy /></template>复制</a-button
+        >
+        <a-button class="btn"
+          ><template #icon> <icon-download /> </template>下载</a-button
+        >
+        <a-button class="btn" @click="toClose"> <icon-close /></a-button>
+      </div>
+    </div>
+    <div class="word-viewer-body">
+      <Editor :content="edit_content"></Editor>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { computed } from 'vue';
+import Editor from '../../components/Editor.vue';
+const props = defineProps({
+  edit_txt: {
+    type: String,
+    default: ''
+  }
+});
+const edit_content = computed(() => {
+  return props.edit_txt;
+});
+
+const emit = defineEmits(['close']);
+const toClose = () => {
+  emit('close');
+};
+</script>
+<style scoped>
+.word-viewer {
+  width: 100%;
+  height: 100%;
+  background-color: var(--color-bg-1);
+}
+.word-viewer-header {
+  padding: 10px;
+}
+.word-viewer-header-btns {
+  display: flex;
+  justify-content: flex-end;
+  gap: 5px;
+  .btn {
+    background-color: var(--color-bg-2);
+    padding: 3px 6px;
+    border-radius: 8px;
+  }
+  .btn:hover {
+    background-color: var(--color-fill-3);
+  }
+}
+.word-viewer-body {
+  width: 100%;
+  height: 100%;
+  text-align: left;
+}
+</style>

+ 5 - 3
vite.config.mjs

@@ -35,7 +35,9 @@ export default defineConfig(({ mode }) => {
       proxy: {
         '/api/': {
           target: 'http://smartai.com:8401',
-          // target: 'http://192.168.20.119:9303',
+
+          // target: 'http://192.168.20.119:9201',
+          // target: 'http://192.168.20.72:9201',
           changeOrigin: true,
           secure: false,
           ws: true,
@@ -51,8 +53,8 @@ export default defineConfig(({ mode }) => {
           rewrite: (path) => path.replace(/^\/dify/, '')
         },
         '/rgflow': {
-          target: 'http://192.168.20.119:28002/',
-          // target: 'http://smartai.com:8294/',
+          // target: 'http://192.168.20.119:28002/',
+          target: 'http://smartai.com:8294/',
           changeOrigin: true,
           secure: false,
           ws: true,

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません