DialogAnswer.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. <template>
  2. <div class="dialog-item dialog-anwser" :class="anwser_klass">
  3. <BtnSelectItem v-if="select_btn_visible" @change="handleSelectChange" />
  4. <div class="dialog-main answer-main">
  5. <div class="dialog-icon answer-icon">
  6. <!-- 根据智能体显示图标 -->
  7. <!-- <img :src="answer.client_custom_icon" /> -->
  8. <!-- 2024-12-26:薛娜提出,使用设计稿的图标 -->
  9. <!-- 需求详情查看 https://note.youdao.com/ynoteshare/index.html?id=d781d48a6d64bab8451026fe65342a7f&type=note&_time=1735176727116 6.1 -->
  10. <img :src="chat_agent_icon" />
  11. </div>
  12. <div class="dialog-content answer-content">
  13. <div>
  14. <DialogContent
  15. :content="answer_html"
  16. :is_loading="is_content_loading"
  17. :max_width="content_max_width"
  18. :show_think="show_think"
  19. >
  20. <DialogActions
  21. v-if="answer_status === 2"
  22. class="answer-actions-btns"
  23. :action_list="answer_action_list"
  24. @copy="handleAnswerCopy"
  25. @share="handleAnswerShare"
  26. @retry="handleAnswerRetry"
  27. @edit="handleAnswerEdit"
  28. />
  29. </DialogContent>
  30. <!-- <BtnStopChat v-if="answer_status === 1" @stop="handleStopChat" /> -->
  31. <!-- 对话已经结束,并且有引用数据 -->
  32. <template v-if="has_refference && answer_status === 2">
  33. <RefferenceFileBlocks
  34. class="refference-files-blocks-container"
  35. :doc_aggs="full_refference.doc_aggs"
  36. :chunks="full_refference.chunks"
  37. :total="full_refference.total"
  38. />
  39. </template>
  40. </div>
  41. </div>
  42. </div>
  43. </div>
  44. </template>
  45. <script setup>
  46. /**
  47. * 智能体的回答内容,可能需要包括下面一些功能
  48. *
  49. * 4. 回答反馈:分享、点赞、复制
  50. */
  51. import { computed, onMounted } from 'vue';
  52. import { Message } from '../../3rd-libs/arco-vue-libs';
  53. import BtnSelectItem from './BtnSelectItem.vue';
  54. import DialogActions from './DialogActions.vue';
  55. import BtnStopChat from './BtnStopChat.vue';
  56. import DialogContent from '../conversation-content/DialogContent.vue';
  57. import formatAnwserString from '../conversation-dialog/format-anwser-string';
  58. // import { formatFileDownloadStr } from '../conversation-dialog/format-dialog-answer-custom-markdown-str';
  59. // import { getIconIntelligent } from '../common/icon-map';
  60. // import { formatStaticAssetsUrl } from '../../utils/utils';
  61. import applyRefferencePopover4DownloadFile from '../answer-refference/apply-refference-popover-4-download-file';
  62. import applyRefferencePopover4RefFlag from '../answer-refference/apply-refference-popover-4-ref-flag';
  63. // import applyRefferencePopover4FileName from '../answer-refference/apply-refference-popover-4-filename';
  64. import { applyCodeHighLight } from '../conversation-dialog/format-code-string';
  65. import RefferenceFileBlocks from '../answer-refference/RefferenceFileBlocks.vue';
  66. import copyText from '../../3rd-libs/clipboard';
  67. import chat_agent_icon from '../../assets/chat/chat-agent-icon.png';
  68. const props = defineProps({
  69. /**
  70. * 是否显示选择按钮
  71. * 仿照kimi,当用户选择分享时,选择按钮会展示
  72. */
  73. show_select: true,
  74. /**
  75. * 答案内容数据
  76. */
  77. answer: Object,
  78. /**
  79. * 对话区域的宽度
  80. */
  81. width: Number,
  82. answer_action_list: Array
  83. });
  84. const emit = defineEmits(['stop', 'retry', 'select', 'edit']);
  85. const select_btn_visible = computed(() => {
  86. return props.show_select === true;
  87. });
  88. /**
  89. * 当前对话的状态
  90. * 0 加载中
  91. * 1 正在对话
  92. * 2 已结束
  93. */
  94. const answer_status = computed(() => {
  95. const answer = props.answer;
  96. return answer.client_custom_chat_status;
  97. });
  98. /**
  99. * 内容是否正在加载中
  100. */
  101. const is_content_loading = computed(() => {
  102. return answer_status.value === 0;
  103. });
  104. /**
  105. * 内容区域的最大宽度
  106. * 内容区域的宽度,根据父容器的宽度,减去此组件内非内容区域元素(icon等)的宽度,以及横向的的margin/padding
  107. * 最终得出内容区域的最大宽度
  108. */
  109. const content_max_width = computed(() => {
  110. return props.width - 40 - 40 - 20;
  111. });
  112. const system_message = computed(() => {
  113. const answer = props.answer;
  114. return answer.client_custom_running_msg || '';
  115. });
  116. /**
  117. * 智能体回答的内容的html字符串
  118. * 是markdown格式
  119. * 需要使用markdown解析器进行解析
  120. */
  121. const answer_html = computed(() => {
  122. const answer = props.answer;
  123. const str = answer.content;
  124. /**
  125. * 第一步处理位于:../conversation-dialogformat-dialog-answer-from-websocket_receive_message.js
  126. * 此处将websocket的答案数据进行第二步处理
  127. * 使用markdown解析器进行解析,并处理一部分字符串
  128. * 最终生成html字符串
  129. */
  130. let answer_string = formatAnwserString(str);
  131. // 第二步处理已经结束,html字符串已经生成
  132. // 接下来进行最后一步:附加内容的字符串拼接
  133. // 由于SQL和代码不能通过markdown解析,所以此处进行字符串的拼接
  134. // 在markdown转换结束后,添加拼接字符串
  135. // answer.client_custom_attached_html_before_render 的生成过程位于 ../conversation-dialogformat-dialog-answer-from-websocket_receive_message.js
  136. if (answer.client_custom_attached_html_before_render) {
  137. answer_string = answer_string + answer.client_custom_attached_html_before_render;
  138. }
  139. // // 第二步处理已经结束,html字符串已经生成
  140. // // 接下来进行最后一步:附加内容的字符串拼接
  141. // // 由于SQL和代码不能通过markdown解析,所以此处进行字符串的拼接
  142. // // 在markdown转换结束后,添加拼接字符串
  143. // // 拼接python代码
  144. // if (answer.client_custom_linked_code) {
  145. // answer_string = answer_string + answer.client_custom_linked_code;
  146. // }
  147. // // 拼接SQL代码
  148. // if (answer.client_custom_linked_sql) {
  149. // answer_string = answer_string + answer.client_custom_linked_sql;
  150. // }
  151. // // 拼接生成的图片
  152. // if (answer.client_custom_linked_image) {
  153. // const img_url = formatStaticAssetsUrl(answer.client_custom_linked_image);
  154. // answer_string = answer_string + `<img src="${img_url}" />`;
  155. // }
  156. // // 拼接文件的下载链接
  157. // if (answer.client_custom_linked_download_files) {
  158. // const file_name = answer.client_custom_linked_download_files.file_name;
  159. // const file_url = answer.client_custom_linked_download_files.file_url;
  160. // const download_file_str = formatFileDownloadStr(file_name, file_url);
  161. // answer_string = answer_string + download_file_str;
  162. // }
  163. /**
  164. * 系统消息展示
  165. * 如果有系统消息,则展示系统消息
  166. * 如果没有系统消息,则不用展示
  167. */
  168. const loader_str = `<div class="answer-system-message-content-container">${system_message.value} <span class="answer-system-message-content-loader"></span></div>`;
  169. /**
  170. * 所有步骤已经走完
  171. * 返回最终的html字符串
  172. * 此字符串就是要渲染到界面上的html字符串
  173. */
  174. if (system_message.value) {
  175. return `${answer_string}${loader_str}`;
  176. } else {
  177. return answer_string;
  178. }
  179. });
  180. /**
  181. * 智能体回答区域容器的class值
  182. * 通过唯一ID生成,可以保证界面中每一个答案都有一个唯一的容器类名
  183. * 为后续其他处理(引用效果、代码美化)做准备:后续处理可以通过此class定位相关子元素
  184. */
  185. const anwser_klass = computed(() => {
  186. const answer = props.answer;
  187. return `klass-${answer.client_custom_unique_id}-answer-content`;
  188. });
  189. /**
  190. * 完整引用数据
  191. */
  192. const full_refference = computed(() => {
  193. if (!props.answer) {
  194. return null;
  195. }
  196. const answer = props.answer;
  197. if (!answer.client_custom_linked_refference_full_content) {
  198. return null;
  199. }
  200. if (Object.keys(answer.client_custom_linked_refference_full_content).length === 0) {
  201. return null;
  202. } else {
  203. for (let key in answer.client_custom_linked_refference_full_content) {
  204. if (answer.client_custom_linked_refference_full_content[key].length === -0) {
  205. return null;
  206. }
  207. }
  208. }
  209. return answer.client_custom_linked_refference_full_content;
  210. });
  211. /**
  212. * 是否显示引用效果
  213. */
  214. const has_refference = computed(() => {
  215. if (!full_refference.value) {
  216. return false;
  217. }
  218. return true;
  219. });
  220. /**
  221. * 是否展示思考过程
  222. * */
  223. const show_think = computed(() => {
  224. return true;
  225. });
  226. const handleSelectChange = () => {
  227. emit('select');
  228. };
  229. /**
  230. * 停止答案的生成
  231. */
  232. const handleStopChat = () => {
  233. emit('stop');
  234. };
  235. /**
  236. * copy
  237. */
  238. const handleAnswerCopy = () => {
  239. copyText(answer_html.value);
  240. Message.success('已复制');
  241. };
  242. /**
  243. * 分享答案
  244. */
  245. const handleAnswerShare = () => {};
  246. /**
  247. * 重新生成答案
  248. */
  249. const handleAnswerRetry = () => {
  250. emit('retry');
  251. };
  252. /**
  253. * 编辑文本
  254. * */
  255. const handleAnswerEdit = () => {
  256. emit('edit');
  257. };
  258. onMounted(() => {
  259. const container_klass = anwser_klass.value;
  260. const answer = props.answer;
  261. const full_refference = answer.client_custom_linked_refference_full_content;
  262. const download_files = answer.client_custom_linked_download_files;
  263. const download_file_list = answer.client_custom_linked_download_files_List || [];
  264. /**
  265. * 文档引用效果
  266. */
  267. applyRefferencePopover4RefFlag(container_klass, full_refference);
  268. // applyRefferencePopover4FileName(container_klass, full_refference);
  269. if (download_files) {
  270. applyRefferencePopover4DownloadFile(container_klass, [download_files, ...download_file_list]);
  271. }
  272. /**
  273. * 处理代码样式: 美化代码的展示
  274. */
  275. applyCodeHighLight(container_klass);
  276. });
  277. </script>
  278. <style src="./dialog-item-common.css" />
  279. <style lang="css" scoped>
  280. .dialog-anwser {
  281. color: var(--color-text-1);
  282. display: flex;
  283. justify-content: space-between;
  284. }
  285. .answer-main {
  286. }
  287. .answer-content {
  288. display: flex;
  289. justify-content: flex-start;
  290. }
  291. .answer-icon {
  292. margin-right: 22px;
  293. }
  294. .answer-icon img {
  295. width: 100%;
  296. transform: scale(1.6);
  297. }
  298. .answer-actions-btns {
  299. margin-top: 5px;
  300. }
  301. .refference-files-blocks-container {
  302. margin-top: 26px;
  303. }
  304. .dialog-content.answer-content .dialog-chat-answer-content {
  305. padding-right: 45px;
  306. }
  307. </style>
  308. <style>
  309. .answer-system-message-content-container {
  310. display: flex;
  311. align-items: center;
  312. }
  313. .answer-system-message-content-loader {
  314. display: inline-block;
  315. margin-left: 14px;
  316. width: 6px;
  317. height: 6px;
  318. border-radius: 50%;
  319. background-color: #165dff;
  320. box-shadow: 10px 0 #165dff, -10px 0 #165dff;
  321. position: relative;
  322. animation: flash 0.8s ease-out infinite alternate;
  323. }
  324. @keyframes flash {
  325. 0% {
  326. background-color: #fff2;
  327. box-shadow: 10px 0 #fff2, -10px 0 #165dff;
  328. }
  329. 50% {
  330. background-color: #165dff;
  331. box-shadow: 10px 0 #fff2, -10px 0 #fff2;
  332. }
  333. 100% {
  334. background-color: #fff2;
  335. box-shadow: 10px 0 #165dff, -10px 0 #fff2;
  336. }
  337. }
  338. </style>