talk.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <template>
  2. <div class="d-flex flex-column" style="height: 100%">
  3. <v-banner single-line>
  4. <div class="my-3">
  5. {{ title }}
  6. </div>
  7. <template #actions>
  8. <slot name="title"></slot>
  9. </template>
  10. </v-banner>
  11. <div style="flex: 1; overflow: auto;" class="pa-3" ref="box">
  12. <div style="height: 100%;">
  13. <v-list three-line>
  14. <!-- 欢迎语 -->
  15. <v-list-item>
  16. <v-list-item-content>
  17. <v-list-item-title class="d-flex align-center" :class="system.nameColor + '--text'">
  18. <v-icon class="mr-3" :color="system.iconColor" >
  19. {{ system.icon }}
  20. </v-icon>
  21. {{ system.name }}
  22. </v-list-item-title>
  23. <v-list-item-subtitle class="d-flex">
  24. <div class="lighten-5 pa-3 round" :class="system.bgColor">
  25. {{ system.welcome }}
  26. </div>
  27. </v-list-item-subtitle>
  28. </v-list-item-content>
  29. </v-list-item>
  30. <v-list-item v-for="talk in talks" :key="talk.id">
  31. <v-list-item-content>
  32. <v-list-item-title
  33. class="d-flex align-center"
  34. :class="
  35. `${talk.type === 'question' ? 'justify-end ' + user.nameColor + '--text' : system.nameColor + '--text'}`
  36. "
  37. >
  38. {{ talk.type === 'question' ? $store.getters.userInfo.username : '' }}
  39. <template v-if="talk.type === 'question'">
  40. <v-chip v-for="tag in talk.tags" :key="tag" x-small class="ml-2">{{ tag }}</v-chip>
  41. </template>
  42. <v-icon
  43. :class="talk.type === 'question' ? 'ml-3' : 'mr-3'"
  44. :color="talk.type === 'question' ? user.iconColor : system.iconColor"
  45. >
  46. {{ talk.type === 'question' ? user.icon : system.icon }}
  47. </v-icon>
  48. {{ talk.type === 'question' ? '' : system.name }}
  49. <template v-if="talk.type !== 'question'">
  50. <v-chip v-for="tag in talk.tags" :key="tag" x-small class="ml-2">{{ tag }}</v-chip>
  51. </template>
  52. </v-list-item-title>
  53. <v-list-item-subtitle class="d-flex" :class="{'justify-end': talk.type === 'question'}">
  54. <div class="lighten-5 pa-3 round" :class=" talk.type === 'question' ? user.bgColor : system.bgColor">
  55. <template v-if="talk.view === 'table'">
  56. <v-simple-table fixed-header dense height="350">
  57. <template v-slot:default>
  58. <thead>
  59. <tr>
  60. <th v-for="header in talk.content.headers" :key="header" class="text-left">
  61. {{header}}
  62. </th>
  63. </tr>
  64. </thead>
  65. <tbody>
  66. <tr
  67. v-for="(body, i) in talk.content.body"
  68. :key="i"
  69. >
  70. <td v-for="header in talk.content.headers" :key="header">{{ body[header] }}</td>
  71. </tr>
  72. </tbody>
  73. </template>
  74. </v-simple-table>
  75. </template>
  76. <template v-else>
  77. <div v-for="(content, index) in talk.content" :key="index + content">{{ content }}</div>
  78. <!-- {{ talk.content }} -->
  79. </template>
  80. </div>
  81. </v-list-item-subtitle>
  82. </v-list-item-content>
  83. </v-list-item>
  84. <!-- loading -->
  85. <v-list-item style="min-height: 10px" v-show="loading">
  86. <v-list-item-content>
  87. <v-list-item-title class="d-flex align-center" :class="system.nameColor + '--text'">
  88. <v-icon class="mr-3" :color="system.iconColor">
  89. {{ system.icon }}
  90. </v-icon>
  91. <div class="mr-3">{{ system.name }} 搜索中 </div>
  92. <v-progress-circular
  93. size="18"
  94. width="2"
  95. indeterminate
  96. :color="system.iconColor"
  97. ></v-progress-circular>
  98. </v-list-item-title>
  99. </v-list-item-content>
  100. </v-list-item>
  101. </v-list>
  102. </div>
  103. </div>
  104. <div class="send d-flex align-center justify-center">
  105. <div class="send-box">
  106. <v-textarea
  107. v-model="question"
  108. dense
  109. class="send-box-area"
  110. auto-grow
  111. label="请输入您想问的内容,按 Ctrl+Enter 换行"
  112. placeholder="请输入您想问的内容,按 Ctrl+Enter 换行"
  113. solo
  114. hide-details
  115. no-resize
  116. rows="1"
  117. @keydown.enter="handleKeyCode($event)"
  118. >
  119. </v-textarea>
  120. <v-btn icon color="primary" class="btn" :disabled="!question" @click="handleSendMsg">
  121. <v-icon>mdi-send</v-icon>
  122. </v-btn>
  123. </div>
  124. </div>
  125. </div>
  126. </template>
  127. <script>
  128. import { api } from '@/api/dataGovernance'
  129. export default {
  130. name: 'knowledge-talk',
  131. props: {
  132. // 1 手册探索 2 图表实验室 3 产品知识库 (RAG + Graph) 4 产品知识库 (RAG)
  133. type: {
  134. type: Number,
  135. default: 1
  136. },
  137. title: {
  138. type: String,
  139. default: '数据探索'
  140. },
  141. welcome: {
  142. type: String,
  143. default: '您好,我是您的智能助手,有什么问题可以问我哦!'
  144. }
  145. },
  146. data () {
  147. return {
  148. // 系统端信息配置
  149. system: {
  150. name: '智能助手',
  151. nameColor: 'indigo',
  152. icon: 'mdi-face-agent',
  153. iconColor: 'indigo',
  154. bgColor: 'indigo',
  155. welcome: this.welcome
  156. },
  157. // 用户端信息配置
  158. user: {
  159. name: this.$store.getters.userInfo.username,
  160. nameColor: 'blue',
  161. icon: 'mdi-account-circle',
  162. iconColor: 'blue',
  163. bgColor: 'blue'
  164. },
  165. loading: false,
  166. talks: [],
  167. question: '',
  168. list: [],
  169. id: 0
  170. }
  171. },
  172. watch: {
  173. // 长度变化自动滑动至底部
  174. talks: {
  175. handler () {
  176. this.$nextTick(() => {
  177. this.$refs.box.scrollTo({
  178. top: this.$refs.box.scrollHeight,
  179. behavior: 'smooth'
  180. })
  181. })
  182. },
  183. deep: true,
  184. immediate: true
  185. }
  186. },
  187. methods: {
  188. handleSendMsg () {
  189. if (!this.question) {
  190. return
  191. }
  192. switch (this.type) {
  193. case 2:
  194. this.getSql()
  195. break
  196. case 4:
  197. this.talkTo(3, true)
  198. break
  199. default:
  200. this.talkTo(this.type)
  201. break
  202. }
  203. },
  204. handleKeyCode (event) {
  205. if (event.keyCode === 13) {
  206. if (!event.ctrlKey) {
  207. event.preventDefault()
  208. this.handleSendMsg()
  209. } else {
  210. this.question += '\n'
  211. }
  212. }
  213. },
  214. async getSql () {
  215. this.loading = true
  216. this.talks.push({
  217. id: this.id++,
  218. type: 'question',
  219. tags: [],
  220. content: this.question.split('\n')
  221. })
  222. const params = {
  223. question: this.question
  224. }
  225. this.question = ''
  226. try {
  227. const { data } = await api.getSql(params)
  228. this.talks.push({
  229. id: this.id++,
  230. type: 'response',
  231. tags: [],
  232. content: data.text.split('\n')
  233. })
  234. this.getTable(data.id)
  235. } catch (error) {
  236. this.$snackbar.error(error)
  237. } finally {
  238. this.loading = false
  239. }
  240. },
  241. async getTable (id) {
  242. this.loading = true
  243. const params = { id }
  244. try {
  245. const { data } = await api.getToTable(params)
  246. if (data.type === 'error') {
  247. return
  248. }
  249. const _table = JSON.parse(data.df)
  250. if (!_table.length) {
  251. return
  252. }
  253. // console.log(_table)
  254. const _headers = Object.keys(_table[0])
  255. this.talks.push({
  256. id: this.id++,
  257. type: 'response',
  258. tags: [],
  259. content: {
  260. headers: _headers,
  261. body: _table
  262. },
  263. view: 'table'
  264. })
  265. this.$emit('change', _headers, _table)
  266. } catch (error) {
  267. this.$snackbar.error(error)
  268. } finally {
  269. this.loading = false
  270. }
  271. },
  272. async talkTo (useType, rag) {
  273. this.talks.push({
  274. id: this.id++,
  275. type: 'question',
  276. tags: [],
  277. content: this.question.split('\n')
  278. })
  279. const param = {
  280. question: this.question,
  281. chat_history: this.list
  282. }
  283. this.question = ''
  284. this.loading = true
  285. const askApi = useType === 3 ? api.askToUnstructured : api.ask
  286. try {
  287. const { data } = await askApi(param)
  288. this.talks.push({
  289. id: this.id++,
  290. type: 'response',
  291. tags: useType === 3 ? ['RAG + Graph'] : [],
  292. content: data.response.split('\n')
  293. })
  294. // 多获取一个图谱+RAG
  295. if (rag) {
  296. const { data: _data } = await api.askToProduct(param)
  297. this.talks.push({
  298. id: this.id++,
  299. type: 'response',
  300. tags: ['RAG'],
  301. content: _data.response.split('\n')
  302. })
  303. }
  304. } catch (error) {
  305. this.$snackbar.error(error)
  306. } finally {
  307. this.loading = false
  308. }
  309. }
  310. }
  311. }
  312. </script>
  313. <style lang="scss" scoped>
  314. .round {
  315. border-radius: 5px;
  316. max-width: 80%;
  317. }
  318. .send {
  319. // height: 130px;
  320. // margin: 20px 0;
  321. padding: 20px;
  322. &-box {
  323. width: 100%;
  324. // max-width: 800px;
  325. position: relative;
  326. .btn {
  327. position: absolute;
  328. right: 20px;
  329. bottom: 12px;
  330. }
  331. &-area {
  332. position: relative;
  333. bottom: 0;
  334. ::v-deep textarea {
  335. padding: 15px 70px 15px 0 !important;
  336. max-height: 300px;
  337. min-height: 60px;
  338. overflow: auto;
  339. margin: 0 !important;
  340. }
  341. }
  342. }
  343. }
  344. </style>