ChatBox.js 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127
  1. import LoginModal from "@/components/LoginModal";
  2. import MarkdownIt from "markdown-it";
  3. import { pcInnerAi,getMinioURl } from "@/api/api";
  4. import {modelList,listBuckets,selectTypeList,getBucketContents,configSave,configList,configDelete,application_types,set_default,configurationInfo} from "@/api/knowledge"
  5. import axios from 'axios';
  6. import { type } from "jquery";
  7. const md = new MarkdownIt();
  8. export default {
  9. components: {
  10. LoginModal,
  11. },
  12. data() {
  13. return {
  14. applicationDialogVisible: false,
  15. isThinking: false,
  16. thinkingDots: '',
  17. messages: [
  18. {
  19. user: "bot",
  20. messageType: "TEXT",
  21. message: "欢迎使用智能AI助理",
  22. html: "",
  23. time: "",
  24. done: true,
  25. },
  26. ],
  27. generating: false,
  28. userInput: "",
  29. websocket: null,
  30. wsUrl:'http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false',
  31. tweaks:{},
  32. showLoginModal: false,
  33. isLoggedIn: false, // 添加登录状态标志
  34. idArray: [],
  35. minioUrls: [], // 新增:用于存储 Minio URL
  36. AImodel:'1',//模型
  37. AIknowledgeBase:"1",//知识库
  38. AIFile:'1',//文件
  39. AIform:{
  40. chat_name:'',
  41. modelLibrary: 'ollama',
  42. model_type: 'chat',
  43. model_name: '',
  44. knowledge_base_names: [],
  45. document_directories: [],
  46. documents: [],
  47. temperature: 0.7,
  48. max_tokens: 150,
  49. top_p: 1.0,
  50. frequency_penalty: 0.0,
  51. presence_penalty: 0.0,
  52. response_format: "text",
  53. context_window: 2048,
  54. user_id: "user123",
  55. session_id: "session456",
  56. language: "en",
  57. timeout: 30,
  58. role_name: "Admin",
  59. role_description: "",
  60. role_permissions: "",
  61. custom_variables:"",
  62. custom_prompt:'',
  63. application_type: "",
  64. is_default: false,
  65. generate_new_api_key: true
  66. },
  67. /* 模型库 */
  68. modelList:[],
  69. /* 模型类型 */
  70. modelTypeList:[],
  71. /* 模型名称 */
  72. modelNameList:[],
  73. /* 知识库 */
  74. kneList:[],
  75. /* 文档目录 */
  76. directoryList: [],
  77. /* 文档 */
  78. documentList: [],
  79. bucket_id:'',
  80. rules: {
  81. chat_name: [
  82. { required: true, message: '请填写应名称', trigger: 'blur' }
  83. ],
  84. model_name: [
  85. { required: true, message: '请选择模型名称', trigger: 'change' }
  86. ],
  87. knowledge_base_names: [
  88. { required: true, message: '请选择至少一个知识库', trigger: 'change' }
  89. ],
  90. /* document_directories: [
  91. { required: true, message: '请选择文档目录', trigger: 'change' }
  92. ], */
  93. documents: [
  94. { required: true, message: '请选择至少一个文档', trigger: 'change' }
  95. ]
  96. },
  97. chatDialogVisible: false,
  98. /* 应用列表 */
  99. knowledgeBases: [],
  100. currentChat: {},
  101. /* 修改弹窗 */
  102. editDialogVisible: false,
  103. editForm: {
  104. // 复制 AIform 的结构,但初始值为空
  105. chat_name: '',
  106. modelLibrary: 'ollama',
  107. model_type: 'chat',
  108. model_name: '',
  109. knowledge_base_names: [],
  110. document_directories: [],
  111. documents: [],
  112. temperature: 0.7,
  113. max_tokens: 150,
  114. top_p: 1.0,
  115. frequency_penalty: 0.0,
  116. presence_penalty: 0.0,
  117. response_format: "text",
  118. context_window: 2048,
  119. user_id: "user123",
  120. session_id: "session456",
  121. language: "en",
  122. timeout: 30,
  123. role_name: "Admin",
  124. role_description: "",
  125. role_permissions:"['read', 'write', 'delete']",
  126. custom_variables:"",
  127. custom_prompt:'',
  128. application_type: "",
  129. is_default: false,
  130. generate_new_api_key: true
  131. },
  132. editDirectoryList: [],
  133. editDocumentList: [],
  134. /* 查看 */
  135. viewDialogVisible: false,
  136. viewForm: null,
  137. session_id:"",
  138. /* 配置类型列表 */
  139. appTypeList:[]
  140. };
  141. },
  142. created() {
  143. // this.connectWebSocket();
  144. },
  145. mounted() {
  146. /* */
  147. if(this.$route.name=='ai'){
  148. /* 外部知识库 */
  149. const AIData=JSON.parse(sessionStorage.getItem("AIData"))
  150. this.wsUrl=AIData.data.url
  151. this.tweaks=AIData.data.tweaks
  152. }else{
  153. /* 内部知识库 */
  154. pcInnerAi().then(res=>{
  155. if(res.status!==200) return
  156. this.wsUrl=res.data.url
  157. this.tweaks=res.data.tweaks
  158. })
  159. }
  160. /* 获取模型列表 */
  161. this.init()
  162. },
  163. computed: {
  164. getKnowledgeBaseNames() {
  165. const names = this.AIform.knowledge_base_names.map(id =>
  166. this.kneList.find(item => item.id === id)?.name || id
  167. );
  168. if (names.length <= 2) {
  169. return names.join(', ');
  170. } else {
  171. return `${names[0]}, ${names[1]} 等${names.length}个`;
  172. }
  173. },
  174. getDocumentNames() {
  175. const names = this.AIform.documents.map(id =>
  176. this.documentList.find(item => item.id === id)?.name || id
  177. );
  178. if (names.length <= 2) {
  179. return names.join(', ');
  180. } else {
  181. return `${names[0]}, ${names[1]} 等${names.length}个`;
  182. }
  183. },
  184. isDocumentSelectDisabled() {
  185. return !this.AIform.knowledge_base_names.length ||this.AIform.document_directories=='';
  186. }
  187. },
  188. methods: {
  189. getKnowledgeBaseName(kbId) {
  190. const kb = this.kneList.find(kb => kb.id === kbId);
  191. return kb ? kb.name : kbId;
  192. },
  193. /* 检索知识库名称 */
  194. getKnowledgeBaseNamesForView(knowledgeBaseIds) {
  195. return knowledgeBaseIds.map(id => {
  196. const knowledgeBase = this.kneList.find(kb => kb.id === id);
  197. return knowledgeBase ? knowledgeBase.name : id;
  198. }).join(", ");
  199. },
  200. /* 设为默认 */
  201. setDefault(val){
  202. set_default({id:val.id}).then(res=>{
  203. if(res.status!==200) return
  204. this.$message.success('设为默认成功!')
  205. this.init()
  206. })
  207. },
  208. /* 重置表单 */
  209. resetForm() {
  210. if (this.$refs.AIformRef) {
  211. this.$refs.AIformRef.resetFields();
  212. }
  213. this.AIform = {
  214. chat_name: '',
  215. modelLibrary: 'ollama',
  216. model_type: 'chat',
  217. model_name: '',
  218. knowledge_base_names: [],
  219. document_directories: [],
  220. documents: [],
  221. temperature: 0.7,
  222. max_tokens: 150,
  223. top_p: 1.0,
  224. frequency_penalty: 0.0,
  225. presence_penalty: 0.0,
  226. response_format: "",
  227. context_window: 2048,
  228. user_id: "",
  229. session_id: "",
  230. language: "en",
  231. timeout: 30,
  232. role_name: "Admin",
  233. role_description: "",
  234. role_permissions: "['read', 'write', 'delete']",
  235. custom_variables:"",
  236. custom_prompt:'',
  237. application_type: "",
  238. is_default: false,
  239. generate_new_api_key: true
  240. };
  241. },
  242. /* 新增聊天应用 */
  243. addApplication() {
  244. this.resetForm();
  245. /*console.log(this.AIform); */
  246. this.$router.push({
  247. path: "/knowledge/appConfig/index",
  248. query: { type:'add' },
  249. });
  250. /* this.applicationDialogVisible = true; */
  251. },
  252. handleCloseApplicationDialog(done) {
  253. this.$confirm('确认关闭?未保存的更改将会丢失。')
  254. .then(_ => {
  255. this.resetForm();
  256. done();
  257. })
  258. .catch(_ => {});
  259. },
  260. cancelApplication() {
  261. this.$confirm('确认取消?未保存的更改将会丢失。')
  262. .then(_ => {
  263. this.resetForm();
  264. this.applicationDialogVisible = false;
  265. })
  266. .catch(_ => {});
  267. },
  268. /* 查看 */
  269. viewApplication(card) {
  270. /* configurationInfo({id:card.id}).then(res=>{
  271. console.log(res);
  272. }) */
  273. this.viewForm = { ...card };
  274. this.$router.push({
  275. path: "/knowledge/appConfig/index",
  276. query: { id: card.id,card:card,type:'view' },
  277. });
  278. /* this.viewDialogVisible = true; */
  279. },
  280. /* 删除 */
  281. deleteApplication(card) {
  282. this.$confirm('确定要删除这个应用吗?', '提示', {
  283. confirmButtonText: '确定',
  284. cancelButtonText: '取消',
  285. type: 'warning'
  286. }).then(() => {
  287. // 调用删除 API
  288. configDelete({id:card.id}).then(response => {
  289. if (response.status==200) {
  290. this.$message({
  291. type: 'success',
  292. message: '删除成功!'
  293. });
  294. // 从列表中移除已删除的应用
  295. /* const index = this.knowledgeBases.findIndex(kb => kb.id === card.id);
  296. if (index > -1) {
  297. this.knowledgeBases.splice(index, 1);
  298. } */
  299. this.fetchApplicationList();
  300. } else {
  301. this.$message.error('删除失败: ' + response.message);
  302. }
  303. }).catch(error => {
  304. console.error('删除应用时出错:', error);
  305. this.$message.error('删除失败,请稍后重试');
  306. });
  307. }).catch(() => {
  308. this.$message({
  309. type: 'info',
  310. message: '已取消删除'
  311. });
  312. });
  313. },
  314. handleEditKnowledgeBaseChange(val) {
  315. // 重置目录和文档选择
  316. this.editForm.document_directories = [];
  317. this.editForm.documents = [];
  318. this.editDocumentList = [];
  319. // 加载新选择的知识库对应的目录列表
  320. this.loadEditDirectoryList(val);
  321. },
  322. handleEditDirectoryChange(val) {
  323. // 重置文档选择
  324. this.editForm.documents = [];
  325. // 加载选中目录对应的文档列表
  326. this.loadEditDocumentList(val);
  327. },
  328. /* 编辑 */
  329. editApplication(card) {
  330. // 根据 card 的数据填充 editForm
  331. this.editForm = JSON.parse(JSON.stringify(card)); // 深拷贝以避免直接修改原对象
  332. // 处理 knowledge_base_names
  333. this.editForm.knowledge_base_names = this.editForm.knowledge_base_names.map(name => {
  334. const kb = this.kneList.find(kb => kb.name === name);
  335. return kb ? kb.id : name; // 如果找不到对应的知识库,保留原名称
  336. });
  337. // 处理 role_permissions
  338. if (this.editForm.role_permissions) {
  339. if (typeof this.editForm.role_permissions !== 'string') {
  340. try {
  341. this.editForm.role_permissions = JSON.stringify(this.editForm.role_permissions);
  342. } catch (error) {
  343. console.error('Error stringifying role_permissions:', error);
  344. this.editForm.role_permissions = '';
  345. }
  346. }
  347. } else {
  348. this.editForm.role_permissions = '';
  349. }
  350. // 处理 custom_variables
  351. if (this.editForm.custom_variables) {
  352. this.editForm.custom_variables = JSON.stringify(this.editForm.custom_variables);
  353. } else {
  354. this.editForm.custom_variables = "";
  355. }
  356. this.$router.push({
  357. path: "/knowledge/appConfig/index",
  358. query: { id: card.id,card:card ,type:'edit'},
  359. });
  360. /* this.editDialogVisible = true; */
  361. console.log(this.editForm);
  362. // 加载知识库对应的目录列表
  363. this.loadEditDirectoryList(this.editForm.knowledge_base_names);
  364. },
  365. handleEditDialogClose() {
  366. this.$refs.editFormRef.resetFields();
  367. this.editDirectoryList = [];
  368. this.editDocumentList = [];
  369. },
  370. async loadEditDirectoryList(val) {
  371. this.editDirectoryList = []; // 清空现有目录列表
  372. let totalDocuments = 0;
  373. let otherFolderCount = 0;
  374. for (const kbName of val) {
  375. // 根据知识库名称找到对应的 ID
  376. const kbId = this.kneList.find(kb => kb.name === kbName)?.id;
  377. /* if (!kbId) {
  378. console.error(`No matching knowledge base found for name: ${kbName}`);
  379. continue;
  380. } */
  381. const typeForm = {
  382. page: 1,
  383. pageSize: 9999,
  384. kb_id: kbName,
  385. };
  386. try {
  387. const res = await selectTypeList(typeForm);
  388. if (res.data) {
  389. this.editDirectoryList = [...new Set([...this.editDirectoryList, ...res.data.dataList])];
  390. res.data.dataList.forEach(folder => {
  391. if (folder.id === "other") {
  392. otherFolderCount += folder.document_count || 0;
  393. } else {
  394. totalDocuments += folder.document_count || 0;
  395. }
  396. });
  397. }
  398. } catch (error) {
  399. console.error(`Error loading directory list for kb_id ${kbId}:`, error);
  400. }
  401. }
  402. this.editDirectoryList.unshift({
  403. id: "001",
  404. name: "全部",
  405. document_count: totalDocuments + otherFolderCount,
  406. });
  407. },
  408. loadEditDocumentList(val) {
  409. // 找到选中目录的名称
  410. const selectedDirectoryName = val;
  411. const id= this.directoryList.find(el=>el.id==val)
  412. // 从 kneList 中找到对应的知识库
  413. const selectedKnowledgeBase = this.kneList.find(kb =>
  414. kb.id === this.editForm.knowledge_base_names[0]
  415. );
  416. /* if (!selectedKnowledgeBase) {
  417. console.error('No matching knowledge base found');
  418. return;
  419. } */
  420. let queryForm = {
  421. page: 1,
  422. pageSize: 9999,
  423. bucket_id:selectedKnowledgeBase.id,
  424. doc_type_id: selectedDirectoryName === '001' ? '' : this.getDirectoryIdByName(selectedDirectoryName),
  425. };
  426. getBucketContents(queryForm).then(res => {
  427. this.editDocumentList = res.data.documents;
  428. });
  429. },
  430. // 添加一个辅助方法来根据目录名称获取目录ID
  431. getDirectoryIdByName(directoryName) {
  432. const directory = this.editDirectoryList.find(dir => dir.name === directoryName);
  433. return directory ? directory.id : null;
  434. },
  435. submitEdit() {
  436. this.$refs.editFormRef.validate(async (valid) => {
  437. if (valid) {
  438. try {
  439. const convertedForm = { ...this.editForm };
  440. console.log(this.editForm.documents);
  441. // 转换知识库、文档目录和文档的ID为名称
  442. convertedForm.knowledge_base_names = this.safeGetNamesByIds(this.editForm.knowledge_base_names, this.kneList);
  443. /* convertedForm.documents = this.safeGetNamesByIds(this.editForm.documents, this.editDocumentList); */
  444. convertedForm.document_directories = this.safeGetNamesByIds(this.editForm.document_directories, this.editDirectoryList);
  445. if (convertedForm.document_directories.length === 0) {
  446. convertedForm.document_directories = ['全部'];
  447. }
  448. console.log('Converted form for edit:', convertedForm);
  449. const response = await axios.post(`${process.env.VUE_APP_BASE_API}/chatbot/configuration/update/`, convertedForm, {
  450. headers: {
  451. 'Content-Type': 'application/json'
  452. }
  453. });
  454. if (response.status === 200) {
  455. this.$message.success('应用更新成功');
  456. this.editDialogVisible = false;
  457. this.fetchApplicationList();
  458. } else {
  459. this.$message.error(response.data.message || '应用更新失败');
  460. }
  461. } catch (error) {
  462. console.error('Error updating application:', error);
  463. this.$message.error('应用更新失败,请稍后重试');
  464. }
  465. } else {
  466. this.$message.error('请填写所有必填字段');
  467. return false;
  468. }
  469. });
  470. },
  471. // 添加一个方法来获取完整的应用配置
  472. async fetchFullApplicationConfig(appId) {
  473. try {
  474. const response = await axios.get(`${process.env.VUE_APP_BASE_API}/chatbot/configDetail/${appId}`);
  475. if (response.status === 200 && response.data.code === 200) {
  476. return response.data.data;
  477. } else {
  478. throw new Error(response.data.message || '获取应用配置失败');
  479. }
  480. } catch (error) {
  481. console.error('Error fetching application config:', error);
  482. this.$message.error('获取应用配置失败,请稍后重试');
  483. return null;
  484. }
  485. },
  486. // 实现获取应用列表的方法
  487. async fetchApplicationList() {
  488. try {
  489. const response = await configList();
  490. if (response.status === 200) {
  491. this.knowledgeBases = response.data; // 假设返回的数据结构符合要求
  492. } else {
  493. throw new Error(response.data.message || '获取应用列表失败');
  494. }
  495. } catch (error) {
  496. console.error('Error fetching application list:', error);
  497. this.$message.error('获取应用列表失败,请稍后重试');
  498. }
  499. },
  500. /* */
  501. openChatDialog(card) {
  502. this.currentChat = card;
  503. this.chatDialogVisible = true;
  504. // 可能需要在这里初始化聊天记录或执行其他操作
  505. this.messages = []; // 清空之前的聊天记录
  506. // 可以添加一个欢迎消息
  507. this.messages.push({
  508. user: "bot",
  509. messageType: "TEXT",
  510. message: `欢迎使用 ${card.chat_name} 聊天应用`,
  511. html: "",
  512. time: "",
  513. done: true,
  514. });
  515. },
  516. /* 关闭 */
  517. handleCloseDialog(done) {
  518. // 在这里可以添加关闭前的确认逻辑
  519. this.$confirm('确认关闭?')
  520. .then(_ => {
  521. done();
  522. })
  523. .catch(_ => {});
  524. },
  525. /* 点击应用 */
  526. getFileTypeIcon(type) {
  527. const iconMap = {
  528. word: 'el-icon-document',
  529. excel: 'el-icon-tickets',
  530. pdf: 'el-icon-document-copy'
  531. };
  532. return iconMap[type] || 'el-icon-document';
  533. },
  534. /* 生成引用 */
  535. generateApplication() {
  536. this.$refs.AIformRef.validate(async (valid) => {
  537. if (valid) {
  538. try {
  539. // 创建一个新对象来存储转换后的数据
  540. const convertedForm = { ...this.AIform };
  541. // 将知识库 ID 转换为名称
  542. convertedForm.knowledge_base_names = this.safeGetNamesByIds(this.AIform.knowledge_base_names, this.kneList);
  543. // 将文档 ID 转换为名称
  544. convertedForm.documents = this.safeGetNamesByIds(this.AIform.documents, this.documentList);
  545. // 将文档目录 ID 转换为名称,如果为空则传递 '全部'
  546. convertedForm.document_directories = this.safeGetNamesByIds(this.AIform.document_directories, this.directoryList);
  547. if (convertedForm.document_directories.length === 0) {
  548. convertedForm.document_directories = ['全部'];
  549. }
  550. console.log('Converted form:', convertedForm);
  551. // 使用 axios 发送 POST 请求
  552. const response = await axios.post(`${process.env.VUE_APP_BASE_API}/chatbot/configCreart/`, convertedForm, {
  553. headers: {
  554. 'Content-Type': 'application/json'
  555. }
  556. });
  557. if (response.status === 200) {
  558. this.$message.success('应用生成成功');
  559. this.resetForm();
  560. this.fetchApplicationList();
  561. this.applicationDialogVisible = false; // 关闭弹窗
  562. // 可选:重定向到新应用或更新 UI
  563. } else {
  564. this.$message.error(response.data.message || '应用生成失败');
  565. }
  566. } catch (error) {
  567. console.error('Error generating application:', error);
  568. this.$message.error('应用生成失败,请稍后重试');
  569. }
  570. } else {
  571. this.$message.error('请填写所有必填字段');
  572. return false;
  573. }
  574. });
  575. },
  576. // 安全的辅助方法:通过 ID 数组获取名称数组
  577. safeGetNamesByIds(ids, list) {
  578. if (!Array.isArray(ids)) {
  579. console.warn('Expected an array of ids, but received:', ids);
  580. return [];
  581. }
  582. return ids.map(id => {
  583. if (id === '001') {
  584. return '全部';
  585. }
  586. const item = list.find(item => item.id === id);
  587. return item ? item.name : '';
  588. }).filter(name => name !== '');
  589. },
  590. /* 监听联动 */
  591. handleKnowledgeBaseChange(val) {
  592. // 重置目录和文档选择
  593. this.AIform.document_directories = [];
  594. this.AIform.documents = [];
  595. this.documentList = [];
  596. this.bucket_id=val[0]
  597. // 根据选中的知识库加载目录列表
  598. // 这里需要调用后端 API 来获取目录列表
  599. this.loadDirectoryList(val);
  600. },
  601. handleDirectoryChange(val) {
  602. // 重置文档选择
  603. this.AIform.documents = [];
  604. // 根据选中的目录加载文档列表
  605. // 这里需要调用后端 API 来获取文档列表
  606. this.loadDocumentList(val);
  607. },
  608. async loadDirectoryList(val) {
  609. this.directoryList = []; // 清空现有目录列表
  610. let totalDocuments = 0;
  611. let otherFolderCount = 0;
  612. for (const kbId of val) {
  613. const typeForm = {
  614. page: 1,
  615. pageSize: 9999,
  616. kb_id: kbId,
  617. };
  618. try {
  619. const res = await selectTypeList(typeForm);
  620. // 假设 res.data 包含目录列表
  621. if (res.data) {
  622. // 将新的目录添加到列表中,避免重复
  623. this.directoryList = [...new Set([...this.directoryList, ...res.data.dataList])];
  624. console.log(res.data.dataList);
  625. // 计算总文档数和其他文件夹数量
  626. res.data.dataList.forEach(folder => {
  627. if (folder.id === "other") {
  628. otherFolderCount += folder.document_count || 0;
  629. } else {
  630. totalDocuments += folder.document_count || 0;
  631. }
  632. });
  633. }
  634. } catch (error) {
  635. console.error(`Error loading directory list for kb_id ${kbId}:`, error);
  636. // 可以在这里添加错误处理,比如显示一个错误提示
  637. }
  638. }
  639. // 在列表开头插入"全部"选项
  640. this.directoryList.unshift({
  641. id: "001",
  642. name: "全部",
  643. /* document_count: totalDocuments + otherFolderCount, */
  644. });
  645. },
  646. loadDocumentList(val) {
  647. const id= this.directoryList.find(el=>el.id==val)
  648. let queryForm={
  649. page: 1,
  650. pageSize: 9999,
  651. bucket_id:val=='001'?this.bucket_id : id.kb_id,//this.bucket_id,
  652. doc_type_id: val=='001'?'' :val,
  653. }
  654. getBucketContents(queryForm).then(res=>{
  655. this.documentList=res.data.documents
  656. })
  657. },
  658. /* 聊天记录 */
  659. selectChat(index) {
  660. this.currentChatIndex = index;
  661. // Load the selected chat messages
  662. // This is where you would typically load the messages for the selected chat
  663. },
  664. newChat() {
  665. this.chatHistory.push({ title: `聊天 ${this.chatHistory.length + 1}`, preview: "新的聊天..." });
  666. this.currentChatIndex = this.chatHistory.length - 1;
  667. // Clear the current messages and start a new chat
  668. this.messages = [];
  669. },
  670. handleLinkClick(event) {
  671. if (event.target.tagName.toLowerCase() === "a") {
  672. event.preventDefault();
  673. // 获取文件名
  674. const fullFileName = event.target.textContent.trim();
  675. // 分离文件名和后缀
  676. const lastDotIndex = fullFileName.lastIndexOf(".");
  677. const fileName =
  678. lastDotIndex > -1
  679. ? fullFileName.slice(0, lastDotIndex)
  680. : fullFileName;
  681. const fileExtension =
  682. lastDotIndex > -1 ? fullFileName.slice(lastDotIndex + 1) : "";
  683. // 编码文件名和后缀以便于URL传参
  684. const encodedFileName = encodeURIComponent(fileName);
  685. const encodedFileExtension = encodeURIComponent(fileExtension);
  686. // 构建新的URL,包含文件名和后缀作为查询参数
  687. const newUrl = `#/preview?fileName=${encodedFileName}&fileExtension=${encodedFileExtension}`;
  688. try {
  689. localStorage.setItem("href", event.target.href);
  690. } catch (e) {
  691. console.warn("无法存储 URL 到 localStorage:", e);
  692. }
  693. // 使用新的URL打开窗口
  694. const newWindow = window.open(newUrl, "_blank");
  695. if (newWindow) {
  696. newWindow.focus();
  697. } else {
  698. alert("弹出窗口被阻止,请允许此网站的弹出窗口。");
  699. }
  700. }
  701. },
  702. handleLoginSuccess() {
  703. this.showLoginModal = false;
  704. this.isLoggedIn = true;
  705. // 登录成功后,可以继续发送消息
  706. this.sendMessage();
  707. },
  708. getUuid() {
  709. return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
  710. /[xy]/g,
  711. function (c) {
  712. var r = (Math.random() * 16) | 0,
  713. v = c == "x" ? r : (r & 0x3) | 0x8;
  714. return v.toString(16);
  715. }
  716. );
  717. },
  718. connectWebSocket() {
  719. const wsUrl = 'http://58.246.234.210:7860/api/v1/run/2f1faff2-99df-4821-87d6-43d693084c4b?stream=false'
  720. // 这里做一个简单的鉴权,只有符合条件的鉴权才能握手成功
  721. // 移除这里的fetch调用,改用WebSocket
  722. this.websocket = new WebSocket(wsUrl);
  723. /* this.websocket.onerror = (event) => {
  724. console.error("WebSocket 连接错误:", event);
  725. };
  726. */
  727. this.websocket.onclose = (event) => {
  728. console.log("WebSocket 连接关闭:", event);
  729. };
  730. this.websocket.onopen = (event) => {
  731. console.log("WebSocket 连接已建立:", event);
  732. };
  733. this.websocket.onmessage = (event) => {
  734. // 解析收到的消息
  735. const result = JSON.parse(event.data);
  736. // 检查消息是否完结
  737. if (result.done) {
  738. this.messages[this.messages.length - 1].done = true;
  739. return;
  740. }
  741. if (this.messages[this.messages.length - 1].done) {
  742. // 添加新的消息
  743. this.messages.push({
  744. time: Date.now(),
  745. message: result.content,
  746. messageType: "TEXT",
  747. user: "bot",
  748. done: false,
  749. });
  750. } else {
  751. // 更新最后一条消息
  752. let lastMessage = this.messages[this.messages.length - 1];
  753. lastMessage.message += result.content;
  754. this.messages[this.messages.length - 1] = lastMessage;
  755. }
  756. };
  757. },
  758. async sendMessage() {
  759. if(this.$route.name=='ai'){
  760. if(!sessionStorage.getItem('AIData')){
  761. this.showLoginModal=true
  762. return
  763. }
  764. }
  765. const chatId = this.getUuid();
  766. const wsUrl =`${process.env.VUE_APP_BASE_API}/chatbot/chat`//this.wsUrl//'http://58.246.234.210:7860/api/v1/run/3ef7369e-b617-40d2-9e2e-54f3ee76ed2e?stream=false'
  767. let message = this.userInput.trim();
  768. if (message) {
  769. // 添加用户消息
  770. this.messages.push({
  771. time: Date.now(),
  772. message: message.replace(/(\r\n|\r|\n)/g, " \n"),
  773. messageType: "TEXT",
  774. user: "user",
  775. done: true,
  776. });
  777. this.userInput = "";
  778. // 添加机器人的响应消息
  779. const botMessage = {
  780. user: "bot",
  781. messageType: "TEXT",
  782. message: "",
  783. html: "",
  784. time: Date.now(),
  785. done: false,
  786. };
  787. this.messages.push(botMessage);
  788. // 开始思考动画
  789. const thinkingController = new AbortController();
  790. const thinkingPromise = this.simulateThinking(
  791. botMessage,
  792. thinkingController.signal
  793. );
  794. // 通过 HTTP 发送消息
  795. try {
  796. ///send-message
  797. const response = await fetch(`${wsUrl}`, {
  798. method: "POST",
  799. headers: {
  800. "Content-Type": "application/json"
  801. },
  802. /* mode: 'no-cors', */
  803. /* credentials: "include", */
  804. body: JSON.stringify({
  805. /* chatId: chatId, */
  806. message: message,
  807. /* output_type: "chat",
  808. input_type: "chat", */
  809. chat_config_id:this.currentChat.id,
  810. user_id:this.$store.state.user.id,
  811. session_id:this.session_id||''
  812. /* tweaks:this.tweaks {
  813. "ChatInput-U82Vu": {},
  814. "Prompt-b8Z1E": {},
  815. "OllamaModel-z9SVx": {},
  816. "Milvus-l0vvr": {},
  817. "OllamaEmbeddings-4Ml5q": {},
  818. "ParseData-LM8yW": {},
  819. "ParseData-jVoZg": {},
  820. "APIRequest-OnZHl": {},
  821. "ParseData-fH1vY": {},
  822. "ChatOutput-ybzb9": {}
  823. } */,
  824. }),
  825. });
  826. // 停止思考动画
  827. thinkingController.abort();
  828. await thinkingPromise;
  829. const result = await response.json();
  830. this.session_id=result.data.session_id
  831. console.log(result.status);
  832. if (result.code !== 200) {
  833. const errorText = await response.text(); // 获取错误文本
  834. throw new Error(
  835. `HTTP error! status: ${response.status}, message: ${errorText}`
  836. );
  837. }
  838. // 等待思考动画完成
  839. await thinkingPromise;
  840. if(this.$route.name !== 'ai'){
  841. // 提取 additional_input 数据
  842. const additionalInput = result.data.milvus_ids//outputs?.[0]?.outputs?.[0]?.results?.message?.data?.additional_input;
  843. // 处理字符串,提取 ID 值
  844. this.idArray = additionalInput/* .split('\n') // 按换行符分割
  845. .map(line => line.trim()) // 去除每行首尾空白 */
  846. /* .filter(line => line.startsWith('ID:')) // 只保留以 'ID:' 开头的行
  847. .map(line => line.substring(3).trim()); // 提取 'ID:' 后面的内容并去除空白 */
  848. // 创建符合要求格式的对象
  849. const idObject = { ids: this.idArray };
  850. console.log("ID Object:", idObject);
  851. // 调用方法来获取 Minio URL,传递新的 idObject
  852. await this.getMinioUrls(idObject);
  853. }
  854. this.handleResponse(result, botMessage);
  855. } catch (error) {
  856. console.error("Error sending message:", error);
  857. // 如果发生错误,停止思考动画
  858. botMessage.done = true;
  859. botMessage.message = "抱歉,发生了错误,请稍后再试。";
  860. }
  861. }
  862. },
  863. async getMinioUrls(idObject) {
  864. this.minioUrls = []; // 清空之前的 URL
  865. try {
  866. console.log("Sending to backend:", JSON.stringify(idObject));
  867. const response = await axios.post(`${process.env.VUE_APP_BASE_API}/milvus/getMinioURl`, idObject, {
  868. headers: {
  869. 'Content-Type': 'application/json'
  870. }
  871. });
  872. if (response.status === 200 && response.data) {
  873. this.minioUrls = response.data.data; // 假设返回的是 URL 数组
  874. console.log("Received Minio URLs:", this.minioUrls);
  875. } else {
  876. console.error('Failed to get Minio URLs');
  877. }
  878. } catch (error) {
  879. console.error('Error fetching Minio URLs:', error);
  880. if (error.response) {
  881. console.error('Response data:', error.response.data);
  882. console.error('Response status:', error.response.status);
  883. console.error('Response headers:', error.response.headers);
  884. } else if (error.request) {
  885. console.error('No response received:', error.request);
  886. } else {
  887. console.error('Error message:', error.message);
  888. }
  889. }
  890. },
  891. async simulateThinking(message, signal) {
  892. this.isThinking = true;
  893. const dotInterval = 200; // 每 200ms 更新一次点
  894. return new Promise((resolve) => {
  895. const updateDots = () => {
  896. if (signal.aborted) {
  897. clearInterval(intervalId);
  898. this.isThinking = false;
  899. this.thinkingDots = "";
  900. message.message = "";
  901. resolve();
  902. return;
  903. }
  904. this.thinkingDots += ".";
  905. if (this.thinkingDots.length > 3) {
  906. this.thinkingDots = "";
  907. }
  908. message.message = "思考中" + this.thinkingDots;
  909. };
  910. const intervalId = setInterval(updateDots, dotInterval);
  911. signal.addEventListener("abort", () => {
  912. clearInterval(intervalId);
  913. this.isThinking = false;
  914. this.thinkingDots = "";
  915. message.message = "";
  916. resolve();
  917. });
  918. });
  919. },
  920. async handleResponse(value, existingMessage) {
  921. const data = value.data.answer//outputs[0].outputs[0].results.message;
  922. existingMessage.messageType = data;
  923. /* existingMessage.time = data.timestamp; */
  924. existingMessage.message = '';
  925. let mainText = data;
  926. let sourceText = '';
  927. if (this.$route.name !== 'ai' && this.minioUrls && this.minioUrls.length > 0) {
  928. sourceText = "\n\n<div class='source-section'><h3>相关资料来源:</h3><ol>";
  929. this.minioUrls.forEach((url, index) => {
  930. sourceText += `<li><a href="${url.url}" target="_blank" class="source-link">${url.object_name}</a></li>`;
  931. });
  932. sourceText += "</ol></div>";
  933. }
  934. console.log(existingMessage, mainText);
  935. // 先进行主要内容的打字效果
  936. await this.typeMessage(existingMessage, mainText);
  937. // 直接添加资料来源部分
  938. if (sourceText) {
  939. existingMessage.message += sourceText;
  940. existingMessage.html = this.renderMarkdown(existingMessage.message);
  941. }
  942. },
  943. typeMessage(message, fullText) {
  944. return new Promise(resolve => {
  945. const delay = 30;
  946. let i = 0;
  947. const typeChar = () => {
  948. if (i < fullText.length) {
  949. message.message += fullText[i];
  950. i++;
  951. setTimeout(typeChar, delay);
  952. } else {
  953. message.done = true;
  954. message.html = this.renderMarkdown(message.message);
  955. resolve();
  956. }
  957. };
  958. typeChar();
  959. });
  960. },
  961. renderMarkdown(rawMarkdown) {
  962. const md = new MarkdownIt({
  963. html: true,
  964. breaks: true,
  965. linkify: true
  966. });
  967. // 自定义链接渲染规则
  968. md.renderer.rules.link_open = (tokens, idx, options, env, self) => {
  969. const token = tokens[idx];
  970. const hrefIndex = token.attrIndex('href');
  971. const href = token.attrs[hrefIndex][1];
  972. return `<span class="custom-link" data-href="${href}">`;
  973. };
  974. md.renderer.rules.link_close = () => '</span>';
  975. const renderedHtml = md.render(rawMarkdown);
  976. // 使用 setTimeout 来确保 DOM 更新后再添加事件监听器
  977. setTimeout(() => {
  978. const links = document.querySelectorAll('.custom-link');
  979. links.forEach(link => {
  980. link.addEventListener('click', (e) => {
  981. e.preventDefault();
  982. const href = link.getAttribute('data-href');
  983. window.open(href, '_blank');
  984. });
  985. });
  986. }, 0);
  987. return renderedHtml;
  988. },
  989. handleKeydown(event) {
  990. // Check if 'Enter' is pressed without the 'Alt' key
  991. if (event.key === "Enter" && !(event.shiftKey || event.altKey)) {
  992. event.preventDefault(); // Prevent the default action to avoid line break in textarea
  993. this.sendMessage();
  994. } else if (event.key === "Enter" && event.altKey) {
  995. // Allow 'Alt + Enter' to insert a newline
  996. const cursorPosition = event.target.selectionStart;
  997. const textBeforeCursor = this.userInput.slice(0, cursorPosition);
  998. const textAfterCursor = this.userInput.slice(cursorPosition);
  999. // Insert the newline character at the cursor position
  1000. this.userInput = textBeforeCursor + "\n" + textAfterCursor;
  1001. // Move the cursor to the right after the inserted newline
  1002. this.$nextTick(() => {
  1003. event.target.selectionStart = cursorPosition + 1;
  1004. event.target.selectionEnd = cursorPosition + 1;
  1005. });
  1006. }
  1007. },
  1008. beforeDestroy() {
  1009. if (this.websocket) {
  1010. this.websocket.close();
  1011. }
  1012. },
  1013. /* 获取列表 */
  1014. init(){
  1015. /* 模型库 */
  1016. modelList({model_type:'model'}).then(res=>{
  1017. this.modelNameList=res.data
  1018. })
  1019. /* 知识库 */
  1020. listBuckets({ user_id: this.$store.state.user.id }).then(res=>{
  1021. this.kneList=res.data
  1022. })
  1023. /* 应用列表 */
  1024. configList().then(res=>{
  1025. this.knowledgeBases=res.data
  1026. })
  1027. /* 获取应用类型 */
  1028. application_types().then(res=>{
  1029. if(res.status!==200) return
  1030. this.appTypeList=res.data.application_types
  1031. })
  1032. }
  1033. },
  1034. updated() {
  1035. const messagesContainer = this.$el.querySelector(".messages");
  1036. if(messagesContainer){
  1037. messagesContainer.scrollTop = messagesContainer.scrollHeight;
  1038. }
  1039. },
  1040. };