Added separate training file area and polling to refresh page
This commit is contained in:
parent
b8bef6a711
commit
6ccb7822c9
4 changed files with 672 additions and 350 deletions
60
site/package-lock.json
generated
60
site/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
||||||
"name": "dynavera",
|
"name": "dynavera",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^14.2.1",
|
||||||
"ant-design-vue": "^4.2.6",
|
"ant-design-vue": "^4.2.6",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.1",
|
||||||
|
|
@ -100,6 +101,7 @@
|
||||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
|
|
@ -1745,6 +1747,7 @@
|
||||||
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
|
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
|
|
@ -1756,6 +1759,12 @@
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/web-bluetooth": {
|
||||||
|
"version": "0.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
|
||||||
|
"integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.53.0",
|
"version": "8.53.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz",
|
||||||
|
|
@ -1801,6 +1810,7 @@
|
||||||
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
|
"integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.53.0",
|
"@typescript-eslint/scope-manager": "8.53.0",
|
||||||
"@typescript-eslint/types": "8.53.0",
|
"@typescript-eslint/types": "8.53.0",
|
||||||
|
|
@ -2431,12 +2441,51 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@vueuse/core": {
|
||||||
|
"version": "14.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.2.1.tgz",
|
||||||
|
"integrity": "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/web-bluetooth": "^0.0.21",
|
||||||
|
"@vueuse/metadata": "14.2.1",
|
||||||
|
"@vueuse/shared": "14.2.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/metadata": {
|
||||||
|
"version": "14.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-14.2.1.tgz",
|
||||||
|
"integrity": "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vueuse/shared": {
|
||||||
|
"version": "14.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-14.2.1.tgz",
|
||||||
|
"integrity": "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/antfu"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": "^3.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -2666,6 +2715,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -3215,6 +3265,7 @@
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -3275,6 +3326,7 @@
|
||||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
|
|
@ -3322,6 +3374,7 @@
|
||||||
"integrity": "sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==",
|
"integrity": "sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.4.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
|
|
@ -4018,6 +4071,7 @@
|
||||||
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
|
|
@ -4696,6 +4750,7 @@
|
||||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
|
|
@ -5143,6 +5198,7 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -5222,6 +5278,7 @@
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -5345,6 +5402,7 @@
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -5635,6 +5693,7 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -5654,6 +5713,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.26.tgz",
|
||||||
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
"integrity": "sha512-SJ/NTccVyAoNUJmkM9KUqPcYlY+u8OVL1X5EW9RIs3ch5H2uERxyyIUI4MRxVCSOiEcupX9xNGde1tL9ZKpimA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.26",
|
"@vue/compiler-dom": "3.5.26",
|
||||||
"@vue/compiler-sfc": "3.5.26",
|
"@vue/compiler-sfc": "3.5.26",
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
"format": "prettier --write --experimental-cli src/"
|
"format": "prettier --write --experimental-cli src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^14.2.1",
|
||||||
"ant-design-vue": "^4.2.6",
|
"ant-design-vue": "^4.2.6",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"dompurify": "^3.3.1",
|
"dompurify": "^3.3.1",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed, h } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
|
|
@ -14,6 +14,10 @@ import {
|
||||||
Modal,
|
Modal,
|
||||||
Tabs,
|
Tabs,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
|
Select,
|
||||||
|
Upload,
|
||||||
|
Steps,
|
||||||
|
Table,
|
||||||
} from 'ant-design-vue'
|
} from 'ant-design-vue'
|
||||||
import { apiClient, isAxiosError, API } from '../router/api'
|
import { apiClient, isAxiosError, API } from '../router/api'
|
||||||
import { useUserStore } from '../stores/userStore'
|
import { useUserStore } from '../stores/userStore'
|
||||||
|
|
@ -21,6 +25,8 @@ import type { Organization } from '../types/organization'
|
||||||
import type { User } from '../types/user'
|
import type { User } from '../types/user'
|
||||||
import type { InviteToken } from '../types/organization'
|
import type { InviteToken } from '../types/organization'
|
||||||
import type { Role } from '../types/organization'
|
import type { Role } from '../types/organization'
|
||||||
|
import type { TrainingFile } from '../types/organization'
|
||||||
|
import { InboxOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -32,15 +38,27 @@ const members = ref<User[]>([])
|
||||||
const invites = ref<InviteToken[]>([])
|
const invites = ref<InviteToken[]>([])
|
||||||
const newInviteMaxUses = ref<number>(1)
|
const newInviteMaxUses = ref<number>(1)
|
||||||
const Roles = ref<Role[]>([])
|
const Roles = ref<Role[]>([])
|
||||||
|
const trainingFiles = ref<TrainingFile[]>([])
|
||||||
const memberSearch = ref('')
|
const memberSearch = ref('')
|
||||||
const roleSearch = ref('')
|
const roleSearch = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const creatingRole = ref(false)
|
|
||||||
const deletingRoleUuid = ref<string | null>(null)
|
const deletingRoleUuid = ref<string | null>(null)
|
||||||
const roleModalVisible = ref(false)
|
const roleModalVisible = ref(false)
|
||||||
const roleMembersModalVisible = ref(false)
|
const roleMembersModalVisible = ref(false)
|
||||||
const selectedRoleForMembers = ref<Role | null>(null)
|
const selectedRoleForMembers = ref<Role | null>(null)
|
||||||
const selectedRoleMembers = ref<User[]>([])
|
const selectedRoleMembers = ref<User[]>([])
|
||||||
|
const roleWizardStep = ref(0)
|
||||||
|
const creatingRoleWizard = ref(false)
|
||||||
|
const createdRoleForWizard = ref<Role | null>(null)
|
||||||
|
const wizardSelectedFile = ref<File | null>(null)
|
||||||
|
const wizardFileDescription = ref('')
|
||||||
|
const wizardUploading = ref(false)
|
||||||
|
const wizardUploadedFiles = ref<TrainingFile[]>([])
|
||||||
|
const uploadModalVisible = ref(false)
|
||||||
|
const uploadRoleUuid = ref('')
|
||||||
|
const uploadSelectedFile = ref<File | null>(null)
|
||||||
|
const uploadFileDescription = ref('')
|
||||||
|
const uploadingFile = ref(false)
|
||||||
const createRoleForm = ref({
|
const createRoleForm = ref({
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
|
@ -122,43 +140,333 @@ const fetchRoles = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetRoleForm = () => {
|
const fetchTrainingFiles = async () => {
|
||||||
|
try {
|
||||||
|
const response = await apiClient.get<TrainingFile[]>(API.knowledge.trainingFiles.list(), {
|
||||||
|
params: { organization_uuid: organizationUuid },
|
||||||
|
})
|
||||||
|
trainingFiles.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch training files:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const resetRoleWizard = () => {
|
||||||
|
roleWizardStep.value = 0
|
||||||
createRoleForm.value = { name: '', description: '' }
|
createRoleForm.value = { name: '', description: '' }
|
||||||
|
createdRoleForWizard.value = null
|
||||||
|
wizardSelectedFile.value = null
|
||||||
|
wizardFileDescription.value = ''
|
||||||
|
wizardUploadedFiles.value = []
|
||||||
|
creatingRoleWizard.value = false
|
||||||
|
wizardUploading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeRoleWizard = () => {
|
||||||
|
roleModalVisible.value = false
|
||||||
|
resetRoleWizard()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openRoleWizard = () => {
|
||||||
|
resetRoleWizard()
|
||||||
|
roleModalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasDuplicateRoleName = (name: string) =>
|
const hasDuplicateRoleName = (name: string) =>
|
||||||
Roles.value.some((role) => role.name.trim().toLowerCase() === name.trim().toLowerCase())
|
Roles.value.some((role) => role.name.trim().toLowerCase() === name.trim().toLowerCase())
|
||||||
|
|
||||||
const createRole = async () => {
|
const allowedExtensions = ['txt', 'pdf', 'md', 'csv', 'json', 'docx', 'doc']
|
||||||
|
const maxUploadBytes = 50 * 1024 * 1024
|
||||||
|
|
||||||
|
const validateUploadFile = (file: File): boolean => {
|
||||||
|
const extension = file.name.split('.').pop()?.toLowerCase()
|
||||||
|
|
||||||
|
if (!extension || !allowedExtensions.includes(extension)) {
|
||||||
|
message.error(
|
||||||
|
`File type ".${extension}" is not allowed. Allowed types: ${allowedExtensions.join(', ')}`,
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maxUploadBytes) {
|
||||||
|
message.error(
|
||||||
|
`File size must not exceed 50MB. Current size: ${(file.size / 1024 / 1024).toFixed(2)}MB`,
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadTrainingFile = async (
|
||||||
|
roleUuid: string,
|
||||||
|
file: File,
|
||||||
|
description: string,
|
||||||
|
): Promise<TrainingFile | null> => {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('file_name', file.name)
|
||||||
|
formData.append('description', description)
|
||||||
|
formData.append('role_uuid', roleUuid)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await apiClient.post<TrainingFile>(API.knowledge.trainingFiles.list(), formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
} catch (error) {
|
||||||
|
if (isAxiosError(error)) {
|
||||||
|
const errorMsg =
|
||||||
|
error.response?.data?.error ||
|
||||||
|
error.response?.data?.file?.[0] ||
|
||||||
|
'Failed to upload file'
|
||||||
|
message.error(errorMsg)
|
||||||
|
} else {
|
||||||
|
message.error('Failed to upload file')
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTrainingFilesByRole = (roleUuid: string): TrainingFile[] =>
|
||||||
|
trainingFiles.value.filter((file) => file.role?.uuid === roleUuid)
|
||||||
|
|
||||||
|
const deleteTrainingFile = async (uuid: string, fileName: string) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: 'Delete File',
|
||||||
|
content: `Are you sure you want to delete "${fileName}"? This action cannot be undone.`,
|
||||||
|
okText: 'Delete',
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await apiClient.delete(API.knowledge.trainingFiles.byId(uuid))
|
||||||
|
message.success('File deleted successfully')
|
||||||
|
trainingFiles.value = trainingFiles.value.filter((file) => file.uuid !== uuid)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete file:', error)
|
||||||
|
message.error('Failed to delete file')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const canDeleteTrainingFile = (record: TrainingFile): boolean => {
|
||||||
|
if (auth.user?.uuid === record.uploaded_by?.uuid) return true
|
||||||
|
if (organization.value?.owner?.uuid === auth.user?.uuid) return true
|
||||||
|
return members.value.some((member) => member.uuid === auth.user?.uuid && member.is_manager)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const trainingFileColumns = [
|
||||||
|
{
|
||||||
|
title: 'File Name',
|
||||||
|
dataIndex: 'file_name',
|
||||||
|
key: 'file_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Uploaded By',
|
||||||
|
key: 'uploaded_by',
|
||||||
|
customRender: ({ record }: { record: TrainingFile }) => {
|
||||||
|
if (!record.uploaded_by) return '-'
|
||||||
|
return `${record.uploaded_by.first_name} ${record.uploaded_by.last_name}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Size',
|
||||||
|
dataIndex: 'file_size',
|
||||||
|
key: 'file_size',
|
||||||
|
customRender: ({ value }: { value: number }) => formatFileSize(value || 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Role',
|
||||||
|
key: 'role',
|
||||||
|
customRender: ({ record }: { record: TrainingFile }) => record.role?.name || '-',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Status',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
customRender: ({ value }: { value: string }) => {
|
||||||
|
const statusMap: Record<string, { color: string; label: string }> = {
|
||||||
|
ingesting: { color: 'processing', label: 'Ingesting' },
|
||||||
|
chunked: { color: 'blue', label: 'Chunked' },
|
||||||
|
embedded: { color: 'success', label: 'Embedded' },
|
||||||
|
failed: { color: 'error', label: 'Failed' },
|
||||||
|
}
|
||||||
|
const status = statusMap[value] || { color: 'default', label: value }
|
||||||
|
return h(Tag, { color: status.color }, () => status.label)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Uploaded',
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
customRender: ({ value }: { value: string }) => new Date(value).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Action',
|
||||||
|
key: 'action',
|
||||||
|
customRender: ({ record }: { record: TrainingFile }) => {
|
||||||
|
if (canDeleteTrainingFile(record)) {
|
||||||
|
return h(
|
||||||
|
Button,
|
||||||
|
{
|
||||||
|
danger: true,
|
||||||
|
size: 'small',
|
||||||
|
icon: h(DeleteOutlined),
|
||||||
|
onClick: () => deleteTrainingFile(record.uuid, record.file_name),
|
||||||
|
},
|
||||||
|
() => 'Delete',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
const createRoleForWizard = async (): Promise<Role | null> => {
|
||||||
const name = createRoleForm.value.name.trim()
|
const name = createRoleForm.value.name.trim()
|
||||||
const description = createRoleForm.value.description.trim()
|
const description = createRoleForm.value.description.trim()
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
message.error('Role name is required')
|
message.error('Role name is required')
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasDuplicateRoleName(name)) {
|
if (hasDuplicateRoleName(name)) {
|
||||||
message.error('A role with this name already exists')
|
message.error('A role with this name already exists')
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
creatingRole.value = true
|
creatingRoleWizard.value = true
|
||||||
try {
|
try {
|
||||||
await apiClient.post(API.roles.list(organizationUuid), { name, description })
|
const response = await apiClient.post<Role>(API.roles.list(organizationUuid), { name, description })
|
||||||
message.success('Role created successfully')
|
message.success('Role created successfully. You can upload training files now.')
|
||||||
roleModalVisible.value = false
|
|
||||||
resetRoleForm()
|
|
||||||
await fetchRoles()
|
await fetchRoles()
|
||||||
|
|
||||||
|
if (response.data?.uuid) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
return Roles.value.find((role) => role.name.trim().toLowerCase() === name.toLowerCase()) || null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create role:', error)
|
console.error('Failed to create role in wizard:', error)
|
||||||
if (isAxiosError(error)) {
|
if (isAxiosError(error)) {
|
||||||
message.error(error.response?.data?.error || 'Failed to create role')
|
message.error(error.response?.data?.error || 'Failed to create role')
|
||||||
} else {
|
} else {
|
||||||
message.error('Failed to create role')
|
message.error('Failed to create role')
|
||||||
}
|
}
|
||||||
|
return null
|
||||||
} finally {
|
} finally {
|
||||||
creatingRole.value = false
|
creatingRoleWizard.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRoleWizardOk = async () => {
|
||||||
|
if (roleWizardStep.value === 0) {
|
||||||
|
const role = await createRoleForWizard()
|
||||||
|
if (!role) return
|
||||||
|
createdRoleForWizard.value = role
|
||||||
|
roleWizardStep.value = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
closeRoleWizard()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRoleWizardFileSelected = (file: File) => {
|
||||||
|
if (!validateUploadFile(file)) {
|
||||||
|
wizardSelectedFile.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wizardSelectedFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadFileFromWizard = async () => {
|
||||||
|
const roleUuid = createdRoleForWizard.value?.uuid
|
||||||
|
if (!roleUuid) {
|
||||||
|
message.error('Role is not available for upload')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wizardSelectedFile.value) {
|
||||||
|
message.error('Please select a file to upload')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wizardUploading.value = true
|
||||||
|
try {
|
||||||
|
const uploaded = await uploadTrainingFile(
|
||||||
|
roleUuid,
|
||||||
|
wizardSelectedFile.value,
|
||||||
|
wizardFileDescription.value,
|
||||||
|
)
|
||||||
|
if (!uploaded) return
|
||||||
|
|
||||||
|
trainingFiles.value.unshift(uploaded)
|
||||||
|
wizardUploadedFiles.value.unshift(uploaded)
|
||||||
|
message.success(`File "${wizardSelectedFile.value.name}" uploaded successfully`)
|
||||||
|
wizardSelectedFile.value = null
|
||||||
|
wizardFileDescription.value = ''
|
||||||
|
} finally {
|
||||||
|
wizardUploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openUploadModal = (role?: Role) => {
|
||||||
|
uploadRoleUuid.value = role?.uuid || ''
|
||||||
|
uploadSelectedFile.value = null
|
||||||
|
uploadFileDescription.value = ''
|
||||||
|
uploadModalVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploadModalFileSelected = (file: File) => {
|
||||||
|
if (!validateUploadFile(file)) {
|
||||||
|
uploadSelectedFile.value = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadSelectedFile.value = file
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploadModalOk = async () => {
|
||||||
|
if (!uploadRoleUuid.value) {
|
||||||
|
message.error('Please select a role for this training file')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!uploadSelectedFile.value) {
|
||||||
|
message.error('Please select a file to upload')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadingFile.value = true
|
||||||
|
try {
|
||||||
|
const uploaded = await uploadTrainingFile(
|
||||||
|
uploadRoleUuid.value,
|
||||||
|
uploadSelectedFile.value,
|
||||||
|
uploadFileDescription.value,
|
||||||
|
)
|
||||||
|
if (!uploaded) return
|
||||||
|
|
||||||
|
trainingFiles.value.unshift(uploaded)
|
||||||
|
message.success(`File "${uploadSelectedFile.value.name}" uploaded successfully`)
|
||||||
|
uploadModalVisible.value = false
|
||||||
|
uploadSelectedFile.value = null
|
||||||
|
uploadFileDescription.value = ''
|
||||||
|
} finally {
|
||||||
|
uploadingFile.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -302,6 +610,7 @@ onMounted(async () => {
|
||||||
await fetchMembers()
|
await fetchMembers()
|
||||||
await fetchInvites()
|
await fetchInvites()
|
||||||
await fetchRoles()
|
await fetchRoles()
|
||||||
|
await fetchTrainingFiles()
|
||||||
|
|
||||||
const currentUserUuid = auth.user?.uuid
|
const currentUserUuid = auth.user?.uuid
|
||||||
const isOwner = organization.value?.owner?.uuid === currentUserUuid
|
const isOwner = organization.value?.owner?.uuid === currentUserUuid
|
||||||
|
|
@ -472,7 +781,10 @@ onMounted(async () => {
|
||||||
placeholder="Search roles by name or description"
|
placeholder="Search roles by name or description"
|
||||||
style="width: 300px"
|
style="width: 300px"
|
||||||
/>
|
/>
|
||||||
<Button type="primary" @click="roleModalVisible = true">
|
<Button @click="openUploadModal()">
|
||||||
|
Upload Training File
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" @click="openRoleWizard">
|
||||||
Create Role
|
Create Role
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
@ -485,12 +797,18 @@ onMounted(async () => {
|
||||||
>
|
>
|
||||||
<template #renderItem="{ item }">
|
<template #renderItem="{ item }">
|
||||||
<List.Item class="Role-item">
|
<List.Item class="Role-item">
|
||||||
|
<div class="role-content">
|
||||||
|
<div class="role-head">
|
||||||
<List.Item.Meta
|
<List.Item.Meta
|
||||||
:title="item.name"
|
:title="item.name"
|
||||||
:description="item.description || 'No description'"
|
:description="item.description || 'No description'"
|
||||||
/>
|
/>
|
||||||
<Space>
|
<Space>
|
||||||
<Tag>{{ item.member_count }} members</Tag>
|
<Tag>{{ item.member_count }} members</Tag>
|
||||||
|
<Tag color="blue">{{ getTrainingFilesByRole(item.uuid).length }} files</Tag>
|
||||||
|
<Button size="small" @click="openUploadModal(item)">
|
||||||
|
Upload Files
|
||||||
|
</Button>
|
||||||
<Button size="small" @click="openRoleMembersModal(item)">
|
<Button size="small" @click="openRoleMembersModal(item)">
|
||||||
View Members
|
View Members
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -503,6 +821,30 @@ onMounted(async () => {
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="role-files">
|
||||||
|
<Typography.Text strong>Training files for this role</Typography.Text>
|
||||||
|
<List
|
||||||
|
v-if="getTrainingFilesByRole(item.uuid).length > 0"
|
||||||
|
:data-source="getTrainingFilesByRole(item.uuid)"
|
||||||
|
size="small"
|
||||||
|
:bordered="false"
|
||||||
|
>
|
||||||
|
<template #renderItem="{ item: file }">
|
||||||
|
<List.Item>
|
||||||
|
<Space style="display: flex; justify-content: space-between; width: 100%">
|
||||||
|
<Typography.Text>{{ file.file_name }}</Typography.Text>
|
||||||
|
<Tag>{{ formatFileSize(file.file_size || 0) }}</Tag>
|
||||||
|
</Space>
|
||||||
|
</List.Item>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
<Typography.Paragraph v-else type="secondary" style="margin: 0.5rem 0 0">
|
||||||
|
No training files uploaded for this role yet.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</List.Item>
|
</List.Item>
|
||||||
</template>
|
</template>
|
||||||
</List>
|
</List>
|
||||||
|
|
@ -511,25 +853,53 @@ onMounted(async () => {
|
||||||
</Typography.Paragraph>
|
</Typography.Paragraph>
|
||||||
</div>
|
</div>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
|
|
||||||
|
<Tabs.TabPane key="files" tab="Files">
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-header">
|
||||||
|
<Typography.Title :level="4" style="color: #1f2937 !important">
|
||||||
|
Training Files ({{ trainingFiles.length }})
|
||||||
|
</Typography.Title>
|
||||||
|
<Button @click="openUploadModal()">Upload Training File</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table
|
||||||
|
v-if="trainingFiles.length > 0"
|
||||||
|
:columns="trainingFileColumns"
|
||||||
|
:data-source="trainingFiles"
|
||||||
|
:pagination="{ pageSize: 10 }"
|
||||||
|
:row-key="(record: TrainingFile) => record.uuid"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
<Typography.Paragraph v-else type="secondary">
|
||||||
|
No training files uploaded yet.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
</div>
|
||||||
|
</Tabs.TabPane>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Card>
|
</Card>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
v-model:open="roleModalVisible"
|
v-model:open="roleModalVisible"
|
||||||
title="Create Role"
|
:title="roleWizardStep === 0 ? 'Create Role' : 'Upload Training Files'"
|
||||||
ok-text="Create"
|
:ok-text="roleWizardStep === 0 ? 'Next' : 'Finish'"
|
||||||
cancel-text="Cancel"
|
cancel-text="Cancel"
|
||||||
:ok-button-props="{ loading: creatingRole }"
|
:ok-button-props="{ loading: roleWizardStep === 0 ? creatingRoleWizard : false }"
|
||||||
@ok="createRole"
|
@ok="handleRoleWizardOk"
|
||||||
@cancel="resetRoleForm"
|
@cancel="closeRoleWizard"
|
||||||
>
|
>
|
||||||
<div style="display: flex; flex-direction: column; gap: 0.75rem">
|
<Steps :current="roleWizardStep" size="small" style="margin-bottom: 1rem">
|
||||||
|
<Steps.Step title="Role Details" />
|
||||||
|
<Steps.Step title="Training Files" />
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
<div v-if="roleWizardStep === 0" style="display: flex; flex-direction: column; gap: 0.75rem">
|
||||||
<Input
|
<Input
|
||||||
v-model:value="createRoleForm.name"
|
v-model:value="createRoleForm.name"
|
||||||
placeholder="Role name"
|
placeholder="Role name"
|
||||||
:maxlength="100"
|
:maxlength="100"
|
||||||
@pressEnter="createRole"
|
@pressEnter="handleRoleWizardOk"
|
||||||
/>
|
/>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
v-model:value="createRoleForm.description"
|
v-model:value="createRoleForm.description"
|
||||||
|
|
@ -538,6 +908,116 @@ onMounted(async () => {
|
||||||
:maxlength="1000"
|
:maxlength="1000"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else style="display: flex; flex-direction: column; gap: 0.75rem">
|
||||||
|
<Typography.Paragraph type="secondary" style="margin-bottom: 0">
|
||||||
|
Upload optional training files for
|
||||||
|
<strong>{{ createdRoleForWizard?.name }}</strong>
|
||||||
|
. You can also do this later.
|
||||||
|
</Typography.Paragraph>
|
||||||
|
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="wizardFileDescription"
|
||||||
|
placeholder="Optional file description"
|
||||||
|
:rows="2"
|
||||||
|
:maxlength="500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Upload.Dragger
|
||||||
|
accept=".txt,.pdf,.md,.csv,.json,.docx,.doc"
|
||||||
|
:before-upload="
|
||||||
|
(file) => {
|
||||||
|
handleRoleWizardFileSelected(file)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:multiple="false"
|
||||||
|
:auto-upload="false"
|
||||||
|
>
|
||||||
|
<p class="ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p class="ant-upload-text">Click or drag file to this area to select</p>
|
||||||
|
<p class="ant-upload-hint">
|
||||||
|
{{ wizardSelectedFile ? wizardSelectedFile.name : 'Single file upload' }}
|
||||||
|
</p>
|
||||||
|
</Upload.Dragger>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:disabled="!wizardSelectedFile"
|
||||||
|
:loading="wizardUploading"
|
||||||
|
@click="uploadFileFromWizard"
|
||||||
|
>
|
||||||
|
Upload Selected File
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div v-if="wizardUploadedFiles.length > 0" class="uploaded-list">
|
||||||
|
<Typography.Text strong>Uploaded in this setup:</Typography.Text>
|
||||||
|
<List :data-source="wizardUploadedFiles" :bordered="false" size="small">
|
||||||
|
<template #renderItem="{ item }">
|
||||||
|
<List.Item>
|
||||||
|
<List.Item.Meta :title="item.file_name" :description="item.role?.name" />
|
||||||
|
</List.Item>
|
||||||
|
</template>
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
v-model:open="uploadModalVisible"
|
||||||
|
title="Upload Training File"
|
||||||
|
ok-text="Upload"
|
||||||
|
cancel-text="Cancel"
|
||||||
|
:ok-button-props="{ loading: uploadingFile, disabled: !uploadRoleUuid || !uploadSelectedFile }"
|
||||||
|
@ok="handleUploadModalOk"
|
||||||
|
@cancel="uploadModalVisible = false"
|
||||||
|
>
|
||||||
|
<div style="display: flex; flex-direction: column; gap: 0.75rem">
|
||||||
|
<Typography.Text>
|
||||||
|
Supported formats:
|
||||||
|
<strong>txt, pdf, md, csv, json, docx, doc</strong>
|
||||||
|
(Max 50MB)
|
||||||
|
</Typography.Text>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Typography.Text strong>Role</Typography.Text>
|
||||||
|
<Select
|
||||||
|
v-model:value="uploadRoleUuid"
|
||||||
|
placeholder="Select a role"
|
||||||
|
style="width: 100%"
|
||||||
|
:options="Roles.map((role) => ({ label: role.name, value: role.uuid }))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Input.TextArea
|
||||||
|
v-model:value="uploadFileDescription"
|
||||||
|
placeholder="Optional file description"
|
||||||
|
:rows="2"
|
||||||
|
:maxlength="500"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Upload.Dragger
|
||||||
|
accept=".txt,.pdf,.md,.csv,.json,.docx,.doc"
|
||||||
|
:before-upload="
|
||||||
|
(file) => {
|
||||||
|
handleUploadModalFileSelected(file)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:multiple="false"
|
||||||
|
:auto-upload="false"
|
||||||
|
>
|
||||||
|
<p class="ant-upload-drag-icon">
|
||||||
|
<InboxOutlined />
|
||||||
|
</p>
|
||||||
|
<p class="ant-upload-text">Click or drag file to this area to select</p>
|
||||||
|
<p class="ant-upload-hint">
|
||||||
|
{{ uploadSelectedFile ? uploadSelectedFile.name : 'Single file upload' }}
|
||||||
|
</p>
|
||||||
|
</Upload.Dragger>
|
||||||
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
|
|
@ -613,6 +1093,27 @@ onMounted(async () => {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.role-content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-files {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.white-tag {
|
.white-tag {
|
||||||
background-color: #ffffff !important;
|
background-color: #ffffff !important;
|
||||||
color: #000000 !important;
|
color: #000000 !important;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, computed, h, watch } from 'vue'
|
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useDocumentVisibility, useEventListener, useIntervalFn } from '@vueuse/core'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Typography,
|
Typography,
|
||||||
|
|
@ -11,15 +12,11 @@ import {
|
||||||
message,
|
message,
|
||||||
Tag,
|
Tag,
|
||||||
Divider,
|
Divider,
|
||||||
Upload,
|
|
||||||
Modal,
|
Modal,
|
||||||
Table,
|
|
||||||
Select,
|
|
||||||
} from 'ant-design-vue'
|
} from 'ant-design-vue'
|
||||||
import { apiClient, isAxiosError, API } from '../router/api'
|
import { apiClient, isAxiosError, API } from '../router/api'
|
||||||
import { useUserStore } from '../stores/userStore'
|
import { useUserStore } from '../stores/userStore'
|
||||||
import { InboxOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
import type { Role, Organization } from '../types/organization'
|
||||||
import type { Role, Organization, TrainingFile } from '../types/organization'
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
@ -28,12 +25,13 @@ const organizationUuid = computed(() => String(route.params.organizationUuid ||
|
||||||
const organization = ref<Organization | null>(null)
|
const organization = ref<Organization | null>(null)
|
||||||
const roles = ref<Role[]>([])
|
const roles = ref<Role[]>([])
|
||||||
const members = ref<Array<{ uuid: string; is_manager?: boolean }>>([])
|
const members = ref<Array<{ uuid: string; is_manager?: boolean }>>([])
|
||||||
const trainingFiles = ref<TrainingFile[]>([])
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const uploading = ref(false)
|
|
||||||
const leavingOrganization = ref(false)
|
const leavingOrganization = ref(false)
|
||||||
const showUploadModal = ref(false)
|
const rolePollingMs = 12000
|
||||||
const auth = useUserStore()
|
const auth = useUserStore()
|
||||||
|
const visibility = useDocumentVisibility()
|
||||||
|
const pollingInFlight = ref(false)
|
||||||
|
let stopVisibilityListener: (() => void) | null = null
|
||||||
|
|
||||||
const isManager = computed(() => {
|
const isManager = computed(() => {
|
||||||
if (!auth.user || !organization.value) return false
|
if (!auth.user || !organization.value) return false
|
||||||
|
|
@ -154,12 +152,20 @@ const selectRole = async (roleUuid: string) => {
|
||||||
try {
|
try {
|
||||||
await apiClient.post(API.roles.join(organization.value.uuid, roleUuid))
|
await apiClient.post(API.roles.join(organization.value.uuid, roleUuid))
|
||||||
message.success('Successfully joined role')
|
message.success('Successfully joined role')
|
||||||
|
|
||||||
|
const joinedRole = roles.value.find((role) => role.uuid === roleUuid)
|
||||||
|
if (joinedRole) {
|
||||||
|
joinedRole.member_count = (joinedRole.member_count ?? 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
if (!auth.userJoinedRoles.some((role) => role.uuid === roleUuid)) {
|
if (!auth.userJoinedRoles.some((role) => role.uuid === roleUuid)) {
|
||||||
auth.setJoinedRoles([
|
auth.setJoinedRoles([
|
||||||
...auth.userJoinedRoles,
|
...auth.userJoinedRoles,
|
||||||
roles.value.find((role) => role.uuid === roleUuid)!,
|
roles.value.find((role) => role.uuid === roleUuid)!,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await fetchRoles()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to join role:', error)
|
console.error('Failed to join role:', error)
|
||||||
if (isAxiosError(error)) {
|
if (isAxiosError(error)) {
|
||||||
|
|
@ -168,231 +174,59 @@ const selectRole = async (roleUuid: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchTrainingFiles = async () => {
|
|
||||||
if (!organization.value?.uuid) return
|
|
||||||
try {
|
|
||||||
const response = await apiClient.get<TrainingFile[]>(API.knowledge.trainingFiles.list(), {
|
|
||||||
params: { organization_uuid: organization.value.uuid },
|
|
||||||
})
|
|
||||||
trainingFiles.value = response.data
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch training files:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const beforeUpload = (file: File) => {
|
|
||||||
const allowedExtensions = ['txt', 'pdf', 'md', 'csv', 'json', 'docx', 'doc']
|
|
||||||
const fileExtension = file.name.split('.').pop()?.toLowerCase()
|
|
||||||
|
|
||||||
if (!fileExtension || !allowedExtensions.includes(fileExtension)) {
|
|
||||||
message.error(
|
|
||||||
`File type ".${fileExtension}" is not allowed. Allowed types: ${allowedExtensions.join(', ')}`,
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxSize = 50 * 1024 * 1024
|
|
||||||
if (file.size > maxSize) {
|
|
||||||
message.error(
|
|
||||||
`File size must not exceed 50MB. Current size: ${(file.size / 1024 / 1024).toFixed(2)}MB`,
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedFile = ref<File | null>(null)
|
|
||||||
const fileDescription = ref('')
|
|
||||||
const selectedRoleUuid = ref<string>('')
|
|
||||||
|
|
||||||
const handleOpenUploadModal = () => {
|
|
||||||
if (!isManager.value) {
|
|
||||||
message.error('Only managers can upload training files')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (roles.value.length === 0) {
|
|
||||||
message.error('No roles found for this organization. Create a role first in Manage Organization.')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
showUploadModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileSelected = (file: File) => {
|
|
||||||
selectedFile.value = file
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileUploadClick = async () => {
|
|
||||||
if (!selectedFile.value) {
|
|
||||||
message.error('Please select a file to upload')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!selectedRoleUuid.value) {
|
|
||||||
message.error('Please select a role for this training file')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleFileUpload(selectedFile.value, fileDescription.value)
|
|
||||||
selectedFile.value = null
|
|
||||||
fileDescription.value = ''
|
|
||||||
selectedRoleUuid.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleFileUpload = async (file: File, description: string = '') => {
|
|
||||||
if (!organization.value?.uuid) {
|
|
||||||
message.error('Organization not loaded')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uploading.value = true
|
|
||||||
try {
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
formData.append('file_name', file.name)
|
|
||||||
formData.append('description', description)
|
|
||||||
if (selectedRoleUuid.value) {
|
|
||||||
formData.append('role_uuid', selectedRoleUuid.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await apiClient.post<TrainingFile>(
|
|
||||||
API.knowledge.trainingFiles.list(),
|
|
||||||
formData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
message.success(`File "${file.name}" uploaded successfully`)
|
|
||||||
trainingFiles.value.unshift(response.data)
|
|
||||||
showUploadModal.value = false
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to upload file:', error)
|
|
||||||
if (isAxiosError(error)) {
|
|
||||||
const errorMsg =
|
|
||||||
error.response?.data?.error ||
|
|
||||||
error.response?.data?.file?.[0] ||
|
|
||||||
'Failed to upload file'
|
|
||||||
message.error(errorMsg)
|
|
||||||
} else {
|
|
||||||
message.error('Failed to upload file')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
uploading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteFile = async (uuid: string, fileName: string) => {
|
|
||||||
if (!organization.value?.uuid) {
|
|
||||||
message.error('Organization not loaded')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Modal.confirm({
|
|
||||||
title: 'Delete File',
|
|
||||||
content: `Are you sure you want to delete "${fileName}"? This action cannot be undone.`,
|
|
||||||
okText: 'Delete',
|
|
||||||
okType: 'danger',
|
|
||||||
cancelText: 'Cancel',
|
|
||||||
onOk: async () => {
|
|
||||||
try {
|
|
||||||
await apiClient.delete(API.knowledge.trainingFiles.byId(uuid))
|
|
||||||
message.success('File deleted successfully')
|
|
||||||
trainingFiles.value = trainingFiles.value.filter((f) => f.uuid !== uuid)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete file:', error)
|
|
||||||
message.error('Failed to delete file')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatFileSize = (bytes: number) => {
|
|
||||||
if (bytes === 0) return '0 Bytes'
|
|
||||||
const k = 1024
|
|
||||||
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
||||||
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
|
|
||||||
}
|
|
||||||
|
|
||||||
const trainingFileColumns = [
|
|
||||||
{
|
|
||||||
title: 'File Name',
|
|
||||||
dataIndex: 'file_name',
|
|
||||||
key: 'file_name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Uploaded By',
|
|
||||||
key: 'uploaded_by',
|
|
||||||
customRender: ({ record }: { record: TrainingFile }) => {
|
|
||||||
if (!record.uploaded_by) return '-'
|
|
||||||
const full_name = `${record.uploaded_by.first_name} ${record.uploaded_by.last_name}`
|
|
||||||
return full_name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Size',
|
|
||||||
dataIndex: 'file_size',
|
|
||||||
key: 'file_size',
|
|
||||||
customRender: ({ value }: { value: number }) => formatFileSize(value || 0),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Role',
|
|
||||||
key: 'role',
|
|
||||||
customRender: ({ record }: { record: TrainingFile }) => record.role?.name || '-',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Status',
|
|
||||||
dataIndex: 'status',
|
|
||||||
key: 'status',
|
|
||||||
customRender: ({ value }: { value: string }) => {
|
|
||||||
const statusMap: Record<string, { color: string; label: string }> = {
|
|
||||||
ingesting: { color: 'processing', label: 'Ingesting' },
|
|
||||||
chunked: { color: 'blue', label: 'Chunked' },
|
|
||||||
embedded: { color: 'success', label: 'Embedded' },
|
|
||||||
failed: { color: 'error', label: 'Failed' },
|
|
||||||
}
|
|
||||||
const status = statusMap[value] || { color: 'default', label: value }
|
|
||||||
return h(Tag, { color: status.color }, () => status.label)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Uploaded',
|
|
||||||
dataIndex: 'created_at',
|
|
||||||
key: 'created_at',
|
|
||||||
customRender: ({ value }: { value: string }) => new Date(value).toLocaleDateString(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Action',
|
|
||||||
key: 'action',
|
|
||||||
customRender: ({ record }: { record: TrainingFile }) => {
|
|
||||||
if (isManager.value || auth.user?.uuid === record.uploaded_by?.uuid) {
|
|
||||||
return h(
|
|
||||||
Button,
|
|
||||||
{
|
|
||||||
danger: true,
|
|
||||||
size: 'small',
|
|
||||||
icon: h(DeleteOutlined),
|
|
||||||
onClick: () => deleteFile(record.uuid, record.file_name),
|
|
||||||
},
|
|
||||||
() => 'Delete',
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const loadOrganizationContext = async () => {
|
const loadOrganizationContext = async () => {
|
||||||
await auth.fetchSession(true)
|
await auth.fetchSession(true)
|
||||||
await fetchOrganization()
|
await fetchOrganization()
|
||||||
await fetchMembers()
|
await fetchMembers()
|
||||||
await fetchRoles()
|
await fetchRoles()
|
||||||
await fetchUserRoleMemberships()
|
await fetchUserRoleMemberships()
|
||||||
await fetchTrainingFiles()
|
}
|
||||||
|
|
||||||
|
const canPollRoles = () => Boolean(organization.value?.uuid)
|
||||||
|
|
||||||
|
const refreshRolesLive = async () => {
|
||||||
|
if (pollingInFlight.value || !canPollRoles()) return
|
||||||
|
pollingInFlight.value = true
|
||||||
|
try {
|
||||||
|
await Promise.all([fetchRoles(), fetchUserRoleMemberships()])
|
||||||
|
} finally {
|
||||||
|
pollingInFlight.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pause: stopRolePolling, resume: resumeRolePolling } = useIntervalFn(
|
||||||
|
() => {
|
||||||
|
void refreshRolesLive()
|
||||||
|
},
|
||||||
|
rolePollingMs,
|
||||||
|
{ immediate: false, immediateCallback: false },
|
||||||
|
)
|
||||||
|
|
||||||
|
const startRolePolling = () => {
|
||||||
|
stopRolePolling()
|
||||||
|
if (visibility.value !== 'visible' || !canPollRoles()) return
|
||||||
|
resumeRolePolling()
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindVisibility = () => {
|
||||||
|
if (stopVisibilityListener) return
|
||||||
|
stopVisibilityListener = useEventListener(document, 'visibilitychange', () => {
|
||||||
|
if (visibility.value !== 'visible') {
|
||||||
|
stopRolePolling()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshRolesLive()
|
||||||
|
if (canPollRoles()) {
|
||||||
|
resumeRolePolling()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const unbindVisibility = () => {
|
||||||
|
if (!stopVisibilityListener) return
|
||||||
|
stopVisibilityListener()
|
||||||
|
stopVisibilityListener = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const leaveOrganization = () => {
|
const leaveOrganization = () => {
|
||||||
|
|
@ -436,16 +270,24 @@ const leaveOrganization = () => {
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadOrganizationContext()
|
await loadOrganizationContext()
|
||||||
|
bindVisibility()
|
||||||
|
startRolePolling()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopRolePolling()
|
||||||
|
unbindVisibility()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => organizationUuid.value,
|
() => organizationUuid.value,
|
||||||
async (next, prev) => {
|
async (next, prev) => {
|
||||||
if (!next || next === prev) return
|
if (!next || next === prev) return
|
||||||
|
stopRolePolling()
|
||||||
roles.value = []
|
roles.value = []
|
||||||
members.value = []
|
members.value = []
|
||||||
trainingFiles.value = []
|
|
||||||
await loadOrganizationContext()
|
await loadOrganizationContext()
|
||||||
|
startRolePolling()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -504,41 +346,6 @@ watch(
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<Typography.Title :level="4" class="section-title">Training Files</Typography.Title>
|
|
||||||
|
|
||||||
<div style="margin-bottom: 1rem">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
:disabled="!isManager"
|
|
||||||
@click="handleOpenUploadModal"
|
|
||||||
style="margin-bottom: 1rem"
|
|
||||||
>
|
|
||||||
Upload Training File
|
|
||||||
</Button>
|
|
||||||
<Typography.Paragraph
|
|
||||||
v-if="isManager && roles.length === 0"
|
|
||||||
type="secondary"
|
|
||||||
style="margin: 0.25rem 0 0"
|
|
||||||
>
|
|
||||||
Create a role in Manage Organization before uploading training files.
|
|
||||||
</Typography.Paragraph>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="trainingFiles.length > 0">
|
|
||||||
<Table
|
|
||||||
:columns="trainingFileColumns"
|
|
||||||
:data-source="trainingFiles"
|
|
||||||
:pagination="{ pageSize: 10 }"
|
|
||||||
:row-key="(record: TrainingFile) => record.uuid"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Typography.Paragraph v-else type="secondary">
|
|
||||||
No training files uploaded yet.
|
|
||||||
</Typography.Paragraph>
|
|
||||||
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
<Typography.Title :level="4" class="section-title">
|
<Typography.Title :level="4" class="section-title">
|
||||||
Available Roles
|
Available Roles
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
|
|
@ -589,53 +396,6 @@ watch(
|
||||||
</Card>
|
</Card>
|
||||||
</Spin>
|
</Spin>
|
||||||
|
|
||||||
<Modal
|
|
||||||
v-model:open="showUploadModal"
|
|
||||||
title="Upload Training File"
|
|
||||||
width="600px"
|
|
||||||
ok-text="Upload"
|
|
||||||
cancel-text="Cancel"
|
|
||||||
:ok-button-props="{ loading: uploading, disabled: !selectedFile || !selectedRoleUuid }"
|
|
||||||
@ok="handleFileUploadClick"
|
|
||||||
@cancel="showUploadModal = false"
|
|
||||||
>
|
|
||||||
<div style="display: flex; flex-direction: column; gap: 1rem">
|
|
||||||
<Typography.Text>
|
|
||||||
Supported formats:
|
|
||||||
<strong>txt, pdf, md, csv, json, docx, doc</strong>
|
|
||||||
(Max 50MB)
|
|
||||||
</Typography.Text>
|
|
||||||
<div>
|
|
||||||
<Typography.Text strong>Role</Typography.Text>
|
|
||||||
<Select
|
|
||||||
v-model:value="selectedRoleUuid"
|
|
||||||
placeholder="Select a role"
|
|
||||||
style="width: 100%"
|
|
||||||
:options="roles.map((role) => ({ label: role.name, value: role.uuid }))"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Upload.Dragger
|
|
||||||
accept=".txt,.pdf,.md,.csv,.json,.docx,.doc"
|
|
||||||
:before-upload="
|
|
||||||
(file) => {
|
|
||||||
beforeUpload(file)
|
|
||||||
handleFileSelected(file)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
"
|
|
||||||
:multiple="false"
|
|
||||||
:auto-upload="false"
|
|
||||||
>
|
|
||||||
<p class="ant-upload-drag-icon">
|
|
||||||
<InboxOutlined />
|
|
||||||
</p>
|
|
||||||
<p class="ant-upload-text">Click or drag file to this area to upload</p>
|
|
||||||
<p class="ant-upload-hint">
|
|
||||||
{{ selectedFile ? selectedFile.name : 'Single file upload' }}
|
|
||||||
</p>
|
|
||||||
</Upload.Dragger>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue