zhengnaiwen_citu vor 1 Woche
Ursprung
Commit
91f0a4cc66

+ 69 - 0
src/api/dataChart.js

@@ -95,3 +95,72 @@ export function createTrainingData (param) {
 export function deleteTrainingData (param) {
   return http.post('/vanna/v0/training_data/delete', param)
 }
+
+// **************** 数据集自动生成和加载 ****************
+
+// 数据集自动生成和加载 创建数据训练任务
+export function createDataTasks (param) {
+  return http.post('/vanna/v0/data_pipeline/tasks', param)
+}
+
+// 数据集自动生成和加载 执行数据训练任务
+export function executeDataTasks (taskId, param) {
+  return http.post(`/vanna/v0/data_pipeline/tasks/${taskId}/execute`, param)
+}
+
+// 数据集自动生成和加载 获取任务状态
+export function getDataTasksStatus (taskId, param) {
+  return http.get(`/vanna/v0/data_pipeline/tasks/${taskId}`, param)
+}
+
+// 数据集自动生成和加载 获取任务列表
+export function getDataTasksList (param) {
+  return http.get('/vanna/v0/data_pipeline/tasks', param)
+}
+
+// 数据集自动生成和加载 删除任务(批量)
+export function deleteDataTasks (param) {
+  return http.del('/vanna/v0/data_pipeline/tasks', param)
+}
+
+// ***************** 表名清单管理 *****************
+
+// 表名清单管理 查询数据库表列表
+export function getDataBaseList (param) {
+  return http.post('/vanna/v0/database/tables', param)
+}
+
+// 表名清单管理 在线提交表名列表
+export function saveDataBase (taskId, param) {
+  return http.post(`/vanna/v0/data_pipeline/tasks/${taskId}/table-list`, param)
+}
+// 表名清单管理 上传表清单文件
+export function uploadDataTasksList (taskId, param) {
+  return http.upload(`/vanna/v0/data_pipeline/tasks/${taskId}/upload-table-li`, param)
+}
+// 表名清单管理 获取表清单文件信息
+export function getDataTasksFileList (param) {
+  return http.get('/vanna/v0/data_pipeline/tasks/{task_id}/table-list-info', param)
+}
+
+// *************** 训练数据文件管理 ****************
+
+// 训练数据文件管理 查看任务文件列表
+export function getTasksFileList (taskId, param) {
+  return http.get(`/vanna/v0/data_pipeline/tasks/${taskId}/files`, param)
+}
+
+// 训练数据文件管理 查看任务文件列表
+export function downloadTasksFileList (taskId, fileName) {
+  return http.getDownload(`/vanna/v0/data_pipeline/tasks/${taskId}/files/${fileName}`)
+}
+
+// 表名清单管理 上传文件到任务目录
+export function uploadTasksList (taskId, param) {
+  return http.upload(`/vanna/v0/data_pipeline/tasks/${taskId}/files`, param)
+}
+
+// 表名清单管理 日志
+export function getTasksLog (taskId, param) {
+  return http.post(`/vanna/v0/data_pipeline/tasks/${taskId}/logs/query`, param)
+}

+ 0 - 116
src/components/MForm/index.vue

@@ -26,35 +26,6 @@
                       @change="handleChange(item)"
                       @input="item.onInput && item.onInput(item)"
                   />
-                  <!-- <v-text-field
-                      v-if="['text', 'password', 'number'].includes(item.type)"
-                      v-model="query[item.key]"
-                      :hide-details="item.hideDetails ?? false"
-                      :dense="item.dense ?? true"
-                      :style="{width: item.width}"
-                      :placeholder="item.placeholder || item.label"
-                      :outlined="item.outlined ?? true"
-                      :type="item.type"
-                      :rules="item.rules"
-                      :disabled="item.disabled"
-                      :color="item.color"
-                      :label="item.label"
-                      :autofocus="item.autofocus"
-                      :required="item.required"
-                      :class="item.class"
-                      :suffix="item.suffix"
-                      :append-icon="item.appendIcon"
-                      :clearable="item.clearable"
-                      :readonly="item.readonly"
-                      :prepend-inner-icon="item.prependInnerIcon"
-                      hide-spin-buttons
-                      @wheel.native="$event => handleWheel($event, item)"
-                      @keyup.enter.native="item.keyupEnterNative && item.keyupEnterNative(index)"
-                      @click="item.click && item.click(index)"
-                      @click:append="item.clickAppend && item.clickAppend(index)"
-                      @change="handleChange(item)"
-                      @input="item.onInput && item.onInput(item)"
-                  /> -->
                   <v-autocomplete
                       v-if="item.type === 'autocomplete'"
                       v-model="query[item.key]"
@@ -77,93 +48,6 @@
                       <slot :name="item.prependItem" :item="item"></slot>
                     </template>
                   </v-autocomplete>
-                  <!-- <v-autocomplete
-                      v-if="item.type === 'autocomplete'"
-                      v-model="query[item.key]"
-                      :attach="!item.noAttach"
-                      :placeholder="item.placeholder || item.label"
-                      :item-text="item.itemText || 'label'"
-                      :item-value="item.itemValue || 'value'"
-                      :outlined="item.outlined ?? true"
-                      :dense="item.dense ?? true"
-                      :search-input.sync="item.searchInput"
-                      :hide-details="item.hideDetails ?? false"
-                      :no-data-text="item.noDataText || 'No data available'"
-                      :rules="item.rules"
-                      :loading="item.loading"
-                      :label="item.label"
-                      :items="item.items"
-                      :disabled="item.disabled"
-                      :multiple="item.multiple"
-                      :clearable="item.clearable"
-                      :readonly="item.readonly"
-                      :hide-no-data="item.hideNoData"
-                      :hide-selected="item.hideSelected"
-                      :return-object="item.returnObject"
-                      @change="handleChange(item)"
-                  >
-                    <template v-if="item.slotAppendItem" v-slot:append-item>
-                      <slot :name="item.slotAppendItem" :item="item"></slot>
-                    </template>
-                    <template v-if="item.prependItem" #prepend-item>
-                      <slot :name="item.prependItem" :item="item"></slot>
-                    </template>
-                  </v-autocomplete> -->
-                  <!-- autocomplete2 多选纸片样式 -->
-                  <v-autocomplete
-                      v-if="item.type === 'autocomplete2'"
-                      v-model="query[item.key]"
-                      :rules="item.rules"
-                      :attach="!item.noAttach"
-                      :loading="item.loading"
-                      :label="item.label"
-                      :placeholder="item.placeholder || item.label"
-                      :items="item.canCreate ? [inputUpdateValue, ...item.items].filter(Boolean) : item.items"
-                      :item-text="item.itemText || 'label'"
-                      :item-value="item.itemValue || 'value'"
-                      :outlined="item.outlined ?? true"
-                      :dense="item.dense ?? true"
-                      :multiple="item.multiple"
-                      :clearable="item.clearable"
-                      :search-input.sync="item.searchInput"
-                      :hide-no-data="item.hideNoData"
-                      :hide-selected="item.hideSelected"
-                      :readonly="item.readonly"
-                      @change="handleChange(item)"
-                      @update:search-input="$event => item.canCreate ? inputUpdateValue = $event : inputUpdateAutocomplete($event)"
-                      :hide-details="item.hideDetails ?? false"
-                      deletable-chips
-                      cache-items
-                      small-chips
-                    ></v-autocomplete>
-                  <v-combobox
-                      v-if="item.type === 'combobox'"
-                      :rules="item.rules"
-                      v-model="query[item.key]"
-                      :attach="true"
-                      :label="item.label"
-                      :placeholder="item.placeholder || item.label"
-                      :items="item.items"
-                      :item-text="item.itemText || 'label'"
-                      :item-value="item.itemValue || 'value'"
-                      :outlined="item.outlined ?? true"
-                      :dense="item.dense ?? true"
-                      :clearable="item.clearable"
-                      :disabled="item.disabled"
-                      @change="handleChange(item)"
-                  >
-                    <template v-if="item.hasIcon" v-slot:selection="data">
-                      <v-icon color="blue darken-2">{{ data.item.label }}</v-icon>
-                    </template>
-                    <template v-if="item.hasIcon" v-slot:item="data">
-                      <v-list-item-avatar>
-                        <v-icon>{{ data.item.label }}</v-icon>
-                      </v-list-item-avatar>
-                      <v-list-item-content>
-                        {{ data.item.label }}
-                      </v-list-item-content>
-                    </template>
-                  </v-combobox>
                   <v-textarea
                     v-if="item.type === 'textarea'"
                     :rules="item.rules"

+ 26 - 3
src/router/routes.js

@@ -2458,20 +2458,43 @@ export default {
           hidden: 0,
           icon: '',
           type: 1,
-          title: '训练数据管理',
+          title: '会话数据管理',
+          path: '/model-system/model-train-manage',
+          children: [],
+          enName: 'model-train-manage',
+          redirect: '',
+          active: '',
+          label: '会话数据管理',
+          sort: 10,
+          component: 'modelSystem/modelTrainManage',
+          meta: {
+            roles: [],
+            enName: 'model-train-manage',
+            icon: '',
+            title: '会话数据管理',
+            fullScreen: false
+          },
+          name: 'model-train',
+          alwaysShow: 0
+        },
+        {
+          hidden: 0,
+          icon: '',
+          type: 1,
+          title: '模型训练',
           path: '/model-system/model-train',
           children: [],
           enName: 'model-train',
           redirect: '',
           active: '',
-          label: '训练数据管理',
+          label: '模型训练',
           sort: 10,
           component: 'modelSystem/modelTrain',
           meta: {
             roles: [],
             enName: 'model-train',
             icon: '',
-            title: '训练数据管理',
+            title: '模型训练',
             fullScreen: false
           },
           name: 'model-train',

+ 109 - 186
src/views/modelSystem/modelTrain/index.vue

@@ -1,242 +1,165 @@
 <template>
-  <div class="content white">
-    <v-banner class="mb-3">
-      训练数据统计
-      <template v-slot:actions>
-        <v-btn
-          text
-          color="primary"
-          @click="getStatistics"
-        >
-          <v-icon left>mdi-refresh</v-icon>
-          刷新
-        </v-btn>
-        <!-- <v-btn
-          text
-          color="deep-purple accent-4"
-          @click="onClearRedisCache"
-        >
-          清除对话缓存
-        </v-btn> -->
+  <div class="pa-3 white">
+    <m-filter :option="filter" @search="handleSearch" />
+    <m-table
+      class="mt-3"
+      :loading="loading"
+      :headers="headers"
+      :items="items"
+      :total="total"
+      :page-info="pageInfo"
+      @add="handleAdd"
+      @delete="handleDelete"
+      @pageHandleChange="pageHandleChange"
+      @sort="handleSort"
+    >
+      <template #status="{item}">
+        <v-chip :color="item.status === 'failed' ? 'error' : 'success'" small>{{ item.status }}</v-chip>
       </template>
-    </v-banner>
-    <v-container v-loading="loading">
-      <v-row >
-        <v-col
-          v-for="elevation in elevations"
-          :key="elevation.value"
-        >
-          <v-card
-            class="pa-2"
-            tile
-            height="150"
-          >
-            <div class="d-flex align-center justify-center flex-column" style="height: 100%;">
-              <div class="text-h3" :class="`${elevation.textColor}--text`">{{ itemData[elevation.value] }}</div>
-              <div>
-                {{ elevation.text }}
-                <span class="success--text">
-                  {{ itemData.type_percentages?.[elevation.value] ? `( ${itemData.type_percentages?.[elevation.value]}% )` : null }}
-                </span>
-              </div>
-            </div>
-          </v-card>
-        </v-col>
-      </v-row>
-
-    </v-container>
-    <v-divider class="my-3"></v-divider>
-    <div class="pa-3">
-      <MSearch class="mb-3" :option="filter" @search="handleSearch"></MSearch>
-      <MTable
-        :headers="headers"
-        :items="items"
-        :loading="tableLoading"
-        :total="total"
-        :page-info="pageInfo"
-        @edit="handleEdit"
-        @delete="handleDelete"
-        @pageHandleChange="pageHandleChange"
-        @sort="handleSort"
-        @add="handleAdd"
-      >
-        <template #question="{ item }">
-          <span style="max-width: 300px;" class="d-inline-block text-truncate">{{ item.question }}</span>
-        </template>
-        <template #content="{ item }">
-          <!-- <tooltip width="300" :text="item.content" /> -->
-          <span style="max-width: 300px;" class="d-inline-block text-truncate">{{ item.content }}</span>
-        </template>
-        <template #actions="{ item }">
-          <v-btn text color="primary" @click="handleDetails(item)">查看</v-btn>
-          <!-- <v-btn text color="primary" @click="handleEdit(item)">编辑</v-btn> -->
-          <v-btn text color="error" @click="handleDelete([item.id])">删除</v-btn>
-        </template>
-      </MTable>
-    </div>
-    <ModelTrainEdit ref="modelTrainEditRefs" @success="onOpen"></ModelTrainEdit>
+      <template #actions="{ item }">
+        <v-btn color="success" text @click="handleExecute(item.task_id)">执行</v-btn>
+        <v-btn color="primary" text @click="handleFindExecute(item.task_id)">查看执行进度</v-btn>
+        <v-btn color="primary" text @click="handleTable(item.task_id)">表名清单</v-btn>
+        <v-btn color="primary" text @click="handleLog(item.task_id)">查看日志</v-btn>
+        <v-btn color="error" text @click="handleDelete([item.task_id])">删除</v-btn>
+      </template>
+    </m-table>
+    <ModelTrainEdit ref="modelTrainEditRefs" @success="init"></ModelTrainEdit>
+    <ModelTrainStatus ref="modelTrainStatusRefs"></ModelTrainStatus>
+    <ModelTrainTable ref="modelTrainTableRefs"></ModelTrainTable>
+    <ModelTrainLog ref="modelTrainLogRefs"></ModelTrainLog>
   </div>
 </template>
 
 <script>
-import {
-  getTrainingData,
-  getTrainingDataList,
-  deleteTrainingData
-} from '@/api/dataChart'
+import MFilter from '@/components/Filter'
 import MTable from '@/components/List/table.vue'
-import MSearch from '@/components/Filter'
 import ModelTrainEdit from './modelTrainEdit.vue'
+import ModelTrainStatus from './modelTrainStatus.vue'
+import ModelTrainTable from './modelTrainTable.vue'
+import ModelTrainLog from './modelTrainLog.vue'
+import {
+  // createDataTasks,
+  executeDataTasks,
+  getDataTasksList,
+  deleteDataTasks
+} from '@/api/dataChart'
 export default {
-  name: 'ModelTrain',
+  name: 'modelTrain',
   components: {
+    MFilter,
     MTable,
-    MSearch,
-    ModelTrainEdit
+    ModelTrainEdit,
+    ModelTrainStatus,
+    ModelTrainTable,
+    ModelTrainLog
   },
   data () {
     return {
-      searchValues: {},
+      loading: false,
+      show: false,
       filter: {
         list: [
-          { type: 'textField', value: null, label: '关键词', key: 'search_keyword', placeholder: '请输入关键词' },
-          {
-            type: 'autocomplete',
-            key: 'training_data_type',
-            value: null,
-            label: '类型 *',
-            placeholder: '请选择类型',
-            items: [
-              { label: 'sql', value: 'sql' },
-              { label: 'documentation', value: 'documentation' },
-              { label: 'ddl', value: 'ddl' },
-              { label: 'error_sql', value: 'error_sql' }
-            ]
-          }
+          { type: 'textField', value: '', label: '任务名称', key: 'task_name' }
         ]
       },
-      loading: false,
-      elevations: [
-        { text: '总数据量', value: 'total_count', textColor: 'primary' },
-        { text: 'DDL类型', value: 'ddl', textColor: 'info' },
-        { text: 'SQL类型', value: 'sql', textColor: 'info' },
-        { text: '文档类型', value: 'documentation', textColor: 'info' },
-        { text: '错误SQL类型', value: 'error_sql', textColor: 'error' }
-      ],
-      itemData: {},
-      tableLoading: false,
+      queryData: {
+        task_name: null
+      },
       headers: [
-        { text: 'ID', value: 'id' },
-        { text: '问题', value: 'question' },
-        { text: '类型', value: 'training_data_type' },
-        { text: '内容', value: 'content' },
-        { text: '操作', value: 'actions' }
+        { text: '任务名称', align: 'start', value: 'task_name' },
+        { text: '数据库名称', align: 'start', value: 'db_name' },
+        { text: '上下文', align: 'start', value: 'business_context' },
+        { text: '状态', align: 'center', value: 'status' },
+        { text: '创建者', align: 'center', value: 'created_by' },
+        { text: '操作', align: 'start', value: 'actions' }
       ],
+      itemData: {},
       items: [],
-      total: 0,
       orders: [],
       pageInfo: {
-        size: 5,
+        size: 10,
         current: 1
-      }
+      },
+      total: 0
     }
   },
   created () {
-    this.onOpen()
+    this.init()
   },
   methods: {
-    onOpen () {
-      this.init()
-      this.getStatistics()
-    },
-    async getStatistics () {
-      this.loading = true
-      try {
-        const { data } = await getTrainingData()
-        this.itemData = {
-          ...data,
-          ...data.type_breakdown
-        }
-      } catch (error) {
-        this.$snackbar.error(error)
-      } finally {
-        this.loading = false
-      }
-    },
     async init () {
-      this.tableLoading = true
-      const orders = {
-        sort_by: undefined,
-        sort_order: undefined
-      }
-      if (this.orders.length) {
-        console.log(this.orders[0])
-        Object.assign(orders, {
-          sort_by: this.orders[0].column.replace(/`/g, ''),
-          sort_order: this.orders[0].asc ? 'asc' : 'desc'
-        })
-      }
+      this.loading = true
       try {
-        const { data } = await getTrainingDataList({
+        const { data } = await getDataTasksList({
+          ...this.queryData,
           page: this.pageInfo.current,
-          page_size: this.pageInfo.size,
-          ...this.searchValues,
-          ...orders
+          page_size: this.pageInfo.size
         })
-        this.items = data.records
-        this.total = data.pagination.total
+        this.items = data.tasks
+        this.total = data.total
       } catch (error) {
         this.$snackbar.error(error)
       } finally {
-        this.tableLoading = false
+        this.loading = false
       }
     },
-    handleSearch (v) {
-      this.searchValues = v
+    handleSearch (val) {
+      Object.assign(this.queryData, val)
       this.pageInfo.current = 1
       this.init()
     },
     handleAdd () {
       this.$refs.modelTrainEditRefs.open()
     },
-    handleDetails (item) {
-      this.$refs.modelTrainEditRefs.open(item, true)
+    // handleEdit (item) {
+    //   this.itemData = item
+    //   this.show = true
+    // },
+    handleFindExecute (id) {
+      this.$refs.modelTrainStatusRefs.open(id)
     },
-    pageHandleChange (index) {
-      this.pageInfo.current = index
-      this.init()
+    handleTable (id) {
+      this.$refs.modelTrainTableRefs.open(id)
     },
-    handleSort (v) {
-      this.orders = v
-      this.init()
+    handleLog (id) {
+      this.$refs.modelTrainLogRefs.open(id)
     },
-    handleEdit (item) {
-      this.$refs.modelTrainEditRefs.open(item)
+    async handleExecute (id) {
+      try {
+        const { data } = await executeDataTasks(id, {
+          execution_mode: 'complete'
+        })
+        this.$snackbar.success(data.response)
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
     },
     handleDelete (ids) {
-      if (Array.isArray(ids) && !ids.length) {
-        this.$snackbar.warning('请选择要删除的数据')
-        return
-      }
-      this.$confirm('提示', '确定删除吗?').then(async () => {
-        try {
-          await deleteTrainingData({
-            ids,
-            confirm: true
-          })
-          this.$snackbar.success('删除成功')
-          this.onOpen()
-        } catch (error) {
-          this.$snackbar.error(error)
-        }
-      })
+      if (Array.isArray(ids) && !ids.length) return this.$snackbar.warning('请选择要删除的选项')
+      this.$confirm('提示', '是否删除该选项')
+        .then(async () => {
+          try {
+            await deleteDataTasks({ ids })
+            this.$snackbar.success('删除成功')
+            this.init()
+          } catch (error) {
+            this.$snackbar.error('删除失败')
+          }
+        })
+    },
+    pageHandleChange (page) {
+      this.pageInfo.current = page
+      this.init()
+    },
+    handleSort (val) {
+      this.orders = val
+      this.init()
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
-.content {
-  font-size: 16px;
-}
+
 </style>

+ 37 - 51
src/views/modelSystem/modelTrain/modelTrainEdit.vue

@@ -1,83 +1,69 @@
 <template>
-  <MDialog title="模型训练" width="600px" :visible.sync="visible" :footer="!readonly" @submit="onSubmit">
+  <m-dialog title="创建任务" :visible.sync="show" @submit="handleSubmit">
     <MForm ref="form" :items="formItems" v-model="formValues"></MForm>
-  </MDialog>
+  </m-dialog>
 </template>
 
 <script>
 import MDialog from '@/components/Dialog'
 import MForm from '@/components/MForm'
 import {
-  createTrainingData
+  createDataTasks
 } from '@/api/dataChart'
 export default {
   name: 'modelTrainEdit',
-  components: {
-    MDialog,
-    MForm
-  },
+  components: { MDialog, MForm },
   data () {
     return {
-      visible: false,
-      readonly: false,
-      formValues: {}
-    }
-  },
-  computed: {
-    formItems () {
-      return [
+      show: false,
+      formValues: {},
+      formItems: [
         {
-          label: '提问内容 *',
-          key: 'question',
-          readonly: this.readonly,
-          type: 'textarea',
-          rules: [v => this.readonly || !!v || '请输入提问内容']
+          label: '任务名称',
+          key: 'task_name',
+          type: 'text',
+          rules: [v => !!v || '请输入任务名称']
         },
         {
-          label: '数据类型',
-          key: 'training_data_type',
-          type: 'autocomplete',
-          readonly: this.readonly,
-          items: [
-            { label: 'sql', value: 'sql' },
-            { label: 'documentation', value: 'documentation' },
-            { label: 'ddl', value: 'ddl' },
-            { label: 'error_sql', value: 'error_sql' }
-          ],
-          rules: [v => this.readonly || !!v || '请选择数据类型']
+          label: 'PostgreSQL连接字符串',
+          key: 'db_connection',
+          type: 'text',
+          rules: [v => !!v || '请输入PostgreSQL连接字符串']
         },
         {
-          label: '回答内容 *',
-          key: 'content',
-          readonly: this.readonly,
-          rows: 5,
+          label: '数据库名称',
+          key: 'db_name',
+          type: 'text',
+          rules: [v => !!v || '请输入数据库名称']
+        },
+        {
+          label: '业务上下文描述',
+          key: 'business_context',
           type: 'textarea',
-          rules: [v => this.readonly || !!v || '请输入回答内容']
+          rules: [v => !!v || '请输入业务上下文描述']
         }
       ]
     }
   },
   methods: {
-    open (item, readonly = false) {
-      this.readonly = readonly
-      this.visible = true
-      if (!item) {
-        this.formValues = {}
-        this.$nextTick(() => {
-          this.$refs.form.resetValidation()
-        })
+    open (item) {
+      this.show = true
+    },
+    async handleSubmit () {
+      const check = this.$refs.form.validate()
+      if (!check) {
         return
       }
-      this.formValues = { ...item }
-      this.$refs.form.resetValidation()
-    },
-    async onSubmit () {
       try {
-        await createTrainingData({
-          data: this.formValues
+        await createDataTasks({
+          ...this.formValues,
+          enable_sql_validation: true,
+          enable_llm_repair: true,
+          modify_original_file: true,
+          enable_training_data_load: true
         })
         this.$snackbar.success('创建成功')
-        this.visible = false
+        this.show = false
         this.$emit('success')
       } catch (error) {
         this.$snackbar.error(error)

+ 83 - 0
src/views/modelSystem/modelTrain/modelTrainLog.vue

@@ -0,0 +1,83 @@
+<template>
+  <v-navigation-drawer
+      v-model="drawer"
+      absolute
+      right
+      temporary
+      overlay-opacity="0"
+      width="700px"
+  >
+    <div class="pa-3 white sticky d-flex align-center justify-end">
+      <DatePicker
+        v-model="date"
+        ref="picker"
+        :option="{
+          range: true,
+          placeholder: '请选择时间',
+          type: 'date',
+          clearable: false
+        }"
+        @change="init"
+      ></DatePicker>
+      <!-- <v-btn rounded color="primary" class="ml-3 half-button">查 询</v-btn> -->
+    </div>
+    <v-timeline
+      :reverse="reverse"
+      dense
+    >
+      <v-timeline-item
+        v-for="(item, index) in items"
+        :key="index"
+        :color="item.level.toLowerCase()"
+      >
+      <div>{{ item.timestamp }} </div>
+      <div :class="{'red--text' : item.level === 'ERROR'}">{{ item.message }}</div>
+
+      </v-timeline-item>
+    </v-timeline>
+  </v-navigation-drawer>
+</template>
+
+<script>
+import DatePicker from '@/components/Form/datePicker.vue'
+import {
+  getTasksLog
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainLog',
+  components: {
+    DatePicker
+  },
+  data () {
+    return {
+      drawer: false,
+      date: null,
+      items: [],
+      id: null
+    }
+  },
+  methods: {
+    async open (id) {
+      this.id = id
+      this.drawer = true
+      this.init()
+    },
+    async init () {
+      try {
+        const { data } = await getTasksLog(this.id, {})
+        this.items = data.logs
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.sticky {
+  position: sticky;
+  top: 0;
+  z-index: 3;
+}
+</style>

+ 71 - 0
src/views/modelSystem/modelTrain/modelTrainStatus.vue

@@ -0,0 +1,71 @@
+<template>
+  <m-dialog title="任务进度" :visible.sync="show" :footer="false">
+    <v-stepper
+      alt-labels
+      class="my-12"
+      elevation="0"
+      v-loading="loading"
+    >
+      <v-stepper-header>
+        <template
+          v-for="(status, index) in statusItems"
+        >
+          <v-stepper-step
+            :key="status.key"
+            :step="index + 1"
+            :complete="status.value === 'completed'"
+            :rules="[() => status.value !== 'failed']"
+          >
+            {{ status.key }}
+            <div class="mt-3">
+              <v-chip>{{ status.value }}</v-chip>
+            </div>
+          </v-stepper-step>
+
+          <v-divider :key="status.key" v-if="index < statusItems.length - 1"></v-divider>
+        </template>
+      </v-stepper-header>
+    </v-stepper>
+  </m-dialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import { getDataTasksStatus } from '@/api/dataChart'
+export default {
+  name: 'modelTrainStatus',
+  components: {
+    MDialog
+  },
+  data () {
+    return {
+      show: false,
+      loading: false,
+      statusItems: []
+    }
+  },
+  methods: {
+    async open (id) {
+      this.show = true
+      this.loading = true
+      try {
+        const { data } = await getDataTasksStatus(id)
+        this.statusItems = Object.keys(data.step_status).map(key => {
+          return {
+            key,
+            value: data.step_status[key]
+          }
+        })
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 90 - 0
src/views/modelSystem/modelTrain/modelTrainTable.vue

@@ -0,0 +1,90 @@
+<template>
+  <m-dialog title="表名清单" :visible.sync="show" :footer="false">
+    <div class="mb-3">
+      <v-btn class="mr-3" color="primary" rounded @click="handleAdd">修改表名</v-btn>
+      <UploadBtn color="primary" @change="handleChangeFile">导入表名</UploadBtn>
+    </div>
+    <v-list
+      subheader
+      v-loading="loading"
+    >
+      <v-list-item
+        v-for="item in items"
+        :key="item"
+      >
+        <v-list-item-content>
+          <v-list-item-title>{{ item }}</v-list-item-title>
+        </v-list-item-content>
+      </v-list-item>
+    </v-list>
+    <ModelTrainTableSubmit ref="modelTrainTableSubmitRefs" @success="getData"></ModelTrainTableSubmit>
+  </m-dialog>
+</template>
+
+<script>
+import UploadBtn from '@/components/UploadBtn'
+import MDialog from '@/components/Dialog'
+import ModelTrainTableSubmit from './modelTrainTableSubmit'
+import {
+  getDataBaseList,
+  uploadDataTasksList
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainTable',
+  components: {
+    MDialog,
+    UploadBtn,
+    ModelTrainTableSubmit
+  },
+  data () {
+    return {
+      show: false,
+      loading: false,
+      items: [],
+
+      id: null
+    }
+  },
+  methods: {
+    open (id) {
+      this.id = id
+      this.show = true
+      this.getData()
+    },
+    async getData () {
+      try {
+        this.loading = true
+        const { data } = await getDataBaseList({
+          task_id: this.id
+        })
+        this.items = data.tables
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    handleAdd () {
+      this.$refs.modelTrainTableSubmitRefs.open(this.id)
+    },
+    async handleChangeFile (file) {
+      const query = new FormData()
+      query.append('file', file)
+      this.loading = true
+      try {
+        await uploadDataTasksList(this.id, query)
+        this.$snackbar.success('更新成功')
+        this.getData()
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 63 - 0
src/views/modelSystem/modelTrain/modelTrainTableSubmit.vue

@@ -0,0 +1,63 @@
+<template>
+  <m-dialog title="修改表名" :visible.sync="show" @submit="handleSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues" v-loading="loading"></MForm>
+  </m-dialog>
+</template>
+
+<script>
+import MForm from '@/components/MForm'
+import MDialog from '@/components/Dialog'
+import {
+  saveDataBase
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainTableSubmit',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      show: false,
+      formItems: [
+        {
+          label: 'tables',
+          key: 'tables',
+          type: 'textarea',
+          placeholder: '请输入表名,表名使用逗号分隔,支持 schema.table 的格式',
+          rows: 8,
+          rules: [v => !!v || '不能为空']
+        }
+      ],
+      formValues: {},
+      id: null,
+      loading: false
+    }
+  },
+  methods: {
+    open (id) {
+      this.id = id
+      this.show = true
+    },
+    async handleSubmit () {
+      if (!this.$refs.form.validate()) return
+      this.loading = true
+      try {
+        await saveDataBase(this.id, this.formValues)
+        this.$snackbar.success('更新成功')
+        this.show = false
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    }
+
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 233 - 0
src/views/modelSystem/modelTrainManage/index.vue

@@ -0,0 +1,233 @@
+<template>
+  <div class="content white">
+    <v-banner class="mb-3">
+      训练数据统计
+      <template v-slot:actions>
+        <v-btn
+          text
+          color="primary"
+          @click="getStatistics"
+        >
+          <v-icon left>mdi-refresh</v-icon>
+          刷新
+        </v-btn>
+      </template>
+    </v-banner>
+    <v-container v-loading="loading">
+      <v-row >
+        <v-col
+          v-for="elevation in elevations"
+          :key="elevation.value"
+        >
+          <v-card
+            class="pa-2"
+            tile
+            height="150"
+          >
+            <div class="d-flex align-center justify-center flex-column" style="height: 100%;">
+              <div class="text-h3" :class="`${elevation.textColor}--text`">{{ itemData[elevation.value] }}</div>
+              <div>
+                {{ elevation.text }}
+                <span class="success--text">
+                  {{ itemData.type_percentages?.[elevation.value] ? `( ${itemData.type_percentages?.[elevation.value]}% )` : null }}
+                </span>
+              </div>
+            </div>
+          </v-card>
+        </v-col>
+      </v-row>
+
+    </v-container>
+    <v-divider class="my-3"></v-divider>
+    <div class="pa-3">
+      <MSearch class="mb-3" :option="filter" @search="handleSearch"></MSearch>
+      <MTable
+        :headers="headers"
+        :items="items"
+        :loading="tableLoading"
+        :total="total"
+        :page-info="pageInfo"
+        @edit="handleEdit"
+        @delete="handleDelete"
+        @pageHandleChange="pageHandleChange"
+        @sort="handleSort"
+        @add="handleAdd"
+      >
+        <template #question="{ item }">
+          <span style="max-width: 300px;" class="d-inline-block text-truncate">{{ item.question }}</span>
+        </template>
+        <template #content="{ item }">
+          <span style="max-width: 300px;" class="d-inline-block text-truncate">{{ item.content }}</span>
+        </template>
+        <template #actions="{ item }">
+          <v-btn text color="primary" @click="handleDetails(item)">查看</v-btn>
+          <v-btn text color="error" @click="handleDelete([item.id])">删除</v-btn>
+        </template>
+      </MTable>
+    </div>
+    <ModelTrainManageEdit ref="ModelTrainManageEditRefs" @success="onOpen"></ModelTrainManageEdit>
+  </div>
+</template>
+
+<script>
+import {
+  getTrainingData,
+  getTrainingDataList,
+  deleteTrainingData
+} from '@/api/dataChart'
+import MTable from '@/components/List/table.vue'
+import MSearch from '@/components/Filter'
+import ModelTrainManageEdit from './modelTrainManageEdit.vue'
+export default {
+  name: 'ModelTrain',
+  components: {
+    MTable,
+    MSearch,
+    ModelTrainManageEdit
+  },
+  data () {
+    return {
+      searchValues: {},
+      filter: {
+        list: [
+          { type: 'textField', value: null, label: '关键词', key: 'search_keyword', placeholder: '请输入关键词' },
+          {
+            type: 'autocomplete',
+            key: 'training_data_type',
+            value: null,
+            label: '类型 *',
+            placeholder: '请选择类型',
+            items: [
+              { label: 'sql', value: 'sql' },
+              { label: 'documentation', value: 'documentation' },
+              { label: 'ddl', value: 'ddl' },
+              { label: 'error_sql', value: 'error_sql' }
+            ]
+          }
+        ]
+      },
+      loading: false,
+      elevations: [
+        { text: '总数据量', value: 'total_count', textColor: 'primary' },
+        { text: 'DDL类型', value: 'ddl', textColor: 'info' },
+        { text: 'SQL类型', value: 'sql', textColor: 'info' },
+        { text: '文档类型', value: 'documentation', textColor: 'info' },
+        { text: '错误SQL类型', value: 'error_sql', textColor: 'error' }
+      ],
+      itemData: {},
+      tableLoading: false,
+      headers: [
+        { text: 'ID', value: 'id' },
+        { text: '问题', value: 'question' },
+        { text: '类型', value: 'training_data_type' },
+        { text: '内容', value: 'content' },
+        { text: '操作', value: 'actions' }
+      ],
+      items: [],
+      total: 0,
+      orders: [],
+      pageInfo: {
+        size: 5,
+        current: 1
+      }
+    }
+  },
+  created () {
+    this.onOpen()
+  },
+  methods: {
+    onOpen () {
+      this.init()
+      this.getStatistics()
+    },
+    async getStatistics () {
+      this.loading = true
+      try {
+        const { data } = await getTrainingData()
+        this.itemData = {
+          ...data,
+          ...data.type_breakdown
+        }
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.loading = false
+      }
+    },
+    async init () {
+      this.tableLoading = true
+      const orders = {
+        sort_by: undefined,
+        sort_order: undefined
+      }
+      if (this.orders.length) {
+        console.log(this.orders[0])
+        Object.assign(orders, {
+          sort_by: this.orders[0].column.replace(/`/g, ''),
+          sort_order: this.orders[0].asc ? 'asc' : 'desc'
+        })
+      }
+      try {
+        const { data } = await getTrainingDataList({
+          page: this.pageInfo.current,
+          page_size: this.pageInfo.size,
+          ...this.searchValues,
+          ...orders
+        })
+        this.items = data.records
+        this.total = data.pagination.total
+      } catch (error) {
+        this.$snackbar.error(error)
+      } finally {
+        this.tableLoading = false
+      }
+    },
+    handleSearch (v) {
+      this.searchValues = v
+      this.pageInfo.current = 1
+      this.init()
+    },
+    handleAdd () {
+      this.$refs.ModelTrainManageEditRefs.open()
+    },
+    handleDetails (item) {
+      this.$refs.ModelTrainManageEditRefs.open(item, true)
+    },
+    pageHandleChange (index) {
+      this.pageInfo.current = index
+      this.init()
+    },
+    handleSort (v) {
+      this.orders = v
+      this.init()
+    },
+    handleEdit (item) {
+      this.$refs.ModelTrainManageEditRefs.open(item)
+    },
+    handleDelete (ids) {
+      if (Array.isArray(ids) && !ids.length) {
+        this.$snackbar.warning('请选择要删除的数据')
+        return
+      }
+      this.$confirm('提示', '确定删除吗?').then(async () => {
+        try {
+          await deleteTrainingData({
+            ids,
+            confirm: true
+          })
+          this.$snackbar.success('删除成功')
+          this.onOpen()
+        } catch (error) {
+          this.$snackbar.error(error)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.content {
+  font-size: 16px;
+}
+</style>

+ 131 - 0
src/views/modelSystem/modelTrainManage/modelTrainManageEdit.vue

@@ -0,0 +1,131 @@
+<template>
+  <MDialog title="模型训练" width="600px" :visible.sync="visible" :footer="!readonly" @submit="onSubmit">
+    <MForm ref="form" :items="formItems" v-model="formValues"></MForm>
+  </MDialog>
+</template>
+
+<script>
+import MDialog from '@/components/Dialog'
+import MForm from '@/components/MForm'
+import {
+  createTrainingData
+} from '@/api/dataChart'
+export default {
+  name: 'modelTrainManage',
+  components: {
+    MDialog,
+    MForm
+  },
+  data () {
+    return {
+      visible: false,
+      readonly: false,
+      formValues: {
+        training_data_type: 'sql'
+      },
+      kinds: {
+        sql: {
+          text: 'SQL语句内容',
+          key: 'sql'
+        },
+        documentation: {
+          text: '文档内容',
+          key: 'content'
+        },
+        ddl: {
+          text: 'DDL语句内容', // ddl类型
+          key: 'ddl'
+        },
+        error_sql: {
+          text: 'SQL语句内容',
+          key: 'error_sql'
+        }
+      }
+    }
+  },
+  computed: {
+    formItems () {
+      const isSql = this.formValues.training_data_type === 'sql' || this.formValues.training_data_type === 'error_sql'
+      return [
+        {
+          label: '数据类型',
+          key: 'training_data_type',
+          type: 'ifRadio',
+          items: [
+            { label: 'sql', value: 'sql', readonly: this.readonly },
+            { label: 'documentation', value: 'documentation', readonly: this.readonly },
+            { label: 'ddl', value: 'ddl', readonly: this.readonly },
+            { label: 'error_sql', value: 'error_sql', readonly: this.readonly }
+          ],
+          rules: [v => this.readonly || !!v || '请选择数据类型'],
+          change: (val) => {
+            this.$refs.form.resetValidation()
+          }
+        },
+        {
+          label: `提问内容 ${isSql ? '*' : ''}`,
+          key: 'question',
+          readonly: this.readonly,
+          type: 'textarea',
+          rules: [v => !isSql || this.readonly ? true : !!v || '请输入提问内容']
+        },
+        {
+          label: this.kinds[this.formValues.training_data_type].text,
+          key: 'content',
+          readonly: this.readonly,
+          rows: 10,
+          type: 'textarea',
+          rules: [v => this.readonly || !!v || '请输入回答内容']
+        }
+      ]
+    }
+  },
+  methods: {
+    open (item, readonly = false) {
+      this.readonly = readonly
+      this.visible = true
+      if (!item) {
+        this.formValues = {
+          training_data_type: 'sql'
+        }
+        this.$nextTick(() => {
+          this.$refs.form.resetValidation()
+        })
+        return
+      }
+      this.formValues = { ...item }
+      this.$refs.form.resetValidation()
+    },
+    async onSubmit () {
+      // const a = {
+      //   data: {
+      //     training_data_type: '选择的类型',
+      //     question: '问题内容', // sql和error_sql类型必填
+      //     content: '文档内容', // documentation类型
+      //     ddl: 'DDL语句内容' // ddl类型
+      //     sql: 'SQL语句内容', // sql和error_sql类型
+      //   }
+      // }
+      const { content, question, training_data_type: type } = this.formValues
+      try {
+        await createTrainingData({
+          data: {
+            training_data_type: type,
+            [this.kinds[type].key]: content,
+            question: type === 'sql' || type === 'error_sql' ? question : undefined
+          }
+        })
+        this.$snackbar.success('创建成功')
+        this.visible = false
+        this.$emit('success')
+      } catch (error) {
+        this.$snackbar.error(error)
+      }
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>