dataChartEditChat.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  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. getAskThroughReact,
  230. addFeedback
  231. } from '@/api/dataChart'
  232. import { mapGetters } from 'vuex'
  233. export default {
  234. name: 'dataChartEditChat',
  235. inject: {
  236. react: {
  237. default: false
  238. }
  239. },
  240. data () {
  241. return {
  242. routingMode: undefined,
  243. chips: [
  244. { text: '聊天模式', value: 'chat_direct' },
  245. { text: '数据库模式', value: 'database_direct' }
  246. ],
  247. loading: false,
  248. disabled: false,
  249. question: '',
  250. items: [
  251. {
  252. type: 1,
  253. welcome: true,
  254. content: '你好,我是您的数据查询小助手,支持查询“高速公路服务区”的相关信息。您可以这样提问: ',
  255. items: [
  256. '现在一共有多少个服务区?分别归属于哪些管理公司?',
  257. '哪个服务区的档口数量最多?',
  258. '赣州分公司下,餐饮类档口的日均订单量是多少?'
  259. ]
  260. }
  261. ],
  262. abortController: null,
  263. conversationId: undefined
  264. // trueData: false
  265. }
  266. },
  267. computed: {
  268. ...mapGetters(['userInfo'])
  269. },
  270. methods: {
  271. onNew () {
  272. if (this.abortController) {
  273. this.abortController.abort('')
  274. }
  275. this.abortController = null
  276. this.items.splice(1, this.items.length - 1)
  277. this.conversationId = undefined
  278. this.$emit('new')
  279. },
  280. handleKeyCode (event) {
  281. if (event.keyCode === 13) {
  282. if (!event.ctrlKey) {
  283. event.preventDefault()
  284. this.handleSendMsg()
  285. } else {
  286. this.question += '\n'
  287. }
  288. }
  289. },
  290. onSend (str) {
  291. if (this.disabled) {
  292. return
  293. }
  294. this.question = str
  295. this.handleSendMsg()
  296. },
  297. async handleSendMsg () {
  298. if (!this.question || this.disabled) {
  299. return
  300. }
  301. this.disabled = true
  302. const question = this.question
  303. this.items.push({
  304. type: 2,
  305. user: '游客',
  306. content: question
  307. })
  308. this.scrollToBottom()
  309. const ask = {
  310. type: 1,
  311. content: {},
  312. showSnackbar: false,
  313. dataValidation: false,
  314. question, // 记录当前问题
  315. showMenu: false,
  316. model: {
  317. dataAxis: null,
  318. typeAxis: null
  319. }
  320. }
  321. this.items.push(ask)
  322. this.question = ''
  323. try {
  324. this.abortController = new AbortController()
  325. const getApi = this.react ? getAskThroughReact : getAsk
  326. const query = this.react
  327. ? {
  328. question,
  329. user_id: this.userInfo.id,
  330. conversation_id: this.conversationId
  331. }
  332. : {
  333. question,
  334. user_id: this.userInfo.id,
  335. routing_mode: this.routingMode,
  336. conversation_id: this.conversationId
  337. }
  338. const { data } = await getApi(query, {
  339. signal: this.abortController.signal
  340. })
  341. ask.content = data
  342. this.conversationId = data.conversation_id
  343. this.scrollToBottom()
  344. } catch (error) {
  345. ask.content = error.message ?? error
  346. } finally {
  347. this.disabled = false
  348. }
  349. },
  350. scrollToBottom () {
  351. this.$nextTick(() => {
  352. const box = this.$refs.chatBox
  353. if (!box) {
  354. return
  355. }
  356. box.scrollTop = box.scrollHeight
  357. })
  358. },
  359. async onAddTrain (item, bool) {
  360. this.loading = true
  361. try {
  362. await addFeedback({
  363. question: item.question,
  364. sql: item.content.sql,
  365. is_thumb_up: bool,
  366. user_id: this.userInfo.id
  367. })
  368. item.dataValidation = true
  369. // this.$snackbar.success('操作成功')
  370. } catch (error) {
  371. this.$snackbar.error(error)
  372. } finally {
  373. this.loading = false
  374. }
  375. },
  376. onRender ({ model, content }) {
  377. if (!model.dataAxis || !model.typeAxis) {
  378. this.$snackbar.error('请选择数据轴和类型轴')
  379. return
  380. }
  381. const { typeAxis, dataAxis } = model
  382. const data = {
  383. type: typeAxis ? content.records.rows.map(e => e[typeAxis]) : [],
  384. data: dataAxis ? dataAxis.map(e => content.records.rows.map(r => r[e])) : []
  385. }
  386. this.$emit('render', data)
  387. },
  388. update (data) {
  389. if (this.abortController && this.disabled) {
  390. this.abortController.abort('')
  391. this.abortController = null
  392. }
  393. this.items.splice(1, this.items.length - 1, ...data.messages.map(e => {
  394. if (e.role === 'user') {
  395. return {
  396. type: 2,
  397. user: '游客',
  398. content: e.content
  399. }
  400. }
  401. return {
  402. type: 1,
  403. content: e.metadata,
  404. showSnackbar: false,
  405. dataValidation: false,
  406. question: e.content, // 记录当前问题
  407. showMenu: false,
  408. model: {
  409. dataAxis: null,
  410. typeAxis: null
  411. }
  412. }
  413. }))
  414. }
  415. }
  416. }
  417. </script>
  418. <style lang="scss" scoped>
  419. .heightFull {
  420. height: 100%;
  421. }
  422. .widthFull {
  423. width: 100%;
  424. }
  425. .chart-content {
  426. &-chat {
  427. // border: 1px solid #ccc;
  428. &-box {
  429. height: 0;
  430. flex: 1;
  431. max-width: 800px;
  432. width: 100%;
  433. margin: 0 auto;
  434. }
  435. &-btn {
  436. .send {
  437. margin: 0 auto;
  438. max-width: 800px;
  439. }
  440. }
  441. }
  442. }
  443. .box-length-70 {
  444. max-width: 70%;
  445. }
  446. .send {
  447. // height: 130px;
  448. // margin: 20px 0;
  449. padding: 20px;
  450. &-box {
  451. width: 100%;
  452. // max-width: 800px;
  453. position: relative;
  454. .btn {
  455. position: absolute;
  456. right: 20px;
  457. bottom: 12px;
  458. }
  459. &-area {
  460. position: relative;
  461. bottom: 0;
  462. ::v-deep textarea {
  463. padding: 15px 70px 15px 0 !important;
  464. max-height: 300px;
  465. min-height: 60px;
  466. overflow: auto;
  467. margin: 0 !important;
  468. }
  469. }
  470. }
  471. }
  472. .position {
  473. &-relative {
  474. position: relative;
  475. }
  476. }
  477. .element {
  478. overflow: auto;
  479. scrollbar-width: none; /* Firefox */
  480. -ms-overflow-style: none; /* IE/Edge */
  481. }
  482. .element::-webkit-scrollbar {
  483. display: none; /* Chrome/Safari/Opera */
  484. }
  485. </style>