文件接口
This commit is contained in:
parent
a256f25ea9
commit
01bf39bc89
|
|
@ -3,7 +3,7 @@ import { api } from './api'
|
||||||
|
|
||||||
export const chat = (params: any) => {
|
export const chat = (params: any) => {
|
||||||
return api({
|
return api({
|
||||||
url: '/chat-docs/chatno',
|
url: '/chat',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: JSON.stringify(params),
|
data: JSON.stringify(params),
|
||||||
})
|
})
|
||||||
|
|
@ -17,20 +17,18 @@ export const chatfile = (params: any) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getfilelist = () => {
|
export const getfilelist = (knowledge_base_id: any) => {
|
||||||
return api({
|
return api({
|
||||||
url: '/chat-docs/list',
|
url: '/local_doc_qa/list_files',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: {
|
params: { knowledge_base_id },
|
||||||
knowledge_base_id: '123',
|
|
||||||
},
|
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const deletefile = (params: any) => {
|
export const deletefile = (params: any) => {
|
||||||
return api({
|
return api({
|
||||||
url: '/chat-docs/delete',
|
url: '/local_doc_qa/delete_file',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: JSON.stringify(params),
|
data: JSON.stringify(params),
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -13,19 +13,18 @@ import HeaderComponent from './components/Header/index.vue'
|
||||||
import { HoverButton, SvgIcon } from '@/components/common'
|
import { HoverButton, SvgIcon } from '@/components/common'
|
||||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||||
import { useChatStore, usePromptStore } from '@/store'
|
import { useChatStore, usePromptStore } from '@/store'
|
||||||
import { fetchChatAPIProcess } from '@/api'
|
|
||||||
import { t } from '@/locales'
|
import { t } from '@/locales'
|
||||||
import { chat, chatfile } from '@/api/chat'
|
import { chat, chatfile } from '@/api/chat'
|
||||||
let controller = new AbortController()
|
let controller = new AbortController()
|
||||||
|
|
||||||
const openLongReply = import.meta.env.VITE_GLOB_OPEN_LONG_REPLY === 'true'
|
// const openLongReply = import.meta.env.VITE_GLOB_OPEN_LONG_REPLY === 'true'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const dialog = useDialog()
|
const dialog = useDialog()
|
||||||
const ms = useMessage()
|
const ms = useMessage()
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
|
const history = ref<any>([])
|
||||||
const { isMobile } = useBasicLayout()
|
const { isMobile } = useBasicLayout()
|
||||||
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
|
const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
|
||||||
const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll()
|
const { scrollRef, scrollToBottom, scrollToBottomIfAtBottom } = useScroll()
|
||||||
|
|
@ -61,7 +60,9 @@ function handleSubmit() {
|
||||||
|
|
||||||
async function onConversation() {
|
async function onConversation() {
|
||||||
const message = prompt.value
|
const message = prompt.value
|
||||||
|
dataSources.value.forEach((item) => {
|
||||||
|
console.log(item)
|
||||||
|
})
|
||||||
if (loading.value)
|
if (loading.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -110,13 +111,13 @@ async function onConversation() {
|
||||||
const lastText = ''
|
const lastText = ''
|
||||||
const fetchChatAPIOnce = async () => {
|
const fetchChatAPIOnce = async () => {
|
||||||
const res = active.value
|
const res = active.value
|
||||||
? await chatfile({ message })
|
? await chatfile({
|
||||||
|
question: message,
|
||||||
|
history: history.value,
|
||||||
|
})
|
||||||
: await chat({
|
: await chat({
|
||||||
question: message,
|
question: message,
|
||||||
history: [[
|
history: history.value,
|
||||||
'工伤保险是什么?',
|
|
||||||
'工伤保险是指用人单位按照国家规定,为本单位的职工和用人单位的其他人员,缴纳工伤保险费,由保险机构按照国家规定的标准,给予工伤保险待遇的社会保险制度。',
|
|
||||||
]],
|
|
||||||
})
|
})
|
||||||
const result = active.value ? res.data.response.text : res.data.response
|
const result = active.value ? res.data.response.text : res.data.response
|
||||||
updateChat(
|
updateChat(
|
||||||
|
|
@ -230,7 +231,6 @@ async function onConversation() {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onRegenerate(index: number) {
|
async function onRegenerate(index: number) {
|
||||||
if (loading.value)
|
if (loading.value)
|
||||||
return
|
return
|
||||||
|
|
@ -239,7 +239,7 @@ async function onRegenerate(index: number) {
|
||||||
|
|
||||||
const { requestOptions } = dataSources.value[index]
|
const { requestOptions } = dataSources.value[index]
|
||||||
|
|
||||||
let message = requestOptions?.prompt ?? ''
|
const message = requestOptions?.prompt ?? ''
|
||||||
|
|
||||||
let options: Chat.ConversationRequest = {}
|
let options: Chat.ConversationRequest = {}
|
||||||
|
|
||||||
|
|
@ -263,48 +263,30 @@ async function onRegenerate(index: number) {
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let lastText = ''
|
const lastText = ''
|
||||||
const fetchChatAPIOnce = async () => {
|
const fetchChatAPIOnce = async () => {
|
||||||
await fetchChatAPIProcess<Chat.ConversationResponse>({
|
const res = active.value
|
||||||
prompt: message,
|
? await chatfile({ message })
|
||||||
options,
|
: await chat({
|
||||||
signal: controller.signal,
|
question: message,
|
||||||
onDownloadProgress: ({ event }) => {
|
history: history.value,
|
||||||
const xhr = event.target
|
})
|
||||||
const { responseText } = xhr
|
const result = active.value ? res.data.response.text : res.data.response
|
||||||
// Always process the final line
|
|
||||||
const lastIndex = responseText.lastIndexOf('\n', responseText.length - 2)
|
|
||||||
let chunk = responseText
|
|
||||||
if (lastIndex !== -1)
|
|
||||||
chunk = responseText.substring(lastIndex)
|
|
||||||
try {
|
|
||||||
const data = JSON.parse(chunk)
|
|
||||||
updateChat(
|
updateChat(
|
||||||
+uuid,
|
+uuid,
|
||||||
index,
|
dataSources.value.length - 1,
|
||||||
{
|
{
|
||||||
dateTime: new Date().toLocaleString(),
|
dateTime: new Date().toLocaleString(),
|
||||||
text: lastText + (data.text ?? ''),
|
text: lastText + (result ?? ''),
|
||||||
inversion: false,
|
inversion: false,
|
||||||
error: false,
|
error: false,
|
||||||
loading: true,
|
loading: false,
|
||||||
conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
|
conversationOptions: null,
|
||||||
requestOptions: { prompt: message, options: { ...options } },
|
requestOptions: { prompt: message, options: { ...options } },
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
scrollToBottomIfAtBottom()
|
||||||
if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
|
loading.value = false
|
||||||
options.parentMessageId = data.id
|
|
||||||
lastText = data.text
|
|
||||||
message = ''
|
|
||||||
return fetchChatAPIOnce()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
updateChatSome(+uuid, index, { loading: false })
|
updateChatSome(+uuid, index, { loading: false })
|
||||||
}
|
}
|
||||||
await fetchChatAPIOnce()
|
await fetchChatAPIOnce()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import type { CSSProperties } from 'vue'
|
import type { CSSProperties } from 'vue'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { NButton, NLayoutSider, NUpload } from 'naive-ui'
|
import { NButton, NLayoutSider, NRadioButton, NRadioGroup } from 'naive-ui'
|
||||||
import List from './List.vue'
|
import List from './List.vue'
|
||||||
import filelist from './filelist.vue'
|
import Knowledge from './knowledge-base/index.vue'
|
||||||
import Footer from './Footer.vue'
|
import Footer from './Footer.vue'
|
||||||
import { useAppStore, useChatStore } from '@/store'
|
import { useAppStore, useChatStore } from '@/store'
|
||||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||||
|
|
@ -46,6 +46,25 @@ const mobileSafeArea = computed(() => {
|
||||||
}
|
}
|
||||||
return {}
|
return {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const songs = [
|
||||||
|
{
|
||||||
|
value: 1,
|
||||||
|
label: '会话',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 2,
|
||||||
|
label: '模型',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 3,
|
||||||
|
label: '知识库',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 4,
|
||||||
|
label: '提示词',
|
||||||
|
},
|
||||||
|
]
|
||||||
//
|
//
|
||||||
watch(
|
watch(
|
||||||
isMobile,
|
isMobile,
|
||||||
|
|
@ -75,39 +94,20 @@ watch(
|
||||||
<div class="flex flex-col h-full " :style="mobileSafeArea">
|
<div class="flex flex-col h-full " :style="mobileSafeArea">
|
||||||
<main class="flex flex-col flex-1 min-h-0">
|
<main class="flex flex-col flex-1 min-h-0">
|
||||||
<div class=" flex justify-between">
|
<div class=" flex justify-between">
|
||||||
<NButton dashed @click="menu = 1">
|
<NRadioGroup v-model:value="menu" name="radiobuttongroup1">
|
||||||
会话
|
<NRadioButton
|
||||||
</NButton>
|
v-for="song in songs"
|
||||||
<NButton dashed @click="menu = 2">
|
:key="song.value"
|
||||||
模型
|
:value="song.value"
|
||||||
</NButton>
|
:label="song.label"
|
||||||
<NButton dashed @click="menu = 3">
|
/>
|
||||||
知识库
|
</NRadioGroup>
|
||||||
</NButton>
|
|
||||||
<NButton dashed @click="menu = 4">
|
|
||||||
提示词
|
|
||||||
</NButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 知识库界面 -->
|
<!-- 知识库界面 -->
|
||||||
<div v-if="menu === 3">
|
<div v-if="menu === 3">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<NUpload
|
<Knowledge />
|
||||||
action="http://127.0.0.1:1002/api/chat-docs/uploadone"
|
|
||||||
:headers="{
|
|
||||||
'naive-info': 'hello!',
|
|
||||||
}"
|
|
||||||
:data="{
|
|
||||||
knowledge_base_id: '123',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<NButton block>
|
|
||||||
文件上传
|
|
||||||
</NButton>
|
|
||||||
</NUpload>
|
|
||||||
</div>
|
|
||||||
<div class="p-2 flex-1 min-h-0 pb-4 overflow-hidden">
|
|
||||||
<filelist />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 会话界面 -->
|
<!-- 会话界面 -->
|
||||||
|
|
@ -121,11 +121,6 @@ watch(
|
||||||
<List />
|
<List />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="p-4">
|
|
||||||
<NButton block @click="show = true">
|
|
||||||
{{ $t('store.siderButton') }}
|
|
||||||
</NButton>
|
|
||||||
</div> -->
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</NLayoutSider>
|
</NLayoutSider>
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,32 @@
|
||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref, toRef } from 'vue'
|
||||||
import { NInput, NPopconfirm, NScrollbar } from 'naive-ui'
|
import { NInput, NP, NPopconfirm, NScrollbar, NText, NUpload, NUploadDragger } from 'naive-ui'
|
||||||
import { SvgIcon } from '@/components/common'
|
import { SvgIcon } from '@/components/common'
|
||||||
import { useAppStore, useChatStore } from '@/store'
|
import { useChatStore } from '@/store'
|
||||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
|
||||||
import { deletefile, getfilelist } from '@/api/chat'
|
import { deletefile, getfilelist } from '@/api/chat'
|
||||||
|
const knowledge = defineProps({
|
||||||
|
knowledgebaseid: {
|
||||||
|
type: String, // 类型字符串
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const knowledge_base_id = toRef(knowledge, 'knowledgebaseid')
|
||||||
|
|
||||||
const { isMobile } = useBasicLayout()
|
|
||||||
|
|
||||||
const appStore = useAppStore()
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
const dataSources = ref<any[]>([])
|
const dataSources = ref<any>([])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const res = await getfilelist()
|
const res = await getfilelist(knowledge_base_id.value)
|
||||||
dataSources.value = res.data.data
|
dataSources.value = res.data.data
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleSelect({ uuid }: Chat.History) {
|
|
||||||
if (isActive(uuid))
|
|
||||||
return
|
|
||||||
|
|
||||||
if (chatStore.active)
|
|
||||||
chatStore.updateHistory(chatStore.active, { isEdit: false })
|
|
||||||
await chatStore.setActive(uuid)
|
|
||||||
|
|
||||||
if (isMobile.value)
|
|
||||||
appStore.setSiderCollapsed(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* function handleEdit({ uuid }: Chat.History, isEdit: boolean, event?: MouseEvent) {
|
/* function handleEdit({ uuid }: Chat.History, isEdit: boolean, event?: MouseEvent) {
|
||||||
event?.stopPropagation()
|
event?.stopPropagation()
|
||||||
chatStore.updateHistory(uuid, { isEdit })
|
chatStore.updateHistory(uuid, { isEdit })
|
||||||
} */
|
} */
|
||||||
|
|
||||||
async function handleDelete(item: any) {
|
async function handleDelete(item: any) {
|
||||||
/* const mid = */await deletefile({ knowledge_base_id: '123', doc_name: item })
|
/* const mid = */await deletefile({ knowledge_base_id: knowledge_base_id.value, doc_name: item })
|
||||||
const res = await getfilelist()
|
const res = await getfilelist(knowledge_base_id.value)
|
||||||
dataSources.value = res.data.data
|
dataSources.value = res.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,13 +35,34 @@ function handleEnter({ uuid }: Chat.History, isEdit: boolean, event: KeyboardEve
|
||||||
if (event.key === 'Enter')
|
if (event.key === 'Enter')
|
||||||
chatStore.updateHistory(uuid, { isEdit })
|
chatStore.updateHistory(uuid, { isEdit })
|
||||||
}
|
}
|
||||||
|
|
||||||
function isActive(uuid: number) {
|
|
||||||
return chatStore.active === uuid
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<NUpload
|
||||||
|
multiple
|
||||||
|
directory-dnd
|
||||||
|
action="http://192.168.1.128:1002/api/local_doc_qa/upload_file"
|
||||||
|
:headers="{
|
||||||
|
'naive-info': 'hello!',
|
||||||
|
}"
|
||||||
|
:data="{
|
||||||
|
knowledge_base_id: knowledge.knowledgebaseid as string,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<NUploadDragger>
|
||||||
|
<!-- <div style="margin-bottom: 12px">
|
||||||
|
<NIcon size="48" :depth="3">
|
||||||
|
<archive-icon />
|
||||||
|
</NIcon>
|
||||||
|
</div> -->
|
||||||
|
<NText style="font-size: 16px">
|
||||||
|
点击或者拖动文件到该区域来上传
|
||||||
|
</NText>
|
||||||
|
<NP depth="3" style="margin: 8px 0 0 0">
|
||||||
|
在弹出的文件选择框,按住ctrl或shift进行多选
|
||||||
|
</NP>
|
||||||
|
</NUploadDragger>
|
||||||
|
</NUpload>
|
||||||
<NScrollbar class="px-4">
|
<NScrollbar class="px-4">
|
||||||
<div class="flex flex-col gap-2 text-sm">
|
<div class="flex flex-col gap-2 text-sm">
|
||||||
<template v-if="!dataSources.length">
|
<template v-if="!dataSources.length">
|
||||||
|
|
@ -64,8 +75,6 @@ function isActive(uuid: number) {
|
||||||
<div v-for="(item, index) of dataSources" :key="index">
|
<div v-for="(item, index) of dataSources" :key="index">
|
||||||
<a
|
<a
|
||||||
class="relative flex items-center gap-3 px-3 py-3 break-all border rounded-md cursor-pointer hover:bg-neutral-100 group dark:border-neutral-800 dark:hover:bg-[#24272e]"
|
class="relative flex items-center gap-3 px-3 py-3 break-all border rounded-md cursor-pointer hover:bg-neutral-100 group dark:border-neutral-800 dark:hover:bg-[#24272e]"
|
||||||
:class="isActive(item.uuid) && ['border-[#4b9e5f]', 'bg-neutral-100', 'text-[#4b9e5f]', 'dark:bg-[#24272e]', 'dark:border-[#4b9e5f]', 'pr-14']"
|
|
||||||
@click="handleSelect(item)"
|
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<SvgIcon icon="ri:message-3-line" />
|
<SvgIcon icon="ri:message-3-line" />
|
||||||
|
|
@ -91,10 +100,10 @@ function isActive(uuid: number) {
|
||||||
<NPopconfirm placement="bottom" @positive-click="handleDelete(item)">
|
<NPopconfirm placement="bottom" @positive-click="handleDelete(item)">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<button class="p-1">
|
<button class="p-1">
|
||||||
<SvgIcon icon="ri:delete-bin-line" />
|
<!-- <SvgIcon icon="ri:delete-bin-line" /> -->
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
{{ $t('chat.deleteHistoryConfirm') }}
|
确定删除此文件?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script setup lang='ts'>
|
||||||
|
import { NButton, NForm, NFormItem, NInput } from 'naive-ui'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import filelist from './filelist.vue'
|
||||||
|
import { getfilelist } from '@/api/chat'
|
||||||
|
const items = ref<any>([])
|
||||||
|
onMounted(async () => {
|
||||||
|
const res = await getfilelist({})
|
||||||
|
res.data.data.forEach((item: any) => {
|
||||||
|
items.value.push({
|
||||||
|
value: item,
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const formValue = ref({
|
||||||
|
user: {
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const rules = {
|
||||||
|
user: {
|
||||||
|
name: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入名称',
|
||||||
|
trigger: 'blur',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const handleValidateClick = (item: any) => {
|
||||||
|
items.value.forEach((res: { value: any; show: boolean }) => {
|
||||||
|
if (res.value === item)
|
||||||
|
res.show = !res.show
|
||||||
|
},
|
||||||
|
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const handleClick = () => {
|
||||||
|
if (formValue.value.user.name.trim() !== '') {
|
||||||
|
items.value.push({
|
||||||
|
value: formValue.value.user.name.trim(),
|
||||||
|
show: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<NForm
|
||||||
|
ref="formRef"
|
||||||
|
inline
|
||||||
|
:label-width="80"
|
||||||
|
:model="formValue"
|
||||||
|
:rules="rules"
|
||||||
|
>
|
||||||
|
<NFormItem label="" path="user.name">
|
||||||
|
<NInput v-model:value="formValue.user.name" placeholder="起个知识库名吧!" />
|
||||||
|
</NFormItem>
|
||||||
|
<NFormItem>
|
||||||
|
<NButton attr-type="button" @click="handleClick">
|
||||||
|
新增
|
||||||
|
</NButton>
|
||||||
|
</NFormItem>
|
||||||
|
</NForm>
|
||||||
|
<div v-for="item in items" :key="item.value">
|
||||||
|
<NButton block size="large" @click="handleValidateClick(item.value)">
|
||||||
|
{{ item.value }}
|
||||||
|
</NButton>
|
||||||
|
<div v-if="item.show" class="p-2 flex-1 min-h-0 pb-4 overflow-hidden">
|
||||||
|
<filelist v-if="item.value" :knowledgebaseid="item.value" />
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -37,7 +37,8 @@ export default defineConfig((env) => {
|
||||||
open: false,
|
open: false,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:7861',
|
target: 'http://146.56.190.29',
|
||||||
|
// target: 'http://127.0.0.1:7861',
|
||||||
changeOrigin: true, // 允许跨域
|
changeOrigin: true, // 允许跨域
|
||||||
rewrite: path => path.replace('/api/', ''),
|
rewrite: path => path.replace('/api/', ''),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue