mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat(web): redesign provider interface (#25)
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
…
|
||||
</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'
|
||||
|
||||
Reference in New Issue
Block a user