chat.vue 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  1. <template>
  2. <div class="chat">
  3. <div class="chat-msg" ref="ref_chatMsg">
  4. <template v-for="(item, index) in state.chats">
  5. <template v-if="item.type === 'ask'">
  6. <askCom :item="item"/>
  7. </template>
  8. <template v-else>
  9. <answerCom
  10. :item="item"
  11. :goodMap="state.goodMap"
  12. :badMap="state.badMap"
  13. @onGood="onGood"
  14. @onBad="onBad"
  15. @onNormal="onNormal"
  16. />
  17. </template>
  18. </template>
  19. </div>
  20. <div class="chat-input">
  21. <div class="chat-input-block" v-loading="state.loading">
  22. <div class="chat-input-block-main">
  23. <div class="chat-input-block-main-auto" ref="ref_auto">
  24. <div class="chat-input-block-main-auto-list">
  25. <template v-for="item in state.autoList">
  26. <div class="chat-input-block-main-auto-item __hover" @click="setText(item)">{{ item }}</div>
  27. </template>
  28. </div>
  29. </div>
  30. <textarea
  31. ref="ref_text"
  32. placeholder="请输入您的问题"
  33. :rows="1"
  34. v-model="state.text"/>
  35. </div>
  36. <div class="chat-input-block-operations">
  37. <div class="cibo-audio">
  38. <audioCom @onLoading="onLoading" @onAudio="onAudio"/>
  39. </div>
  40. <div class="cibo-split"/>
  41. <el-tooltip content="发送" placement="top">
  42. <div class="cibo-send __hover" @click="onSend">
  43. <img src="@/views/smart-ask-answer/assistant/imgs/send.png"/>
  44. </div>
  45. </el-tooltip>
  46. </div>
  47. </div>
  48. </div>
  49. </div>
  50. </template>
  51. <script setup lang="ts">
  52. import {computed, getCurrentInstance, onMounted, reactive, ref, watch} from "vue";
  53. import askCom from './component/ask/index.vue';
  54. import answerCom from './component/answer/index.vue';
  55. import audioCom from './component/audio/index.vue'
  56. import {get, post, ssePost} from './dify/base'
  57. import {useRouter} from "vue-router";
  58. import {YMDHms} from "@/utils/czr-util";
  59. import {updateFeedback} from "@/views/smart-ask-answer/assistant/dify/share";
  60. import {cmsAiQueryQuestionReclist} from "@/views/smart-ask-answer/assistant/cms/api";
  61. const emit = defineEmits(['getText'])
  62. const router = useRouter()
  63. const state: any = reactive({
  64. text: '',
  65. loading: false,
  66. params: {
  67. response_mode: "streaming",
  68. conversation_id: "",
  69. files: [],
  70. query: "",
  71. inputs: {},
  72. parent_message_id: null
  73. },
  74. chats: [
  75. {
  76. type: 'answer',
  77. welcome: true,
  78. },
  79. ],
  80. goodMap: new Map(),
  81. badMap: new Map(),
  82. autoList: []
  83. })
  84. const ref_text = ref()
  85. const ref_chatMsg = ref()
  86. const ref_auto = ref()
  87. const onSend = () => {
  88. state.loading = true
  89. state.params.query = state.text + ''
  90. emit('getText', state.params.query)
  91. state.text = ''
  92. const ask = {
  93. type: 'ask',
  94. content: state.params.query + ''
  95. }
  96. state.chats.push(ask)
  97. const answer = reactive({
  98. type: 'answer',
  99. content: '',
  100. messageId: ''
  101. })
  102. state.chats.push(answer)
  103. ssePost(`/installed-apps/${window.czrConfig.dify.appId}/chat-messages`, {
  104. body: state.params,
  105. }, {
  106. onData: (message: string, isFirstMessage: boolean, { conversationId: newConversationId, messageId, taskId }: any) => {
  107. answer.content += message
  108. ref_chatMsg.value.scrollTo({
  109. top: ref_chatMsg.value.scrollHeight,
  110. behavior: 'smooth'
  111. });
  112. },
  113. onFile(file) {
  114. },
  115. onThought(thought) {
  116. },
  117. onMessageEnd: (messageEnd) => {
  118. if (!state.params.conversation_id) {
  119. state.params.conversation_id = messageEnd.conversation_id
  120. post(`/installed-apps/${window.czrConfig.dify.appId}/conversations/${state.params.conversation_id}/name`, {body: {auto_generate: false, name: YMDHms(new Date())}})
  121. }
  122. answer.messageId = messageEnd.message_id
  123. state.params.parent_message_id = messageEnd.message_id
  124. state.loading = false
  125. },
  126. onMessageReplace: (messageReplace) => {
  127. },
  128. onError() {
  129. },
  130. onWorkflowStarted: ({ workflow_run_id, task_id }) => {
  131. },
  132. onWorkflowFinished: ({ data: workflowFinishedData }) => {
  133. },
  134. onIterationStart: ({ data: iterationStartedData }) => {
  135. },
  136. onIterationFinish: ({ data: iterationFinishedData }) => {
  137. },
  138. onNodeStarted: ({ data: nodeStartedData }) => {
  139. },
  140. onNodeFinished: ({ data: nodeFinishedData }) => {
  141. },
  142. onTTSChunk: (messageId: string, audio: string) => {
  143. },
  144. onTTSEnd: (messageId: string, audio: string) => {
  145. },
  146. })
  147. }
  148. const onLoading = () => {
  149. state.loading = true
  150. }
  151. const onAudio = (text) => {
  152. state.text += text
  153. state.loading = false
  154. }
  155. const initChat = () => {
  156. get(`/installed-apps/${window.czrConfig.dify.appId}/parameters`).then((res: any) => {
  157. if (res.opening_statement) {
  158. state.chats = [
  159. {
  160. type: 'answer',
  161. welcome: true,
  162. },
  163. ]
  164. }
  165. })
  166. }
  167. const setText = (text: string) => {
  168. state.text = text
  169. }
  170. const onGood = (item) => {
  171. updateFeedback({
  172. url: `/messages/${item.messageId}/feedbacks`,
  173. body: {rating: 'like'},
  174. }, true, window.czrConfig.dify.appId).then((res: any) => {
  175. if (res.result === 'success') {
  176. state.goodMap.set(item.messageId, item)
  177. }
  178. })
  179. }
  180. const onBad = (item) => {
  181. updateFeedback({
  182. url: `/messages/${item.messageId}/feedbacks`,
  183. body: {rating: 'dislike'},
  184. }, true, window.czrConfig.dify.appId).then((res: any) => {
  185. if (res.result === 'success') {
  186. state.badMap.set(item.messageId, item)
  187. }
  188. })
  189. }
  190. const onNormal = (item) => {
  191. updateFeedback({
  192. url: `/messages/${item.messageId}/feedbacks`,
  193. body: {rating: null},
  194. }, true, window.czrConfig.dify.appId).then((res: any) => {
  195. if (res.result === 'success') {
  196. state.badMap.delete(item.messageId)
  197. state.goodMap.delete(item.messageId)
  198. }
  199. })
  200. }
  201. const initTextHandle = () => {
  202. let debounceTimer;
  203. const DEBOUNCE_TIME = 300;
  204. const textarea = ref_text.value
  205. const floatingDiv = ref_auto.value
  206. textarea.addEventListener('input', (e) => {
  207. textarea.style.height = 'auto';
  208. textarea.style.height = Math.min(textarea.scrollHeight + 2, 200) + 'px';
  209. clearTimeout(debounceTimer);
  210. floatingDiv.style.display = 'none';
  211. if (e.target.value) {
  212. debounceTimer = setTimeout(() => {
  213. updateFloatingDivPosition(e.target);
  214. }, DEBOUNCE_TIME)
  215. } else {
  216. floatingDiv.style.display = 'none';
  217. }
  218. });
  219. textarea.addEventListener('blur', function() {
  220. setTimeout(() => {
  221. floatingDiv.style.display = 'none';
  222. }, 200);
  223. });
  224. textarea.addEventListener('focus', function(e) {
  225. floatingDiv.style.maxWidth = (textarea.clientWidth + 32) + 'px';
  226. if (e.target.value) {
  227. updateFloatingDivPosition(e.target);
  228. floatingDiv.style.display = 'flex';
  229. }
  230. });
  231. const updateFloatingDivPosition = (t) => {
  232. floatingDiv.style.visibility = 'hidden';
  233. floatingDiv.style.display = 'flex';
  234. const params1 = {
  235. data: {
  236. pageIndex: 1,
  237. pageSize: 10,
  238. condition: {
  239. questionContent: t.value
  240. }
  241. }
  242. }
  243. cmsAiQueryQuestionReclist(params1).then(res => {
  244. state.autoList = res?.data?.list.map(v => v.questionContent) || []
  245. if (state.autoList.length > 0) {
  246. setTimeout(() => {
  247. floatingDiv.style.top = (-floatingDiv.clientHeight - 2 - 20) + 'px';
  248. floatingDiv.style.visibility = 'visible';
  249. }, 10)
  250. }
  251. })
  252. }
  253. }
  254. onMounted(() => {
  255. initChat()
  256. initTextHandle()
  257. })
  258. defineExpose({setText})
  259. </script>
  260. <style lang="scss" scoped>
  261. $scrollRight: 16px;
  262. .chat {
  263. width: 100%;
  264. height: 100%;
  265. padding: 32px calc(32px - $scrollRight) 0 32px;
  266. background: #FFFFFF;
  267. border-radius: 16px;
  268. display: flex;
  269. flex-direction: column;
  270. .chat-msg {
  271. flex: 1;
  272. overflow-y: auto;
  273. display: flex;
  274. flex-direction: column;
  275. gap: 16px;
  276. padding-right: $scrollRight;
  277. }
  278. .chat-input {
  279. width: 100%;
  280. padding: 24px calc(5px + $scrollRight) 24px 5px;
  281. .chat-input-block {
  282. width: 100%;
  283. height: 100%;
  284. background: #FFFFFF;
  285. box-shadow: 0px 8px 16px 0px rgba(210,219,232,0.35);
  286. border-radius: 16px;
  287. border: 1px solid #E6E7EE;
  288. display: flex;
  289. flex-direction: column;
  290. padding: 16px;
  291. .chat-input-block-main {
  292. position: relative;
  293. .chat-input-block-main-auto {
  294. position: absolute;
  295. padding: 12px 16px;
  296. background: #FFFFFF;
  297. box-shadow: 0px 8px 16px 0px rgba(210,219,232,0.35);
  298. border-radius: 16px;
  299. border: 1px solid #E6E7EE;
  300. min-width: 200px;
  301. display: none;
  302. max-height: 200px;
  303. left: -16px;
  304. overflow: hidden;
  305. width: max-content;
  306. .chat-input-block-main-auto-list {
  307. flex: 1;
  308. overflow-y: auto;
  309. .chat-input-block-main-auto-item {
  310. padding: 6px 0;
  311. border-bottom: 1px dashed #D8DAE5;
  312. &:last-child {
  313. border-bottom: none;
  314. }
  315. }
  316. }
  317. }
  318. >textarea {
  319. width: 100%;
  320. resize: none;
  321. border: none;
  322. }
  323. }
  324. .chat-input-block-operations {
  325. margin-top: 12px;
  326. display: flex;
  327. align-items: center;
  328. justify-content: flex-end;
  329. .cibo-split {
  330. width: 1px;
  331. height: 17px;
  332. background: #E6E7EE;
  333. }
  334. .cibo-audio {
  335. display: flex;
  336. align-items: center;
  337. margin-right: 8px;
  338. }
  339. .cibo-send {
  340. margin-left: 16px;
  341. width: 32px;
  342. height: 32px;
  343. background: #1D64FD;
  344. border-radius: 50%;
  345. display: flex;
  346. align-items: center;
  347. justify-content: center;
  348. >img {
  349. margin-right: 3px;
  350. }
  351. }
  352. }
  353. }
  354. }
  355. }
  356. </style>