dataChartEditChat.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. <template>
  2. <div class="chart-content-chat heightFull d-flex flex-column position-relative overflow-hidden">
  3. <v-btn
  4. class="history"
  5. color="indigo lighten-1"
  6. dark
  7. @click.stop="drawer = !drawer"
  8. >
  9. 最近会话
  10. </v-btn>
  11. <div class="chart-content-chat-title mb-3">
  12. <v-tabs>
  13. <v-tab>AI 取数</v-tab>
  14. </v-tabs>
  15. </div>
  16. <div class="chart-content-chat-box overflow-y-auto position-relative element" ref="chatBox" v-loading="loading">
  17. <div class="pa-3">
  18. <div
  19. v-for="(item, index) in items"
  20. :key="index"
  21. :class="['d-flex', 'mb-3', item.type === 1 ? 'flex-row' : 'flex-row-reverse' ]"
  22. >
  23. <v-avatar color="indigo" size="36">
  24. <span class="white--text">{{ item.type === 1 ? 'AI' : 'T' }}</span>
  25. </v-avatar>
  26. <div :class="[item.type === 1 ? 'ml-3 flex-grow-1 flex-shrink-1' : 'mr-3 box-length-70']">
  27. <div
  28. :class="['d-flex align-center', `justify-${item.type === 1 ? 'start' : 'end'}`]"
  29. >
  30. {{ item.type === 1 ? 'AI助手' : '游客' }}
  31. <template v-if="item.type === 1">
  32. <v-btn
  33. v-if="item.content.sql"
  34. class="ml-3"
  35. small
  36. elevation="0"
  37. depressed
  38. @click="item.showSnackbar = !item.showSnackbar"
  39. >
  40. {{ !item.showSnackbar ? '查看SQL' : '收起SQL'}}
  41. </v-btn>
  42. </template>
  43. </div>
  44. <div class="mt-2" :class="{ 'indigo lighten-5 pa-3 rounded': item.type === 2 }">
  45. <template v-if="typeof item.content === 'string'">
  46. <div>
  47. <span v-if="item.welcome" class="mdi mdi-hand-wave"></span>
  48. {{item.content}}
  49. </div>
  50. <div v-if="item.welcome">
  51. <div class="mt-1" v-for="_item in item.items" :key="_item">
  52. <span class="defaultLink" @click="onSend(_item)">{{_item}}</span>
  53. </div>
  54. </div>
  55. </template>
  56. <template v-else-if="Object.keys(item.content).length === 0">
  57. <span>
  58. 正在思考中
  59. <v-progress-circular
  60. indeterminate
  61. size="14"
  62. class="ml-1"
  63. width="2"
  64. color="primary"
  65. ></v-progress-circular>
  66. </span>
  67. </template>
  68. <template v-else>
  69. <div>
  70. {{ item.content.response }}
  71. </div>
  72. <div v-if="item.showSnackbar" class="pa-3 blue-grey lighten-3 mt-3">
  73. {{ item.content.sql }}
  74. </div>
  75. <div class="mt-3" v-if="item.content.records && item.content.records.columns.length">
  76. <div>
  77. <v-menu
  78. v-model="item.showMenu"
  79. :close-on-content-click="false"
  80. :close-on-click="false"
  81. max-width="300"
  82. attach=".chart-content-chat-box"
  83. >
  84. <template v-slot:activator="{ on , attrs}">
  85. <v-btn
  86. v-on="on"
  87. v-bind="attrs"
  88. text
  89. color="primary"
  90. >我要画图</v-btn>
  91. </template>
  92. <div class="white">
  93. <v-banner>画图配置</v-banner>
  94. <div class="pa-3">
  95. <v-autocomplete
  96. v-model="item.model.typeAxis"
  97. :items="item.content.records.columns"
  98. class="mb-3"
  99. outlined
  100. dense
  101. hide-details
  102. label="类型轴"
  103. ></v-autocomplete>
  104. <v-autocomplete
  105. v-model="item.model.dataAxis"
  106. :items="item.content.records.columns"
  107. class="mb-3"
  108. outlined
  109. dense
  110. hide-details
  111. label="数据轴"
  112. multiple
  113. chips
  114. small-chips
  115. ></v-autocomplete>
  116. <div class="text-right">
  117. <v-btn small class="mr-3" @click="item.showMenu = false">关闭</v-btn>
  118. <v-btn small color="primary" @click="onRender(item)">图表预览</v-btn>
  119. </div>
  120. </div>
  121. </div>
  122. </v-menu>
  123. </div>
  124. <v-card flat outlined height="324">
  125. <div class="pa-3">
  126. <v-simple-table
  127. fixed-header
  128. dense
  129. height="300px"
  130. >
  131. <template v-slot:default>
  132. <thead>
  133. <tr>
  134. <th
  135. v-for="header in item.content.records.columns"
  136. :key="header"
  137. class="text-left"
  138. >{{ header }}</th>
  139. </tr>
  140. </thead>
  141. <tbody>
  142. <tr
  143. v-for="(row, index) in item.content.records.rows"
  144. :key="index"
  145. >
  146. <td
  147. v-for="header in item.content.records.columns"
  148. :key="header"
  149. class="text-left"
  150. >{{ row[header] }}</td>
  151. </tr>
  152. </tbody>
  153. </template>
  154. </v-simple-table>
  155. </div>
  156. </v-card>
  157. </div>
  158. <div class="d-flex align-center" v-if="!item.dataValidation">
  159. 您认为结果是否正确
  160. <v-btn
  161. class="ma-2"
  162. text
  163. icon
  164. small
  165. color="blue lighten-2"
  166. @click="onAddTrain(item, true)"
  167. >
  168. <v-icon>mdi-thumb-up</v-icon>
  169. </v-btn>
  170. <v-btn
  171. class="ma-2"
  172. text
  173. icon
  174. small
  175. color="red lighten-2"
  176. @click="onAddTrain(item, false)"
  177. >
  178. <v-icon>mdi-thumb-down</v-icon>
  179. </v-btn>
  180. </div>
  181. </template>
  182. </div>
  183. </div>
  184. </div>
  185. </div>
  186. </div>
  187. <div class="pa-3 chart-content-chat-btn">
  188. <div class="send">
  189. <div class="pa-3 text-center" v-if="conversationId">
  190. <v-btn color="indigo" outlined small @click="onNew">
  191. <v-icon left>
  192. mdi-chat-plus-outline
  193. </v-icon>新对话
  194. </v-btn>
  195. </div>
  196. <div class="send-box">
  197. <v-textarea
  198. v-model="question"
  199. class="send-box-area"
  200. auto-grow
  201. placeholder="请输入您想问的内容,按 Ctrl+Enter 换行"
  202. outlined
  203. hide-details
  204. no-resize
  205. rows="1"
  206. @keydown.enter="handleKeyCode($event)"
  207. >
  208. </v-textarea>
  209. <v-btn icon color="primary" class="btn" :disabled="!question || disabled" @click="handleSendMsg">
  210. <v-icon>mdi-send</v-icon>
  211. </v-btn>
  212. </div>
  213. <div>
  214. <v-chip-group
  215. active-class="primary--text"
  216. column
  217. v-model="routingMode"
  218. >
  219. <v-chip
  220. v-for="chip in chips"
  221. :key="chip.value"
  222. small
  223. :value="chip.value"
  224. >
  225. {{ chip.text }}
  226. </v-chip>
  227. </v-chip-group>
  228. </div>
  229. </div>
  230. </div>
  231. <v-navigation-drawer
  232. v-model="drawer"
  233. absolute
  234. temporary
  235. right
  236. width="300"
  237. overlay-opacity="0"
  238. >
  239. <v-list dense>
  240. <v-list-item-group
  241. v-model="selected"
  242. color="primary"
  243. @change="onSelectConversation"
  244. >
  245. <v-list-item
  246. v-for="(conversation) in conversationList"
  247. :key="conversation.conversation_id"
  248. >
  249. <v-list-item-content>
  250. <v-list-item-title>{{ conversation.conversation_title }}</v-list-item-title>
  251. </v-list-item-content>
  252. </v-list-item>
  253. </v-list-item-group>
  254. </v-list>
  255. </v-navigation-drawer>
  256. </div>
  257. </template>
  258. <script>
  259. import {
  260. getAsk,
  261. addFeedback,
  262. getConversations,
  263. getConversationsById
  264. } from '@/api/dataChart'
  265. import { mapGetters } from 'vuex'
  266. export default {
  267. name: 'dataChartEditChat',
  268. data () {
  269. return {
  270. selected: null,
  271. lastSelected: null,
  272. drawer: false,
  273. routingMode: undefined,
  274. chips: [
  275. { text: '聊天模式', value: 'chat_direct' },
  276. { text: '数据库模式', value: 'database_direct' }
  277. ],
  278. loading: false,
  279. disabled: false,
  280. question: '',
  281. items: [
  282. {
  283. type: 1,
  284. welcome: true,
  285. content: '你好,我是您的数据查询小助手,支持查询“高速公路服务区”的相关信息。您可以这样提问: ',
  286. items: [
  287. '现在一共有多少个服务区?分别归属于哪些管理公司?',
  288. '哪个服务区的档口数量最多?',
  289. '赣州分公司下,餐饮类档口的日均订单量是多少?'
  290. ]
  291. }
  292. ],
  293. abortController: null,
  294. conversationId: undefined,
  295. conversationList: []
  296. // trueData: false
  297. }
  298. },
  299. computed: {
  300. ...mapGetters(['userInfo'])
  301. },
  302. created () {
  303. this.getConversationList()
  304. },
  305. methods: {
  306. onNew () {
  307. this.abortController.abort('')
  308. this.abortController = null
  309. this.items.splice(1, this.items.length - 1)
  310. this.conversationId = undefined
  311. },
  312. handleKeyCode (event) {
  313. if (event.keyCode === 13) {
  314. if (!event.ctrlKey) {
  315. event.preventDefault()
  316. this.handleSendMsg()
  317. } else {
  318. this.question += '\n'
  319. }
  320. }
  321. },
  322. onSend (str) {
  323. if (this.disabled) {
  324. return
  325. }
  326. this.question = str
  327. this.handleSendMsg()
  328. },
  329. async handleSendMsg () {
  330. if (!this.question || this.disabled) {
  331. return
  332. }
  333. this.disabled = true
  334. const question = this.question
  335. this.items.push({
  336. type: 2,
  337. user: '游客',
  338. content: question
  339. })
  340. this.scrollToBottom()
  341. const ask = {
  342. type: 1,
  343. content: {},
  344. showSnackbar: false,
  345. dataValidation: false,
  346. question, // 记录当前问题
  347. showMenu: false,
  348. model: {
  349. dataAxis: null,
  350. typeAxis: null
  351. }
  352. }
  353. this.items.push(ask)
  354. this.question = ''
  355. try {
  356. this.abortController = new AbortController()
  357. const { data } = await getAsk({
  358. question,
  359. user_id: this.userInfo.id,
  360. routing_mode: this.routingMode,
  361. conversation_id: this.conversationId
  362. }, {
  363. signal: this.abortController.signal
  364. })
  365. ask.content = data
  366. this.conversationId = data.conversation_id
  367. this.scrollToBottom()
  368. } catch (error) {
  369. ask.content = error.message
  370. } finally {
  371. this.disabled = false
  372. }
  373. },
  374. scrollToBottom () {
  375. this.$nextTick(() => {
  376. const box = this.$refs.chatBox
  377. if (!box) {
  378. return
  379. }
  380. box.scrollTop = box.scrollHeight
  381. })
  382. },
  383. async onAddTrain (item, bool) {
  384. this.loading = true
  385. try {
  386. await addFeedback({
  387. question: item.question,
  388. sql: item.content.sql,
  389. is_thumb_up: bool,
  390. user_id: this.userInfo.id
  391. })
  392. item.dataValidation = true
  393. // this.$snackbar.success('操作成功')
  394. } catch (error) {
  395. this.$snackbar.error(error)
  396. } finally {
  397. this.loading = false
  398. }
  399. },
  400. onRender ({ model, content }) {
  401. if (!model.dataAxis || !model.typeAxis) {
  402. this.$snackbar.error('请选择数据轴和类型轴')
  403. return
  404. }
  405. const { typeAxis, dataAxis } = model
  406. const data = {
  407. type: typeAxis ? content.records.rows.map(e => e[typeAxis]) : [],
  408. data: dataAxis ? dataAxis.map(e => content.records.rows.map(r => r[e])) : []
  409. }
  410. this.$emit('render', data)
  411. },
  412. async getConversationList () {
  413. try {
  414. const { data } = await getConversations({ limit: 5 })
  415. this.conversationList = data.conversations
  416. } catch (error) {
  417. this.$snackbar.error(error)
  418. }
  419. },
  420. async onSelectConversation (index) {
  421. if (!index) {
  422. setTimeout(() => {
  423. this.selected = this.lastSelected
  424. })
  425. return
  426. }
  427. this.lastSelected = index
  428. try {
  429. const { data } = await getConversationsById(this.conversationList[index].conversation_id)
  430. this.items.splice(1, this.items.length - 1, ...data.messages.map(e => {
  431. if (e.role === 'user') {
  432. return {
  433. type: 2,
  434. user: '游客',
  435. content: e.content
  436. }
  437. }
  438. return {
  439. type: 1,
  440. content: e.metadata,
  441. showSnackbar: false,
  442. dataValidation: false,
  443. question: e.content, // 记录当前问题
  444. showMenu: false,
  445. model: {
  446. dataAxis: null,
  447. typeAxis: null
  448. }
  449. }
  450. }))
  451. } catch (error) {
  452. this.$snackbar.error(error)
  453. }
  454. }
  455. }
  456. }
  457. </script>
  458. <style lang="scss" scoped>
  459. .heightFull {
  460. height: 100%;
  461. }
  462. .widthFull {
  463. width: 100%;
  464. }
  465. .chart-content {
  466. &-chat {
  467. border: 1px solid #ccc;
  468. &-box {
  469. height: 0;
  470. flex: 1;
  471. max-width: 800px;
  472. width: 100%;
  473. margin: 0 auto;
  474. }
  475. &-btn {
  476. .send {
  477. margin: 0 auto;
  478. max-width: 800px;
  479. }
  480. }
  481. }
  482. }
  483. .box-length-70 {
  484. max-width: 70%;
  485. }
  486. .send {
  487. // height: 130px;
  488. // margin: 20px 0;
  489. padding: 20px;
  490. &-box {
  491. width: 100%;
  492. // max-width: 800px;
  493. position: relative;
  494. .btn {
  495. position: absolute;
  496. right: 20px;
  497. bottom: 12px;
  498. }
  499. &-area {
  500. position: relative;
  501. bottom: 0;
  502. ::v-deep textarea {
  503. padding: 15px 70px 15px 0 !important;
  504. max-height: 300px;
  505. min-height: 60px;
  506. overflow: auto;
  507. margin: 0 !important;
  508. }
  509. }
  510. }
  511. }
  512. .position {
  513. &-relative {
  514. position: relative;
  515. }
  516. }
  517. .element {
  518. overflow: auto;
  519. scrollbar-width: none; /* Firefox */
  520. -ms-overflow-style: none; /* IE/Edge */
  521. }
  522. .element::-webkit-scrollbar {
  523. display: none; /* Chrome/Safari/Opera */
  524. }
  525. .history {
  526. position: absolute;
  527. right: 20px;
  528. top: 20px;
  529. z-index: 1;
  530. }
  531. </style>