feat(web): redesign provider interface (#25)

This commit is contained in:
Quincy
2026-02-03 16:42:43 +08:00
committed by GitHub
parent d775c6df6d
commit e6fd287b4d
45 changed files with 1647 additions and 480 deletions
+16 -19
View File
@@ -1,29 +1,26 @@
<template>
<section class="h-[calc(100vh-calc(var(--spacing)*20))] max-w-187 gap-8 w-full *:w-full m-auto flex flex-col">
<!-- <ScrollArea class="flex-none w-full rounded-md border">
<div class="p-4">
<h4 class="mb-4 text-sm leading-none font-medium">
Tags
</h4>
<template
v-for="tag in 1000"
:key="tag"
>
<div class="text-sm">
{{ tag }}
</div>
</template>
</div>
</ScrollArea> -->
<section class="flex-1 h-0">
<section class="h-[calc(100vh-calc(var(--spacing)*20))] [&_s] max-w-187 gap-8 w-full *:w-full m-auto flex flex-col ">
<section class="flex-1 h-0 [&:has(p)]:block! [&:has(p)+section_.logo-title]:hidden [&:has(p)+section]:mt-0! hidden">
<ScrollArea
ref="chat-container"
class="max-h-full h-full w-full rounded-md border p-4 **:focus-visible:ring-0! "
class="max-h-full h-full w-full rounded-md p-4 **:focus-visible:ring-0! "
>
<ChatList />
</ScrollArea>
</section>
<section class="flex-none relative">
<section class="flex-none relative mt-60">
<section class="mb-8 logo-title">
<img
src="../../../public/logo.png"
width="100"
class="m-auto"
alt="logo.png"
>
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight text-center text-muted-foreground title-container">
Memoh
</h4>
</section>
<Textarea
v-model="curInputSay"
class="pb-16 pt-4"
+23 -25
View File
@@ -1,9 +1,6 @@
<template>
<section class="w-screen h-screen flex *:m-auto bg-linear-to-t from-[#BFA4A0] to-[#7784AC] ">
<section
v-if="!loading"
class="w-full max-w-sm flex flex-col gap-10 "
>
<section class="w-full max-w-sm flex flex-col gap-10 ">
<section>
<img
src="../../../public/logo.png"
@@ -50,7 +47,7 @@
<FormControl>
<Input
type="password"
:placeholder="$t('prompt.enter',{msg:$t(`login.password`).toLocaleLowerCase()})"
:placeholder="$t('prompt.enter', { msg: $t(`login.password`).toLocaleLowerCase() })"
autocomplete="password"
v-bind="componentField"
/>
@@ -75,6 +72,7 @@
type="submit"
@click="login"
>
<Spinner v-if="loading" />
{{ $t("login.login") }}
</Button>
<Button
@@ -87,17 +85,6 @@
</Card>
</form>
</section>
<section
v-else
class="fixed inset-0 flex"
>
<img
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik0xMiwxQTExLDExLDAsMSwwLDIzLDEyLDExLDExLDAsMCwwLDEyLDFabTAsMTlhOCw4LDAsMSwxLDgtOEE4LDgsMCwwLDEsMTIsMjBaIiBvcGFjaXR5PSIwLjI1Ii8+PHBhdGggZmlsbD0iY3VycmVudENvbG9yIiBkPSJNMTAuMTQsMS4xNmExMSwxMSwwLDAsMC05LDguOTJBMS41OSwxLjU5LDAsMCwwLDIuNDYsMTIsMS41MiwxLjUyLDAsMCwwLDQuMTEsMTAuN2E4LDgsMCwwLDEsNi42Ni02LjYxQTEuNDIsMS40MiwwLDAsMCwxMiwyLjY5aDBBMS41NywxLjU3LDAsMCwwLDEwLjE0LDEuMTZaIj48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIGR1cj0iMC43NXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiB0eXBlPSJyb3RhdGUiIHZhbHVlcz0iMCAxMiAxMjszNjAgMTIgMTIiLz48L3BhdGg+PC9zdmc+"
alt=""
width="80"
class="m-auto"
>
</section>
</section>
</template>
@@ -113,6 +100,7 @@ import {
FormItem,
FormLabel,
FormMessage,
Spinner
} from '@memoh/ui'
import { useRouter } from 'vue-router'
import { toTypedSchema } from '@vee-validate/zod'
@@ -121,7 +109,7 @@ import * as z from 'zod'
import request from '@/utils/request'
import { useUserStore } from '@/store/User.ts'
import { ref } from 'vue'
import { toast } from 'vue-sonner'
const router = useRouter()
const formSchema = toTypedSchema(z.object({
username: z.string().min(1),
@@ -132,26 +120,36 @@ const form = useForm({
})
const { login: LoginHandle } = useUserStore()
const loading=ref(false)
const loading = ref(false)
const login = form.handleSubmit(async (values) => {
try {
loading.value=true
loading.value = true
const loginState = await request({
url: '/auth/login',
method: 'post',
data: { ...values }
})
}, false)
const data = loginState?.data
if (data?.access_token) {
LoginHandle({ id: data.user_id, username: data.username, role: data.role, displayName: data.display_name }, data.access_token)
}
if (data?.access_token && data?.user_id) {
LoginHandle({
id: data?.user_id,
username: data?.username,
displayName: '',
role: ''
}, data.access_token)
} else {
throw new Error('用户名和密码错误')
}
router.replace({
name:'Main'
name: 'Main'
})
} catch (error) {
toast.error('用户名或密码错误', {
description: '请重新输入用户名和密码',
})
return error
} finally {
loading.value=false
loading.value = false
}
})
+172 -201
View File
@@ -1,230 +1,201 @@
<script setup lang="ts">
// import type { Payment } from '@/components/columns'
import { h, computed, ref, provide, watch, type ComputedRef, reactive } from 'vue'
import CreateModel from '@/components/CreateModel/index.vue'
import { useQuery, useMutation, useQueryCache } from '@pinia/colada'
import { computed, ref, provide, watch, reactive } from 'vue'
import modelSetting from './modelSetting.vue'
import { useQuery, useQueryCache } from '@pinia/colada'
import {
Button,
// Pagination,
// PaginationContent,
// PaginationEllipsis,
// PaginationItem,
// PaginationNext,
// PaginationPrevious,
Checkbox
ScrollArea,
Sidebar,
SidebarContent,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
InputGroup, InputGroupAddon, InputGroupInput,
SidebarFooter,
Toggle,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectGroup,
SelectItem,
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from '@memoh/ui'
import DataTable from '@/components/DataTable/index.vue'
import { mdiMagnify,mdiListBoxOutline } from '@mdi/js'
// import DataTable from '@/components/DataTable/index.vue'
import SvgIcon from '@jamescoyle/vue-icon'
import request from '@/utils/request'
import { type ColumnDef } from '@tanstack/vue-table'
import {type ModelTable as ModelType} from '@memoh/shared'
import { i18nRef } from '@/i18n'
import { type ProviderInfo } from '@memoh/shared'
import AddProvider from '@/components/AddProvider/index.vue'
import { clientType } from '@memoh/shared'
const openDialogModel = ref(false)
const editModelInfo = ref<ModelType & { id: string } | null>(null)
provide('open', openDialogModel)
provide('editModelInfo', editModelInfo)
const filterProvider = ref('')
const { data: providerData } = useQuery({
key: ['provider'],
query: () => request({
url: `/providers?client_type=${filterProvider.value}`,
watch(openDialogModel, () => {
if (!openDialogModel.value) {
editModelInfo.value = null
}
}).then(fetchValue => fetchValue.data)
})
const queryCache = useQueryCache()
watch(filterProvider, () => {
queryCache.invalidateQueries({
key: ['provider']
})
}, {
immediate: true
})
const cacheQuery = useQueryCache()
const {
mutate: deleteModel,
} = useMutation({
mutation: (id: string) =>
request({
url: `model/${id}`,
method: 'DELETE'
}),
onSettled: () => {
cacheQuery.invalidateQueries({
key: ['models']
})
}
const curProvider = ref<Partial<ProviderInfo> & { id: string }>()
const selectProvider = (value: string) => computed(() => {
return curProvider.value?.name === value
})
const {
mutate: setDefaultModel,
} = useMutation({
mutation: (payload: { id: string, type: string }) =>
request({
url: `/model/${payload.type}/default?userId=${payload.id}`,
method: 'get'
}),
onSettled: () => {
cacheQuery.invalidateQueries({
key: ['models']
})
}
const searchProviderTxt = reactive({
temp_value: '',
value: ''
})
const curFilterProvider = computed(() => {
if (!Array.isArray(providerData.value)) {
return []
}
const searchReg = new RegExp([...searchProviderTxt.value].map(v => `\\u{${v.codePointAt(0)?.toString(16)}}`).join(''), 'u')
return providerData.value.filter((provider: Partial<ProviderInfo> & { id: string }) => {
return searchReg.test(provider.name as string)
})
})
const renderCheckDefault = () => {
return [...[{ title: 'Chat', key: 'chat', type: 'defaultChatModel' },
{ title: 'Summary', key: 'summary', type: 'defaultSummaryModel' },
{ title: 'Embedding', key: 'embedding', type: 'defaultEmbeddingModel' }].map((modelSetting) => (
{
accessorKey: `${modelSetting.key}`,
header: () => h('div', { class: 'text-left' }, modelSetting.title),
cell({ row }) {
const type = modelSetting.type as 'defaultChatModel' | 'defaultSummaryModel' | 'defaultEmbeddingModel'
return row.original.type === modelSetting.key ? h(Checkbox, {
state: row.original[type],
disabled: row.original[type] ? true : false,
'onUpdate:modelValue'(val) {
row.original[type] = val as boolean
setDefaultModel({
id: row.original.id,
type: modelSetting.key
})
}
}) : h('div')
}
} as ColumnDef<ModelType>
))]
}
const checkDefaultModel = ref(renderCheckDefault())
const columns: ComputedRef<ColumnDef<ModelType>[]> = computed(() => [
{
accessorKey: 'modelId',
header: () => h('div', { class: 'text-left py-4' }, 'Name'),
cell({ row }) {
return h('div', { class: 'text-left' }, row.getValue('modelId'))
watch(curFilterProvider, () => {
if (Array.isArray(curFilterProvider.value) && curFilterProvider.value.length > 0) {
curProvider.value = curFilterProvider.value[0]
} else {
curProvider.value = {
id:''
}
},
{
accessorKey: 'baseUrl',
header: () => h('div', { class: 'text-left' }, 'Base Url'),
},
{
accessorKey: 'apiKey',
header: () => h('div', { class: 'text-left' }, 'Api Key'),
},
{
accessorKey: 'clientType',
header: () => h('div', { class: 'text-left' }, 'Client Type'),
},
{
accessorKey: 'name',
header: () => h('div', { class: 'text-left' }, 'Name'),
},
{
accessorKey: 'type',
header: () => h('div', { class: 'text-left' }, 'Type'),
},
...checkDefaultModel.value
,
{
accessorKey: 'control',
header: () => h('div', { class: 'text-center' }, '操作'),
cell: ({ row }) => h('div', { class: ' w-full flex justify-center gap-4' }, [h(Button, {
'onClick': () => {
editModelInfo.value = row.original
openDialogModel.value = true
}
}, () => i18nRef('button.edit').value), h(Button, {
variant: 'destructive', onClick() {
deleteModel(row.original.id)
}
}, () => i18nRef('button.delete').value)])
}
])
const { data: modelData } = useQuery({
key: ['models'],
async query() {
const fetchModeData = await request({
url: '/model'
})
const defaultModel = await request({
url: '/settings'
})
const defaultModelValue = defaultModel?.data?.data
fetchModeData.data.items = fetchModeData.data.items.map((item: { model: ModelType, id: 'string' }) => ({
id: item.id,
model: {
...item.model,
defaultChatModel: defaultModelValue?.defaultChatModel === item.id ? true : false,
defaultEmbeddingModel: defaultModelValue?.defaultEmbeddingModel === item.id ? true : false,
defaultSummaryModel: defaultModelValue?.defaultSummaryModel === item.id ? true : false
}
}))
return fetchModeData
}
}, {
immediate: true
})
provide('curProvider', curProvider)
watch(modelData, () => {
checkDefaultModel.value = renderCheckDefault()
})
const displayFormat = computed(() => {
return modelData.value?.data?.items?.map((currentModel: { model: Omit<ModelType, 'id'>, id: 'string' }) => ({ id: currentModel.id, ...currentModel.model })) ?? []
})
const pagination = computed(() => {
return modelData.value?.data.pagination ?? {}
const openStatus = reactive({
provideOpen: false
})
</script>
<template>
<div class="w-full py-10 mx-auto">
<div class="flex mb-4">
<CreateModel />
</div>
<div class="[&_td:last-child]:w-45">
<DataTable
:columns="columns"
:data="displayFormat"
/>
</div>
<!-- <div class="flex flex-col mt-4">
<Pagination
v-slot="{ page }"
:total="pagination.value?.total ?? 0"
:items-per-page="10"
show-edges
<div class="w-full mx-auto">
<div class="[&_td:last-child]:w-45 model-select">
<SidebarProvider
:open="true"
class="min-h-[initial]! flex **:data-[sidebar=sidebar]:bg-transparent absolute inset-0"
>
<PaginationContent v-slot="{ items }">
<PaginationPrevious />
<template
v-for="(item, index) in items"
:key="index"
<Sidebar class="h-full relative top-0 ">
<SidebarHeader>
<InputGroup class="shadow-none">
<InputGroupInput
v-model="searchProviderTxt.temp_value"
placeholder="搜索模型平台"
/>
<InputGroupAddon
align="inline-end"
class="cursor-pointer"
@click="() => {
searchProviderTxt.value = searchProviderTxt.temp_value
}"
>
<svg-icon
type="mdi"
:path="mdiMagnify"
class="translate-icon"
/>
</InputGroupAddon>
</InputGroup>
</SidebarHeader>
<SidebarContent class="px-2 scrollbar-none">
<SidebarMenu
v-for="providerItem in curFilterProvider"
:key="providerItem.name"
>
<SidebarMenuItem>
<SidebarMenuButton
as-child
class="justify-start py-5! px-4"
>
<Toggle
:class="`py-4 border border-transparent ${curProvider?.name === providerItem.name ? 'border-inherit' : ''}`"
:model-value="selectProvider(providerItem.name as string).value"
@update:model-value="(isSelect) => {
if (isSelect) {
curProvider = providerItem
}
}"
>
{{ providerItem.name }}
</Toggle>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarContent>
<SidebarFooter>
<Select v-model:model-value="filterProvider">
<SelectTrigger class="w-full">
<SelectValue :placeholder="$t('prompt.select', { msg: 'Type' })" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem
v-for="type in clientType"
:key="type"
:value="type"
>
{{ type }}
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<AddProvider v-model:open="openStatus.provideOpen" />
</SidebarFooter>
</Sidebar>
<section class="flex-1 h-full ">
<ScrollArea
v-if="curProvider?.id"
class="max-h-full h-full"
>
<PaginationItem
v-if="item.type === 'page'"
:key="index"
:value="item.value"
:is-active="item.value === page"
>
{{ item.value }}
</PaginationItem>
<PaginationEllipsis
v-else
:key="item.type"
:index="index"
class="w-9 h-9 flex items-center justify-center"
>
&#8230;
</PaginationEllipsis>
</template>
<PaginationNext />
</PaginationContent>
</Pagination>
</div> -->
<model-setting />
</ScrollArea>
<Empty
v-else
class="h-full flex justify-center items-center"
>
<EmptyHeader>
<EmptyMedia variant="icon">
<svg-icon
type="mdi"
:path="mdiListBoxOutline"
/>
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>No Provider</EmptyTitle>
<EmptyDescription>没有添加模型提供商,无法配置模型</EmptyDescription>
<EmptyContent>
<!-- <Button>Add data</Button> -->
<AddProvider v-model:open="openStatus.provideOpen" />
</EmptyContent>
</Empty>
</section>
</SidebarProvider>
</div>
</div>
</template>
@@ -0,0 +1,426 @@
<template>
<div class="p-4 **:[input]:mt-3 **:[input]:mb-4">
<section class="flex justify-between items-center ">
<h4 class="scroll-m-20 tracking-tight">
{{ curProvider?.name }}
</h4>
</section>
<Separator class="mt-4 mb-6" />
<form @submit="editProvider">
<section>
<h4 class="scroll-m-20 font-semibold tracking-tight">
Name
</h4>
<FormField
v-slot="{ componentField }"
name="name"
>
<FormItem>
<FormControl>
<Input
type="text"
placeholder="请输入API密钥"
v-bind="componentField"
/>
</FormControl>
</FormItem>
</FormField>
</section>
<section>
<h4 class="scroll-m-20 font-semibold tracking-tight">
API 密钥
</h4>
<FormField
v-slot="{ componentField }"
name="api_key"
>
<FormItem>
<FormControl>
<Input
type="text"
placeholder="请输入API密钥"
v-bind="componentField"
/>
</FormControl>
</FormItem>
</FormField>
</section>
<section>
<h4 class="scroll-m-20 font-semibold tracking-tight">
URL
</h4>
<FormField
v-slot="{ componentField }"
name="base_url"
>
<FormItem>
<FormControl>
<Input
type="text"
placeholder="请输入URL"
v-bind="componentField"
/>
</FormControl>
</FormItem>
</FormField>
</section>
<section class="flex justify-end mt-4 gap-4">
<Popover>
<template #default="{ close }">
<PopoverTrigger as-child>
<Button variant="outline">
<svg-icon
type="mdi"
:path="mdiTrashCanOutline"
/>
</Button>
</PopoverTrigger>
<PopoverContent class="w-80">
<div class="grid gap-4">
<p class="leading-7 not-first:mt-6 ">
确认是否删除模型平台?
</p>
<section class="flex gap-4">
<Button
variant="outline"
class="ml-auto"
>
取消
</Button>
<Button @click="() => { deleteProvider(); close() }">
<Spinner v-if="deleteLoading" />
确定
</Button>
</section>
</div>
</PopoverContent>
</template>
</Popover>
<Button
type="submit"
:disabled="isChange || !form.meta.value.valid"
>
<Spinner v-if="editLoading" />
确定修改
</Button>
</section>
</form>
<Separator class="mt-4 mb-6" />
<section>
<section class="flex justify-between items-center mb-4 ">
<h4 class="scroll-m-20 font-semibold tracking-tight">
模型
</h4>
<CreateModel
v-if="curProvider?.id !== undefined"
:id="curProvider?.id as string"
/>
</section>
<section
v-if="modelDataList?.length > 0"
class="flex flex-col gap-4"
>
<Item
v-for="modelData in modelDataList"
:key="modelData.model_id"
variant="outline"
>
<ItemContent>
<ItemTitle>
{{ modelData.name }}
</ItemTitle>
<ItemDescription class="gap-2 flex flex-wrap items-center mt-3 ">
<Badge
variant="outline"
>
{{ modelData.type }}
</Badge>
</ItemDescription>
</ItemContent>
<ItemActions>
<Select
:default-value="modelData.enable_as"
@update:model-value="(value) => {
modelData.value = value
enableModel({
as: value as string === 'empty' ? '' : value as string,
model_id: modelData.model_id
})
}"
>
<SelectTrigger class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="empty">
No Enable
</SelectItem>
<SelectItem value="chat">
Chat
</SelectItem>
<SelectItem value="embedding">
Embedding
</SelectItem>
<SelectItem value="memery">
Memery
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
<Button
variant="outline"
class="cursor-pointer"
@click="() => {
openModel.state = true;
openModel.title = 'edit';
openModel.curState = deleteEnableAd(modelData)
}"
>
<svg-icon
type="mdi"
:path="mdiCog"
/>
</Button>
<Popover>
<template #default="{ close }">
<PopoverTrigger as-child>
<Button variant="outline">
<svg-icon
type="mdi"
:path="mdiTrashCanOutline"
/>
</Button>
</PopoverTrigger>
<PopoverContent class="w-80">
<div class="grid gap-4">
<p class="leading-7 not-first:mt-6 ">
确认是否删除模型?
</p>
<section class="flex gap-4">
<Button
variant="outline"
class="ml-auto"
>
取消
</Button>
<Button @click="() => { deleteModel(modelData.name); close() }">
<Spinner v-if="deleteModelLoading" />
确定
</Button>
</section>
</div>
</PopoverContent>
</template>
</Popover>
</ItemActions>
</Item>
</section>
<Empty
v-else
class="h-full flex justify-center items-center"
>
<EmptyHeader>
<EmptyMedia variant="icon">
<svg-icon
type="mdi"
:path="mdiListBoxOutline"
/>
</EmptyMedia>
</EmptyHeader>
<EmptyTitle>还没有添加模型</EmptyTitle>
<EmptyDescription>请为当前Provider添加模型</EmptyDescription>
<EmptyContent>
<!-- <Button>Add data</Button> -->
</EmptyContent>
</Empty>
</section>
</div>
</template>
<script setup lang="ts">
import {
Switch, Separator, Spinner, Input, Button,
FormControl,
FormField,
FormItem,
Item,
ItemContent,
ItemDescription,
ItemActions,
ItemTitle,
Badge,
Popover,
PopoverContent,
PopoverTrigger,
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectGroup,
SelectItem
} from '@memoh/ui'
import CreateModel from '@/components/CreateModel/index.vue'
import { computed, inject, provide, reactive, ref, toRef, toValue, watch } from 'vue'
import { type ProviderInfo } from '@memoh/shared'
import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
import request from '@/utils/request'
import { toTypedSchema } from '@vee-validate/zod'
import z from 'zod'
import { useForm } from 'vee-validate'
import SvgIcon from '@jamescoyle/vue-icon'
import { mdiListBoxOutline, mdiCog, mdiTrashCanOutline } from '@mdi/js'
import { type ModelInfo } from '@memoh/shared'
const openModel = reactive<{
state: boolean,
title: 'title' | 'edit',
curState: ModelInfo | null
}>({
state: false,
title: 'title',
curState: null
})
provide('openModel', toRef(openModel, 'state'))
provide('openModelTitle', toRef(openModel, 'title'))
provide('openModelState', toRef(openModel, 'curState'))
const deleteEnableAd = (value:ModelInfo) => {
const copyModelData = { ...value }
if ('enable_as' in copyModelData) {
delete copyModelData['enable_as']
}
return copyModelData
}
const providerSchema = toTypedSchema(z.object({
name: z.string().min(1),
base_url: z.string().min(1),
client_type: z.string().min(1),
api_key: z.string().min(1),
metadata: z.object({
additionalProp1: z.object()
})
}))
const form = useForm({
validationSchema: providerSchema
})
const curProvider = inject('curProvider', ref<Partial<ProviderInfo & { id: string }>>())
const queryCache = useQueryCache()
const { mutate: deleteProvider, isLoading: deleteLoading } = useMutation({
mutation: () => request({
url: `/providers/${curProvider.value?.id}`,
method: 'DELETE'
}),
onSettled: () => queryCache.invalidateQueries({
key: ['provider']
})
})
const { mutate: changeProvider, isLoading: editLoading } = useMutation({
mutation: (data: typeof form.values) => request({
url: `/providers/${curProvider.value?.id}`,
method: 'PUT',
data
}),
onSettled: () => queryCache.invalidateQueries({
key: ['provider']
})
})
const { mutate: deleteModel, isLoading: deleteModelLoading } = useMutation({
mutation: (id) => request({
url: `/models/model/${id}`,
method: 'DELETE'
}),
onSettled: () => queryCache.invalidateQueries({
key: ['model']
})
})
const { mutate: enableModel } = useMutation({
mutation: (data: { as: string, model_id: string }) => (request({
url: '/models/enable',
data,
method: 'post'
})),
onSettled: () => queryCache.invalidateQueries({
key: ['model']
})
})
const { mutate: updateMultimodal } = useMutation({
mutation: (data: ModelInfo) => request({
url: `models/model/${data?.model_id}`,
data,
method: 'PUT'
}),
onSettled: () => {
queryCache.invalidateQueries({
key: ['model']
})
}
})
const { data: modelDataList } = useQuery({
key: ['model'],
query: () => request({
url: `/providers/${curProvider.value?.id}/models`,
}).then(fetchData => fetchData.data.map((model: ModelInfo) => ({
...model,
enable_as: model.enable_as ?? 'empty'
})))
})
const editProvider = form.handleSubmit(async (value) => {
try {
await changeProvider(value)
} catch {
return
}
})
watch(curProvider, (newVal) => {
form.setValues({
name: newVal?.name,
base_url: newVal?.base_url,
client_type: newVal?.client_type,
api_key: newVal?.api_key
})
queryCache.invalidateQueries({
key: ['model']
})
}, {
immediate: true
})
const isChange = computed(() => {
const rawCurProvider = toValue(curProvider)
return JSON.stringify(form.values) === JSON.stringify({
name: rawCurProvider?.name,
base_url: rawCurProvider?.base_url,
client_type: rawCurProvider?.client_type,
api_key: rawCurProvider?.api_key,
metadata: {
additionalProp1: {}
}
})
})
</script>
@@ -180,7 +180,6 @@
import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
import request from '@/utils/request'
import { watch, reactive, computed } from 'vue'
import { type ModelTable } from '@memoh/shared'
import { toTypedSchema } from '@vee-validate/zod'
import z from 'zod'
import { useForm, useFormValues } from 'vee-validate'