dataChartEditChat.vue 14 KB

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