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:
@@ -4,28 +4,31 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
Toaster,
|
||||
Separator
|
||||
} from '@memoh/ui'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiTranslate, mdiBrightness6 } from '@mdi/js'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
// @ts-ignore
|
||||
import 'vue-sonner/style.css'
|
||||
|
||||
const mode = useColorMode()
|
||||
const modeToggleMap:Record<'dark'|'light','dark'|'light'> = {
|
||||
const modeToggleMap: Record<'dark' | 'light', 'dark' | 'light'> = {
|
||||
dark: 'light',
|
||||
light:'dark'
|
||||
light: 'dark'
|
||||
}
|
||||
console.log(mode.value)
|
||||
const toggleMode = () => {
|
||||
if (mode.value !== 'auto') {
|
||||
mode.value = modeToggleMap[mode.value]
|
||||
mode.value = modeToggleMap[mode.value]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<section class="[&_input]:shadow-none!">
|
||||
<div
|
||||
class="fixed top-0 flex right-8 z-9999 [&:is(:has([data-state=open]))_.translate-icon]:opacity-100 align h-16 items-center"
|
||||
>
|
||||
@@ -38,7 +41,7 @@ const toggleMode = () => {
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="$i18n.locale= 'zh'">
|
||||
<DropdownMenuItem @click="$i18n.locale = 'zh'">
|
||||
中文
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="$i18n.locale = 'en'">
|
||||
@@ -46,7 +49,7 @@ const toggleMode = () => {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiBrightness6"
|
||||
@@ -55,5 +58,6 @@ const toggleMode = () => {
|
||||
/>
|
||||
</div>
|
||||
<RouterView />
|
||||
<Toaster position="top-center" />
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<section>
|
||||
<Dialog v-model:open="open">
|
||||
<DialogTrigger as-child>
|
||||
<Button
|
||||
class="w-full shadow-none! text-muted-foreground mb-4"
|
||||
variant="outline"
|
||||
>
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiPlus"
|
||||
class="mr-1"
|
||||
/> 添加
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="sm:max-w-106.25">
|
||||
<form @submit="createProvider">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加提供商</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Separator class="my-4" />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
|
||||
<div class="flex-col gap-3 flex">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="name"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
Name
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入Name"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="api_key"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
API 密钥
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入Api Key"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="base_url"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
URL
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入URL"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="client_type"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
Type
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<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>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
<DialogFooter class="mt-8">
|
||||
<DialogClose as-child>
|
||||
<Button variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="(form.meta.value.valid===false)||isLoading"
|
||||
>
|
||||
<Spinner
|
||||
v-if="isLoading"
|
||||
class="mr-1"
|
||||
/>
|
||||
添加MCP
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { mdiPlus } from '@mdi/js'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
FormField,
|
||||
FormControl,
|
||||
FormItem,
|
||||
DialogDescription,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
Separator,
|
||||
Label,
|
||||
Spinner
|
||||
} from '@memoh/ui'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import z from 'zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import request from '@/utils/request'
|
||||
import { type ProviderInfo } from '@memoh/shared'
|
||||
import { clientType } from '@memoh/shared'
|
||||
|
||||
|
||||
const open = defineModel<boolean>('open')
|
||||
|
||||
const cacheQuery=useQueryCache()
|
||||
const {mutate:providerFetch,isLoading}=useMutation({
|
||||
mutation: (data: ProviderInfo) => request({
|
||||
url: '/providers',
|
||||
data,
|
||||
method:'post'
|
||||
}),
|
||||
onSettled: () => cacheQuery.invalidateQueries({
|
||||
key:['provider']
|
||||
})
|
||||
})
|
||||
const providerSchema = toTypedSchema(z.object({
|
||||
api_key: z.string().min(1),
|
||||
base_url: z.string().min(1),
|
||||
client_type: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
metadata: z.object({
|
||||
additionalProp1:z.object()
|
||||
})
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: providerSchema,
|
||||
})
|
||||
|
||||
const createProvider=form.handleSubmit(async (value) => {
|
||||
try {
|
||||
|
||||
await providerFetch(value)
|
||||
open.value=false
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -3,145 +3,82 @@
|
||||
<Dialog v-model:open="open">
|
||||
<DialogTrigger as-child>
|
||||
<Button variant="default">
|
||||
{{ $t("button.add",{msg:"Model"}) }}
|
||||
{{ $t("button.add", { msg: "Model" }) }}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="sm:max-w-106.25">
|
||||
<form @submit="addModel">
|
||||
<DialogHeader>
|
||||
<DialogTitle> {{ $t("button.add", { msg: "Model" }) }}</DialogTitle>
|
||||
<DialogTitle>
|
||||
<!-- {{ $t("button.add", { msg: "Model" }) }} -->
|
||||
{{ title === 'edit' ? '编辑Model' : '添加Model' }}
|
||||
</DialogTitle>
|
||||
<DialogDescription class="mb-4">
|
||||
使用不用厂商的大模型
|
||||
<Separator class="my-4" />
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="modelId"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Model Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="$t('prompt.enter',{msg:'Model Name'})"
|
||||
v-bind="componentField"
|
||||
autocomplete="modelId"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="baseUrl"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Base Url
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="$t('prompt.enter', { msg: 'Base Url' })"
|
||||
v-bind="componentField"
|
||||
autocomplete="baseurl"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="apiKey"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Api Key
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
:placeholder="$t('prompt.enter', { msg: 'Api Key' })"
|
||||
autocomplete="apiKey"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="clientType"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Client Type
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('prompt.select',{msg:'Client Type'})" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="OpenAI">
|
||||
OpenAI
|
||||
</SelectItem>
|
||||
<SelectItem value="Anthropic">
|
||||
Anthropic
|
||||
</SelectItem>
|
||||
<SelectItem value="Google">
|
||||
Google
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<div class="flex flex-col gap-3">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="name"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Display Name
|
||||
</FormLabel>
|
||||
<Label class="mb-2">
|
||||
Name
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
:placeholder="$t('prompt.enter', { msg: 'Display Name' })"
|
||||
autocomplete="name"
|
||||
type="text"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="model_id"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
Model ID
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="dimensions"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
Dimensions
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="type"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Role
|
||||
</FormLabel>
|
||||
<Label class="mb-2">
|
||||
Type
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('prompt.select', { msg: 'Role' })" />
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
@@ -155,9 +92,21 @@
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="is_multimodal"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
是否开启多模态
|
||||
</Label>
|
||||
<Switch
|
||||
id="airplane-mode"
|
||||
v-model="componentField.modelValue"
|
||||
@update:model-value="componentField['onUpdate:modelValue']"
|
||||
/>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
@@ -167,7 +116,11 @@
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="!form.meta.value.valid"
|
||||
>
|
||||
<Spinner v-if="isLoading" />
|
||||
{{ $t("button.add", { msg: "Model" }) }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -198,69 +151,81 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
Switch,
|
||||
Separator,
|
||||
Label,
|
||||
Spinner
|
||||
} from '@memoh/ui'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { inject, watch, type Ref,ref } from 'vue'
|
||||
import { inject, watch, type Ref, ref, reactive, computed } from 'vue'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import z from 'zod'
|
||||
import request from '@/utils/request'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
|
||||
import {type ModelInfo } from '@memoh/shared'
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
modelId:z.string().min(1),
|
||||
baseUrl: z.string().min(1),
|
||||
apiKey: z.string().min(1),
|
||||
clientType: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
type: z.string().min(1),
|
||||
'is_multimodal': z.coerce.boolean(),
|
||||
'model_id': z.string().min(1),
|
||||
'name': z.string().min(1),
|
||||
'type': z.string().min(1),
|
||||
'dimensions':z.coerce.number().min(1)
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: formSchema
|
||||
validationSchema: formSchema,
|
||||
initialValues: {
|
||||
dimensions:1
|
||||
}
|
||||
})
|
||||
|
||||
const { id } = defineProps<{ id: string }>()
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
type ModelInfoType= Parameters<(Parameters<typeof form.handleSubmit>)[0]>[0]
|
||||
const { mutate: createModel } = useMutation({
|
||||
mutation: (modelInfo:ModelInfoType ) => request({
|
||||
url: '/model',
|
||||
data: {
|
||||
...modelInfo,
|
||||
},
|
||||
method: 'post'
|
||||
}),
|
||||
onSettled: () => { open.value = false; queryCache.invalidateQueries({ key: ['models'], exact: true })}
|
||||
})
|
||||
|
||||
const { mutate: updateModel } = useMutation({
|
||||
mutation: (modelInfo: ModelInfoType) => request({
|
||||
url: `/model/${editInfo.value?.id}`,
|
||||
type ModelInfoType = Parameters<(Parameters<typeof form.handleSubmit>)[0]>[0]
|
||||
const { mutate: createModel,isLoading } = useMutation({
|
||||
mutation: (modelInfo: ModelInfoType & {
|
||||
dimensions: number,
|
||||
llm_provider_id: string
|
||||
}) => request({
|
||||
url: '/models',
|
||||
data: {
|
||||
...modelInfo,
|
||||
},
|
||||
method: 'PUT'
|
||||
method: 'post'
|
||||
}),
|
||||
onSettled: () => { open.value = false; queryCache.invalidateQueries({ key: ['models'], exact: true }) }
|
||||
})
|
||||
const addModel = form.handleSubmit(async (modelInfo) => {
|
||||
if (editInfo.value?.id) {
|
||||
updateModel(modelInfo)
|
||||
} else {
|
||||
createModel(modelInfo)
|
||||
}
|
||||
|
||||
onSettled: () => { open.value = false; queryCache.invalidateQueries({ key: ['model'], exact: true }) }
|
||||
})
|
||||
|
||||
const open = inject<Ref<boolean>>('open',ref(false))
|
||||
const editInfo = inject('editModelInfo',ref<null|(ModelInfoType&{id:string})>(null))
|
||||
watch(open, () => {
|
||||
|
||||
const addModel = form.handleSubmit(async (modelInfo) => {
|
||||
try {
|
||||
await createModel({
|
||||
...modelInfo,
|
||||
llm_provider_id: id
|
||||
})
|
||||
open.value=false
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const open = inject<Ref<boolean>>('openModel', ref(false))
|
||||
const title = inject<Ref<'edit' | 'title'>>('openModelTitle', ref('title'))
|
||||
const editInfo =inject<Ref<ModelInfo|null>>('openModelState',ref(null))
|
||||
|
||||
watch(open, () => {
|
||||
if (open.value && editInfo?.value) {
|
||||
form.setValues(editInfo.value)
|
||||
form.setValues(editInfo.value)
|
||||
} else {
|
||||
form.resetForm()
|
||||
}
|
||||
|
||||
if (!open.value) {
|
||||
title.value = 'title'
|
||||
editInfo.value=null
|
||||
}
|
||||
}, {
|
||||
immediate:true
|
||||
immediate: true
|
||||
})
|
||||
</script>
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<SidebarInset>
|
||||
<SidebarInset class="grid grid-rows-[auto_auto_1fr]">
|
||||
<header
|
||||
class="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12"
|
||||
>
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<div class="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger class="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
@@ -34,13 +34,19 @@
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
</header>
|
||||
<main class="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<router-view v-slot="{ Component }">
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</main>
|
||||
<Separator />
|
||||
<section class="w-full relative">
|
||||
<ScrollArea class="absolute! inset-0">
|
||||
<router-view
|
||||
v-slot="{ Component }"
|
||||
class="p-4"
|
||||
>
|
||||
<KeepAlive>
|
||||
<component :is="Component" />
|
||||
</KeepAlive>
|
||||
</router-view>
|
||||
</ScrollArea>
|
||||
</section>
|
||||
</SidebarInset>
|
||||
</template>
|
||||
|
||||
@@ -53,6 +59,7 @@ import {
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
Separator,
|
||||
ScrollArea
|
||||
// DropdownMenu,
|
||||
// DropdownMenuContent,
|
||||
// DropdownMenuItem,
|
||||
@@ -60,8 +67,6 @@ import {
|
||||
} from '@memoh/ui'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
// import SvgIcon from '@jamescoyle/vue-icon'
|
||||
// import { mdiTranslate } from '@mdi/js'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
|
||||
@@ -20,31 +20,35 @@
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
对话操作
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<Collapsible
|
||||
v-for="sidebarItem in sidebarInfo"
|
||||
:key="sidebarItem.title"
|
||||
class="group/collapsible"
|
||||
>
|
||||
<SidebarMenuItem>
|
||||
<CollapsibleTrigger as-child>
|
||||
<SidebarMenuButton
|
||||
:tooltip="sidebarItem.title"
|
||||
@click="router.push({ name: sidebarItem.name })"
|
||||
>
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="sidebarItem.icon"
|
||||
/>
|
||||
<span>{{ sidebarItem.title }}</span>
|
||||
</SidebarMenuButton>
|
||||
</CollapsibleTrigger>
|
||||
</SidebarMenuItem>
|
||||
</Collapsible>
|
||||
<SidebarGroupContent class="[&_ul+ul]:mt-2!">
|
||||
<SidebarMenu
|
||||
v-for="sidebarItem in sidebarInfo"
|
||||
:key="sidebarItem.title"
|
||||
>
|
||||
<SidebarMenuItem class="[&_[aria-pressed=true]]:bg-accent!">
|
||||
<SidebarMenuButton
|
||||
as-child
|
||||
class="justify-start py-5! px-4"
|
||||
:tooltip="sidebarItem.title"
|
||||
>
|
||||
<Toggle
|
||||
:class="` border border-transparent w-full flex justify-start ${curSlider === sidebarItem.name ? 'border-inherit' : ''}`"
|
||||
:model-value="curSelectSlide(sidebarItem.name as string).value"
|
||||
@update:model-value="(isSelect) => {
|
||||
if (isSelect) {
|
||||
curSlider = sidebarItem.name
|
||||
}
|
||||
}"
|
||||
@click="router.push({ name: sidebarItem.name })"
|
||||
>
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="sidebarItem.icon"
|
||||
/>
|
||||
<span>{{ sidebarItem.title }}</span>
|
||||
</Toggle>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
@@ -71,7 +75,6 @@ import {
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
@@ -79,19 +82,24 @@ import {
|
||||
SidebarRail,
|
||||
CollapsibleTrigger,
|
||||
Collapsible,
|
||||
Button
|
||||
Button,
|
||||
Toggle
|
||||
} from '@memoh/ui'
|
||||
import { computed } from 'vue'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiRobot, mdiChatOutline, mdiCogBox, mdiListBox, mdiHome, mdiBookArrowDown } from '@mdi/js'
|
||||
import { mdiRobot, mdiChatOutline, mdiCogBox } from '@mdi/js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/User.ts'
|
||||
import i18n from '@/i18n'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const router=useRouter()
|
||||
const router = useRouter()
|
||||
|
||||
const { t } = i18n.global
|
||||
|
||||
const curSlider = ref('chat')
|
||||
const curSelectSlide = (cur: string) => computed(() => {
|
||||
return curSlider.value === cur
|
||||
})
|
||||
const sidebarInfo = computed(() => [
|
||||
{
|
||||
title: t('slidebar.chat'),
|
||||
@@ -111,15 +119,16 @@ const sidebarInfo = computed(() => [
|
||||
title: t('slidebar.setting'),
|
||||
name: 'settings',
|
||||
icon: mdiCogBox
|
||||
}, {
|
||||
title: 'MCP',
|
||||
name: 'mcp',
|
||||
icon: mdiListBox
|
||||
}, {
|
||||
title: t('slidebar.platform'),
|
||||
name: 'platform',
|
||||
icon: mdiBookArrowDown
|
||||
}
|
||||
},
|
||||
// {
|
||||
// title: 'MCP',
|
||||
// name: 'mcp',
|
||||
// icon: mdiListBox
|
||||
// }, {
|
||||
// title: t('slidebar.platform'),
|
||||
// name: 'platform',
|
||||
// icon: mdiBookArrowDown
|
||||
// }
|
||||
])
|
||||
|
||||
const { exitLogin } = useUserStore()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<section class="flex">
|
||||
<sidebar-provider>
|
||||
<sidebar-provider class="**:data-[sidebar=sidebar]:bg-transparent">
|
||||
<slot name="sidebar" />
|
||||
<slot name="main" />
|
||||
</sidebar-provider>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -4,6 +4,19 @@
|
||||
|
||||
@layer components {
|
||||
@import 'markstream-vue/index.css';
|
||||
.scrollbar-none {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@layer utilities{
|
||||
.shadow-sm{
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
@@ -121,6 +134,7 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@source "../../ui/src";
|
||||
|
||||
Reference in New Issue
Block a user