feat(ui): 对话管理
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/artist.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 46 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/dou_yin.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 14 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/elon_musk.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 23 KiB  | 
| 
		 After Width: | Height: | Size: 20 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/girl_friend.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 27 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/good_comment.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 32 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/gpt.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/kong_zi.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 40 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/lu_xun.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 27 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/mid_journey.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/programmer.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 41 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/psychiatrist.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 25 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/red_book.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 14 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/seller.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 16 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/steve_jobs.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 25 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/teacher.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 28 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/translator.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 20 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/user.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 24 KiB  | 
| 
		 After Width: | Height: | Size: 19 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								gpt-vue/projects/vue-admin/public/images/avatar/yi_yan.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.0 KiB  | 
@@ -8,6 +8,7 @@ function useRequest<T>(request: Request<T>) {
 | 
			
		||||
  const loading = ref(false)
 | 
			
		||||
 | 
			
		||||
  const requestData = async (params?: any) => {
 | 
			
		||||
    loading.value = true
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await request(params)
 | 
			
		||||
      result.value = res.data
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,15 @@ const menu = [
 | 
			
		||||
    },
 | 
			
		||||
    component: () => import('@/views/Functions/FunctionsContainer.vue')
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/chats',
 | 
			
		||||
    name: 'Chats',
 | 
			
		||||
    meta: {
 | 
			
		||||
      title: "对话管理",
 | 
			
		||||
      icon: IconCalendar,
 | 
			
		||||
    },
 | 
			
		||||
    component: () => import('@/views/Chats/ChatsContainer.vue')
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/loginLog',
 | 
			
		||||
    name: 'LoginLog',
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,110 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, h } from "vue";
 | 
			
		||||
import { Message, Modal } from "@arco-design/web-vue";
 | 
			
		||||
import SearchTable from "@/components/SearchTable/SearchTable.vue";
 | 
			
		||||
import type { SearchTableColumns } from "@/components/SearchTable/type";
 | 
			
		||||
import app from "@/main";
 | 
			
		||||
import { getList, message, remove } from "./api";
 | 
			
		||||
import ChatsLogs from "./ChatsLogs.vue";
 | 
			
		||||
 | 
			
		||||
const columns: SearchTableColumns[] = [
 | 
			
		||||
  {
 | 
			
		||||
    dataIndex: "user_id",
 | 
			
		||||
    title: "账户ID",
 | 
			
		||||
    search: {
 | 
			
		||||
      valueType: "input",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    dataIndex: "username",
 | 
			
		||||
    title: "账户",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    dataIndex: "title",
 | 
			
		||||
    title: "标题",
 | 
			
		||||
    search: {
 | 
			
		||||
      valueType: "input",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    dataIndex: "model",
 | 
			
		||||
    title: "模型",
 | 
			
		||||
    search: {
 | 
			
		||||
      valueType: "input",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    dataIndex: "msg_num",
 | 
			
		||||
    title: "消息数量",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    dataIndex: "token",
 | 
			
		||||
    title: "消耗算力",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    dataIndex: "username",
 | 
			
		||||
    title: "账户",
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    dataIndex: "created_at",
 | 
			
		||||
    title: "创建时间",
 | 
			
		||||
    search: {
 | 
			
		||||
      valueType: "range",
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    title: "操作",
 | 
			
		||||
    fixed: "right",
 | 
			
		||||
    slotName: "actions",
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const tabsList = [
 | 
			
		||||
  { key: "1", title: "对话列表", api: getList, columns },
 | 
			
		||||
  { key: "2", title: "消息记录", api: message, columns },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const activeKey = ref(tabsList[0].key);
 | 
			
		||||
 | 
			
		||||
const handleRemove = async (chat_id, reload) => {
 | 
			
		||||
  await remove({ chat_id });
 | 
			
		||||
  Message.success("删除成功");
 | 
			
		||||
  await reload();
 | 
			
		||||
  return true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const handleCheck = (record) => {
 | 
			
		||||
  if (activeKey.value === "1") {
 | 
			
		||||
    Modal._context = app._context;
 | 
			
		||||
    Modal.info({
 | 
			
		||||
      title: "对话详情",
 | 
			
		||||
      width: 800,
 | 
			
		||||
      content: () => h(ChatsLogs, { id: record.chat_id }),
 | 
			
		||||
    });
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  Modal.info({
 | 
			
		||||
    title: "消息详情",
 | 
			
		||||
    content: record.content,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <a-tabs v-model:active-key="activeKey" lazy-load justify>
 | 
			
		||||
    <a-tab-pane v-for="item in tabsList" :key="item.key" :title="item.title">
 | 
			
		||||
      <SearchTable :request="item.api" :columns="item.columns">
 | 
			
		||||
        <template #actions="{ record, reload }">
 | 
			
		||||
          <a-link @click="handleCheck(record)">查看</a-link>
 | 
			
		||||
          <a-popconfirm
 | 
			
		||||
            content="是否删除?"
 | 
			
		||||
            position="left"
 | 
			
		||||
            type="warning"
 | 
			
		||||
            :on-before-ok="() => handleRemove(record.id, reload)"
 | 
			
		||||
          >
 | 
			
		||||
            <a-link status="danger">删除</a-link>
 | 
			
		||||
          </a-popconfirm>
 | 
			
		||||
        </template>
 | 
			
		||||
      </SearchTable>
 | 
			
		||||
    </a-tab-pane>
 | 
			
		||||
  </a-tabs>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										94
									
								
								gpt-vue/projects/vue-admin/src/views/Chats/ChatsLogs.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,94 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted } from "vue";
 | 
			
		||||
import { Message } from "@arco-design/web-vue";
 | 
			
		||||
import { dateFormat } from "@gpt-vue/packages/utils";
 | 
			
		||||
import useRequest from "@/composables/useRequest";
 | 
			
		||||
import { history } from "./api";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  id: String,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const [getData, data, loading] = useRequest(history);
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  await getData({ chat_id: props.id });
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
<template>
 | 
			
		||||
  <template v-if="loading">
 | 
			
		||||
    <div class="custom-skeleton">
 | 
			
		||||
      <a-skeleton-shape />
 | 
			
		||||
      <div style="flex: 1">
 | 
			
		||||
        <a-skeleton-line :rows="2" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </template>
 | 
			
		||||
  <template v-else>
 | 
			
		||||
    <div v-for="item in data" :key="item.id">
 | 
			
		||||
      <div class="item-container" :class="item.type">
 | 
			
		||||
        <div class="left">
 | 
			
		||||
          <a-avatar shape="square">
 | 
			
		||||
            <img :src="item.icon" />
 | 
			
		||||
          </a-avatar>
 | 
			
		||||
        </div>
 | 
			
		||||
        <a-space class="right" direction="vertical">
 | 
			
		||||
          <div>{{ item.content }}</div>
 | 
			
		||||
          <a-space>
 | 
			
		||||
            <div class="code">
 | 
			
		||||
              <icon-clock-circle />
 | 
			
		||||
              {{ dateFormat(item.created_at) }}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="code">算力消耗: {{ item.tokens }}</div>
 | 
			
		||||
            <a-typography-text
 | 
			
		||||
              v-if="item.type === 'reply'"
 | 
			
		||||
              copyable
 | 
			
		||||
              :copy-delay="1000"
 | 
			
		||||
              :copy-text="item.content"
 | 
			
		||||
              @copy="Message.success('复制成功')"
 | 
			
		||||
            >
 | 
			
		||||
              <template #copy-icon>
 | 
			
		||||
                <a-button class="code" size="mini">
 | 
			
		||||
                  <icon-copy />
 | 
			
		||||
                </a-button>
 | 
			
		||||
              </template>
 | 
			
		||||
              <template #copy-tooltip>复制回答</template>
 | 
			
		||||
            </a-typography-text>
 | 
			
		||||
          </a-space>
 | 
			
		||||
        </a-space>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </template>
 | 
			
		||||
</template>
 | 
			
		||||
<style lang="less" scoped>
 | 
			
		||||
.item-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  padding: 20px 10px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  gap: 20px;
 | 
			
		||||
  border-bottom: 1px solid #d9d9e3;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  align-items: flex-start;
 | 
			
		||||
  &.reply {
 | 
			
		||||
    background: #f7f7f8;
 | 
			
		||||
  }
 | 
			
		||||
  .left {
 | 
			
		||||
    width: 40px;
 | 
			
		||||
  }
 | 
			
		||||
  .right {
 | 
			
		||||
    flex: 1;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  .code {
 | 
			
		||||
    background-color: #e7e7e8;
 | 
			
		||||
    color: #888;
 | 
			
		||||
    padding: 3px 5px;
 | 
			
		||||
    margin-right: 10px;
 | 
			
		||||
    border-radius: 5px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
.custom-skeleton {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  gap: 12px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										34
									
								
								gpt-vue/projects/vue-admin/src/views/Chats/api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,34 @@
 | 
			
		||||
import http from "@/http/config";
 | 
			
		||||
 | 
			
		||||
export const getList = (data) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/chat/list",
 | 
			
		||||
    method: "post",
 | 
			
		||||
    data
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const message = (data) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/chat/message",
 | 
			
		||||
    method: "post",
 | 
			
		||||
    data
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const history = (params) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/chat/history",
 | 
			
		||||
    method: "get",
 | 
			
		||||
    params
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const remove = (params) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/chat/remove",
 | 
			
		||||
    method: "get",
 | 
			
		||||
    params
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -34,11 +34,11 @@ const columns: TableColumnData[] = [
 | 
			
		||||
const openFormModal = usePopup(FunctionsForm, {
 | 
			
		||||
  nodeProps: ([_, record]) => ({ record }),
 | 
			
		||||
  popupProps: ([reload, record], exposed) => ({
 | 
			
		||||
    title: `${record.id ? "编辑" : "新增"}函数`,
 | 
			
		||||
    title: `${record?.id ? "编辑" : "新增"}函数`,
 | 
			
		||||
    width: "800px",
 | 
			
		||||
    onBeforeOk: async (done) => {
 | 
			
		||||
      await exposed()?.handleSubmit(save, {
 | 
			
		||||
        id: record.id,
 | 
			
		||||
        id: record?.id,
 | 
			
		||||
        parameters: exposed()?.parameters(),
 | 
			
		||||
      });
 | 
			
		||||
      await reload();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,9 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, watchEffect } from "vue";
 | 
			
		||||
import { Message } from "@arco-design/web-vue";
 | 
			
		||||
import useSubmit from "@/composables/useSubmit";
 | 
			
		||||
import FunctionsFormTable from "./FunctionsFormTable.vue";
 | 
			
		||||
import { token } from "./api";
 | 
			
		||||
import translateTableData from "./translateTableData";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
@@ -16,7 +18,7 @@ const { formRef, formData, handleSubmit, submitting } = useSubmit({
 | 
			
		||||
  action: "",
 | 
			
		||||
  token: "",
 | 
			
		||||
  parameters: {},
 | 
			
		||||
  enabled: false,
 | 
			
		||||
  enabled: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const rules = {
 | 
			
		||||
@@ -25,6 +27,12 @@ const rules = {
 | 
			
		||||
  description: [{ required: true, message: "请输入函数功能描述" }],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const generateToken = async () => {
 | 
			
		||||
  const { data } = await token();
 | 
			
		||||
  Message.success("生成 Token 成功");
 | 
			
		||||
  formData.token = data;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
watchEffect(() => {
 | 
			
		||||
  Object.assign(formData, props.record ?? {});
 | 
			
		||||
  tableData.value = translateTableData.get(formData.parameters);
 | 
			
		||||
@@ -56,15 +64,15 @@ defineExpose({
 | 
			
		||||
        <a-input v-model="formData.action" placeholder="该函数实现的API地址,可以是第三方服务API" />
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item field="token" label="API Token">
 | 
			
		||||
        <a-input-search v-model="formData.token" placeholder="API授权Token">
 | 
			
		||||
        <a-input v-model="formData.token" placeholder="API授权Token">
 | 
			
		||||
          <template #append>
 | 
			
		||||
            <a-tooltip
 | 
			
		||||
              content="只有本地服务才可以使用自动生成Token第三方服务请填写第三方服务API Token"
 | 
			
		||||
            >
 | 
			
		||||
              <a-button>生成Token</a-button>
 | 
			
		||||
              <a-button type="text" @click="generateToken">生成Token</a-button>
 | 
			
		||||
            </a-tooltip>
 | 
			
		||||
          </template>
 | 
			
		||||
        </a-input-search>
 | 
			
		||||
        </a-input>
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item field="enabled" label="启用状态">
 | 
			
		||||
        <a-switch v-model="formData.enabled" />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,4 @@
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { IconDelete } from "@arco-design/web-vue/es/icon";
 | 
			
		||||
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  modelValue: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
@@ -33,7 +31,7 @@ const handleRemoveRow = (index) => {
 | 
			
		||||
            <a-input v-model="scope.record.name" />
 | 
			
		||||
          </template>
 | 
			
		||||
        </a-table-column>
 | 
			
		||||
        <a-table-column title="参数类型" :width="120">
 | 
			
		||||
        <a-table-column title="参数类型" :width="150">
 | 
			
		||||
          <template #cell="scope">
 | 
			
		||||
            <a-select
 | 
			
		||||
              v-model="scope.record.type"
 | 
			
		||||
@@ -48,7 +46,7 @@ const handleRemoveRow = (index) => {
 | 
			
		||||
          </template>
 | 
			
		||||
        </a-table-column>
 | 
			
		||||
 | 
			
		||||
        <a-table-column title="必填参数" :width="100">
 | 
			
		||||
        <a-table-column title="必填参数" :width="100" align="center">
 | 
			
		||||
          <template #cell="scope">
 | 
			
		||||
            <a-checkbox v-model="scope.record.required" />
 | 
			
		||||
          </template>
 | 
			
		||||
@@ -58,15 +56,19 @@ const handleRemoveRow = (index) => {
 | 
			
		||||
          <template #cell="scope">
 | 
			
		||||
            <a-button
 | 
			
		||||
              status="danger"
 | 
			
		||||
              :icon="IconDelete"
 | 
			
		||||
              circle
 | 
			
		||||
              shape="circle"
 | 
			
		||||
              @click="handleRemoveRow(scope.rowIndex)"
 | 
			
		||||
              size="small"
 | 
			
		||||
            />
 | 
			
		||||
            >
 | 
			
		||||
              <icon-delete />
 | 
			
		||||
            </a-button>
 | 
			
		||||
          </template>
 | 
			
		||||
        </a-table-column>
 | 
			
		||||
      </template>
 | 
			
		||||
    </a-table>
 | 
			
		||||
    <a-button type="primary" long @click="handleCreateRow">新增参数</a-button>
 | 
			
		||||
    <a-button type="primary" @click="handleCreateRow">
 | 
			
		||||
      <template #icon><icon-plus /></template>
 | 
			
		||||
      <span>新增参数</span>
 | 
			
		||||
    </a-button>
 | 
			
		||||
  </a-space>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
@@ -32,10 +32,10 @@ export const setStatus = (data) => {
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const token = (data) => {
 | 
			
		||||
export const token = (params) => {
 | 
			
		||||
  return http({
 | 
			
		||||
    url: "/api/admin/function/token",
 | 
			
		||||
    method: "post",
 | 
			
		||||
    data
 | 
			
		||||
    method: "get",
 | 
			
		||||
    params
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,13 +18,16 @@ const get = (origin) => {
 | 
			
		||||
 | 
			
		||||
const set = (tableData) => {
 | 
			
		||||
  const properties = tableData.reduce((prev, curr) => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...prev,
 | 
			
		||||
      [curr.name]: {
 | 
			
		||||
        description: curr.description,
 | 
			
		||||
        type: curr.type,
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
    if (curr.name) {
 | 
			
		||||
      return {
 | 
			
		||||
        ...prev,
 | 
			
		||||
        [curr.name]: {
 | 
			
		||||
          description: curr.description,
 | 
			
		||||
          type: curr.type,
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    return prev
 | 
			
		||||
  }, {});
 | 
			
		||||
  const required = tableData.filter((i) => i.required).map((i) => i.name);
 | 
			
		||||
  return { properties, required, type: "object" }
 | 
			
		||||
 
 | 
			
		||||