Spaces:
Runtime error
Runtime error
| <script lang="ts" setup> | |
| import { h, onMounted, reactive, ref } from 'vue' | |
| import { NButton, NDataTable, NInput, NModal, NSelect, NSpace, NSwitch, NTag, useDialog, useMessage } from 'naive-ui' | |
| import type { CHATMODEL } from './model' | |
| import { KeyConfig, Status, UserRole, apiModelOptions, userRoleOptions } from './model' | |
| import { fetchGetKeys, fetchUpdateApiKeyStatus, fetchUpsertApiKey } from '@/api' | |
| import { t } from '@/locales' | |
| import { useAuthStore } from '@/store' | |
| import { useBasicLayout } from '@/hooks/useBasicLayout' | |
| const ms = useMessage() | |
| const dialog = useDialog() | |
| const authStore = useAuthStore() | |
| const { isMobile } = useBasicLayout() | |
| const loading = ref(false) | |
| const show = ref(false) | |
| const handleSaving = ref(false) | |
| const keyConfig = ref(new KeyConfig('', 'ChatGPTAPI', [], [], '')) | |
| const keys = ref([]) | |
| const columns = [ | |
| { | |
| title: 'Key', | |
| key: 'key', | |
| resizable: true, | |
| width: 200, | |
| minWidth: 100, | |
| maxWidth: 200, | |
| ellipsis: true, | |
| }, | |
| { | |
| title: 'Api Model', | |
| key: 'keyModel', | |
| width: 190, | |
| }, | |
| { | |
| title: 'Chat Model', | |
| key: 'chatModels', | |
| width: 320, | |
| render(row: any) { | |
| const tags = row.chatModels.map((chatModel: CHATMODEL) => { | |
| return h( | |
| NTag, | |
| { | |
| style: { | |
| marginRight: '6px', | |
| }, | |
| type: 'info', | |
| bordered: false, | |
| }, | |
| { | |
| default: () => chatModel, | |
| }, | |
| ) | |
| }) | |
| return tags | |
| }, | |
| }, | |
| { | |
| title: 'User Roles', | |
| key: 'userRoles', | |
| width: 200, | |
| render(row: any) { | |
| const tags = row.userRoles.map((userRole: UserRole) => { | |
| return h( | |
| NTag, | |
| { | |
| style: { | |
| marginRight: '6px', | |
| }, | |
| type: 'info', | |
| bordered: false, | |
| }, | |
| { | |
| default: () => UserRole[userRole], | |
| }, | |
| ) | |
| }) | |
| return tags | |
| }, | |
| }, | |
| { | |
| title: 'Remark', | |
| key: 'remark', | |
| width: 220, | |
| }, | |
| { | |
| title: 'Action', | |
| key: '_id', | |
| width: 220, | |
| render(row: KeyConfig) { | |
| const actions: any[] = [] | |
| if (row.status === Status.Normal) { | |
| actions.push(h( | |
| NButton, | |
| { | |
| size: 'small', | |
| style: { | |
| marginRight: '6px', | |
| }, | |
| type: 'info', | |
| onClick: () => handleEditKey(row), | |
| }, | |
| { default: () => t('chat.editKeyButton') }, | |
| )) | |
| actions.push(h( | |
| NButton, | |
| { | |
| size: 'small', | |
| style: { | |
| marginRight: '6px', | |
| }, | |
| type: 'error', | |
| onClick: () => handleUpdateApiKeyStatus(row._id as string, Status.Disabled), | |
| }, | |
| { default: () => t('chat.deleteKey') }, | |
| )) | |
| } | |
| return actions | |
| }, | |
| }, | |
| ] | |
| const pagination = reactive({ | |
| page: 1, | |
| pageSize: 100, | |
| pageCount: 1, | |
| itemCount: 1, | |
| prefix({ itemCount }: { itemCount: number | undefined }) { | |
| return `Total is ${itemCount}.` | |
| }, | |
| showSizePicker: true, | |
| pageSizes: [100], | |
| onChange: (page: number) => { | |
| pagination.page = page | |
| handleGetKeys(pagination.page) | |
| }, | |
| onUpdatePageSize: (pageSize: number) => { | |
| pagination.pageSize = pageSize | |
| pagination.page = 1 | |
| handleGetKeys(pagination.page) | |
| }, | |
| }) | |
| async function handleGetKeys(page: number) { | |
| if (loading.value) | |
| return | |
| keys.value.length = 0 | |
| loading.value = true | |
| const size = pagination.pageSize | |
| const data = (await fetchGetKeys(page, size)).data | |
| data.keys.forEach((key: never) => { | |
| keys.value.push(key) | |
| }) | |
| keyConfig.value = keys.value[0] | |
| pagination.page = page | |
| pagination.pageCount = data.total / size + (data.total % size === 0 ? 0 : 1) | |
| pagination.itemCount = data.total | |
| loading.value = false | |
| } | |
| async function handleUpdateApiKeyStatus(id: string, status: Status) { | |
| dialog.warning({ | |
| title: t('chat.deleteKey'), | |
| content: t('chat.deleteKeyConfirm'), | |
| positiveText: t('common.yes'), | |
| negativeText: t('common.no'), | |
| onPositiveClick: async () => { | |
| await fetchUpdateApiKeyStatus(id, status) | |
| ms.info('OK') | |
| await handleGetKeys(pagination.page) | |
| }, | |
| }) | |
| } | |
| async function handleUpdateKeyConfig() { | |
| if (!keyConfig.value.key) { | |
| ms.error('Api key is required') | |
| return | |
| } | |
| handleSaving.value = true | |
| try { | |
| await fetchUpsertApiKey(keyConfig.value) | |
| await handleGetKeys(pagination.page) | |
| show.value = false | |
| } | |
| catch (error: any) { | |
| ms.error(error.message) | |
| } | |
| handleSaving.value = false | |
| } | |
| function handleNewKey() { | |
| keyConfig.value = new KeyConfig('', 'ChatGPTAPI', [], [], '') | |
| show.value = true | |
| } | |
| function handleEditKey(key: KeyConfig) { | |
| keyConfig.value = key | |
| show.value = true | |
| } | |
| onMounted(async () => { | |
| await handleGetKeys(pagination.page) | |
| }) | |
| </script> | |
| <template> | |
| <div class="p-4 space-y-5 min-h-[300px]"> | |
| <div class="space-y-6"> | |
| <NSpace vertical :size="12"> | |
| <NSpace> | |
| <NButton @click="handleNewKey()"> | |
| New Key | |
| </NButton> | |
| </NSpace> | |
| <NDataTable | |
| ref="table" | |
| remote | |
| :loading="loading" | |
| :row-key="(rowData) => rowData._id" | |
| :columns="columns" | |
| :data="keys" | |
| :pagination="pagination" | |
| :max-height="444" | |
| :scroll-x="1300" | |
| striped @update:page="handleGetKeys" | |
| /> | |
| </NSpace> | |
| </div> | |
| </div> | |
| <NModal v-model:show="show" :auto-focus="false" preset="card" :style="{ width: !isMobile ? '50%' : '100%' }"> | |
| <div class="p-4 space-y-5 min-h-[200px]"> | |
| <div class="space-y-6"> | |
| <div class="flex items-center space-x-4"> | |
| <span class="flex-shrink-0 w-[100px]">{{ $t('setting.apiModel') }}</span> | |
| <div class="flex-1"> | |
| <NSelect | |
| style="width: 100%" | |
| :value="keyConfig.keyModel" | |
| :options="apiModelOptions" | |
| @update-value="value => keyConfig.keyModel = value" | |
| /> | |
| </div> | |
| <p v-if="!isMobile"> | |
| <a v-if="keyConfig.keyModel === 'ChatGPTAPI'" target="_blank" href="https://platform.openai.com/account/api-keys">Get Api Key</a> | |
| <a v-else target="_blank" href="https://chat.openai.com/api/auth/session">Get Access Token</a> | |
| </p> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <span class="flex-shrink-0 w-[100px]">{{ $t('setting.api') }}</span> | |
| <div class="flex-1"> | |
| <NInput | |
| v-model:value="keyConfig.key" type="textarea" | |
| :autosize="{ minRows: 3, maxRows: 4 }" placeholder="" | |
| /> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <span class="flex-shrink-0 w-[100px]">{{ $t('setting.chatModels') }}</span> | |
| <div class="flex-1"> | |
| <NSelect | |
| style="width: 100%" | |
| multiple | |
| :value="keyConfig.chatModels" | |
| :options="authStore.session?.allChatModels" | |
| @update-value="value => keyConfig.chatModels = value" | |
| /> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <span class="flex-shrink-0 w-[100px]">{{ $t('setting.userRoles') }}</span> | |
| <div class="flex-1"> | |
| <NSelect | |
| style="width: 100%" | |
| multiple | |
| :value="keyConfig.userRoles" | |
| :options="userRoleOptions" | |
| @update-value="value => keyConfig.userRoles = value" | |
| /> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <span class="flex-shrink-0 w-[100px]">{{ $t('setting.status') }}</span> | |
| <div class="flex-1"> | |
| <NSwitch | |
| :round="false" | |
| :value="keyConfig.status === Status.Normal" | |
| @update:value="(val) => { keyConfig.status = val ? Status.Normal : Status.Disabled }" | |
| /> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <span class="flex-shrink-0 w-[100px]">{{ $t('setting.remark') }}</span> | |
| <div class="flex-1"> | |
| <NInput | |
| v-model:value="keyConfig.remark" type="textarea" | |
| :autosize="{ minRows: 1, maxRows: 2 }" placeholder="" | |
| /> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <span class="flex-shrink-0 w-[100px]" /> | |
| <NButton type="primary" :loading="handleSaving" @click="handleUpdateKeyConfig()"> | |
| {{ $t('common.save') }} | |
| </NButton> | |
| </div> | |
| </div> | |
| </div> | |
| </NModal> | |
| </template> | |