mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
feat: models import (#164)
This commit is contained in:
@@ -90,6 +90,53 @@
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<Separator />
|
||||
|
||||
<FormField
|
||||
v-slot="{ value, handleChange }"
|
||||
name="auto_import"
|
||||
>
|
||||
<FormItem class="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div class="space-y-0.5">
|
||||
<Label class="text-base">
|
||||
{{ $t('provider.autoImport') }}
|
||||
</Label>
|
||||
<p class="text-[0.8rem] text-muted-foreground">
|
||||
{{ $t('provider.autoImportHint') }}
|
||||
</p>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
:model-value="value"
|
||||
@update:model-value="handleChange"
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-if="form.values.auto_import"
|
||||
v-slot="{ value, handleChange }"
|
||||
name="client_type"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
{{ $t('models.importClientType') }}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<SearchableSelectPopover
|
||||
:model-value="value"
|
||||
:options="CLIENT_TYPE_LIST"
|
||||
:placeholder="$t('models.clientTypePlaceholder')"
|
||||
@update:model-value="handleChange"
|
||||
/>
|
||||
</FormControl>
|
||||
<p class="text-[0.8rem] text-muted-foreground">
|
||||
{{ $t('models.importClientTypeHint') }}
|
||||
</p>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
</template>
|
||||
</FormDialogShell>
|
||||
@@ -103,26 +150,52 @@ import {
|
||||
FormControl,
|
||||
FormItem,
|
||||
Label,
|
||||
Switch,
|
||||
Separator,
|
||||
} from '@memoh/ui'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import z from 'zod'
|
||||
import { useForm,Form,Field } from 'vee-validate'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import { postProviders } from '@memoh/sdk'
|
||||
import { postProviders, postProvidersByIdImportModels } from '@memoh/sdk'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
|
||||
import { useDialogMutation } from '@/composables/useDialogMutation'
|
||||
import SearchableSelectPopover from '@/components/searchable-select-popover/index.vue'
|
||||
import { CLIENT_TYPE_LIST } from '@/constants/client-types'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const open = defineModel<boolean>('open')
|
||||
const { t } = useI18n()
|
||||
const { run } = useDialogMutation()
|
||||
|
||||
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
const { mutateAsync: createProviderMutation, isLoading } = useMutation({
|
||||
mutation: async (data: Record<string, unknown>) => {
|
||||
const { data: result } = await postProviders({ body: data as any, throwOnError: true })
|
||||
const payload = {
|
||||
...data,
|
||||
metadata: { additionalProp1: {} },
|
||||
}
|
||||
const { data: result } = await postProviders({ body: payload as any, throwOnError: true })
|
||||
if (data.auto_import && result?.id) {
|
||||
try {
|
||||
const { data: importResult } = await postProvidersByIdImportModels({
|
||||
path: { id: result.id },
|
||||
body: { client_type: data.client_type as string },
|
||||
throwOnError: true,
|
||||
})
|
||||
if (importResult) {
|
||||
toast.success(t('models.importSuccess', {
|
||||
created: importResult.created,
|
||||
skipped: importResult.skipped,
|
||||
}))
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Auto import failed:', e)
|
||||
toast.error(t('models.importFailed'))
|
||||
}
|
||||
}
|
||||
return result
|
||||
},
|
||||
onSettled: () => queryCache.invalidateQueries({ key: ['providers'] }),
|
||||
@@ -132,13 +205,16 @@ const providerSchema = toTypedSchema(z.object({
|
||||
api_key: z.string().min(1),
|
||||
base_url: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
metadata: z.object({
|
||||
additionalProp1: z.object({}),
|
||||
}),
|
||||
auto_import: z.boolean().optional(),
|
||||
client_type: z.string().optional(),
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: providerSchema,
|
||||
initialValues: {
|
||||
auto_import: false,
|
||||
client_type: 'openai-completions',
|
||||
},
|
||||
})
|
||||
|
||||
const createProvider = form.handleSubmit(async (value) => {
|
||||
@@ -148,6 +224,7 @@ const createProvider = form.handleSubmit(async (value) => {
|
||||
fallbackMessage: t('common.saveFailed'),
|
||||
onSuccess: () => {
|
||||
open.value = false
|
||||
form.resetForm()
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<FormDialogShell
|
||||
v-model:open="open"
|
||||
:title="$t('models.importModels')"
|
||||
:cancel-text="$t('common.cancel')"
|
||||
:submit-text="$t('common.import')"
|
||||
:submit-disabled="!clientType"
|
||||
:loading="isLoading"
|
||||
@submit="handleImport"
|
||||
>
|
||||
<template #trigger>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<FontAwesomeIcon :icon="['fas', 'file-import']" />
|
||||
{{ $t('models.importModels') }}
|
||||
</Button>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-3 mt-4">
|
||||
<Label class="mb-2">
|
||||
{{ $t('models.importClientType') }}
|
||||
</Label>
|
||||
<SearchableSelectPopover
|
||||
v-model="clientType"
|
||||
:options="clientTypeOptions"
|
||||
:placeholder="$t('models.clientTypePlaceholder')"
|
||||
class="w-full"
|
||||
/>
|
||||
<p class="text-[0.8rem] text-muted-foreground">
|
||||
{{ $t('models.importClientTypeHint') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</FormDialogShell>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import { postProvidersByIdImportModels } from '@memoh/sdk'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { Button, Label } from '@memoh/ui'
|
||||
import FormDialogShell from '@/components/form-dialog-shell/index.vue'
|
||||
import SearchableSelectPopover from '@/components/searchable-select-popover/index.vue'
|
||||
import { CLIENT_TYPE_LIST, CLIENT_TYPE_META } from '@/constants/client-types'
|
||||
import { useDialogMutation } from '@/composables/useDialogMutation'
|
||||
|
||||
const props = defineProps<{
|
||||
providerId: string
|
||||
}>()
|
||||
|
||||
const open = ref(false)
|
||||
const { t } = useI18n()
|
||||
const { run } = useDialogMutation()
|
||||
const queryCache = useQueryCache()
|
||||
|
||||
const clientType = ref('openai-completions')
|
||||
|
||||
const clientTypeOptions = computed(() =>
|
||||
CLIENT_TYPE_LIST.map((ct) => ({
|
||||
value: ct.value,
|
||||
label: ct.label,
|
||||
description: ct.hint,
|
||||
keywords: [ct.label, ct.hint, CLIENT_TYPE_META[ct.value]?.value ?? ct.value],
|
||||
})),
|
||||
)
|
||||
|
||||
const { mutateAsync: importModelsMutation, isLoading } = useMutation({
|
||||
mutation: async () => {
|
||||
const { data } = await postProvidersByIdImportModels({
|
||||
path: { id: props.providerId },
|
||||
body: { client_type: clientType.value },
|
||||
throwOnError: true,
|
||||
})
|
||||
return data
|
||||
},
|
||||
onSettled: () => {
|
||||
queryCache.invalidateQueries({ key: ['provider-models'] })
|
||||
},
|
||||
})
|
||||
|
||||
async function handleImport() {
|
||||
await run(
|
||||
() => importModelsMutation(),
|
||||
{
|
||||
fallbackMessage: t('models.importFailed'),
|
||||
onSuccess: (data) => {
|
||||
if (data) {
|
||||
toast.success(t('models.importSuccess', {
|
||||
created: data.created,
|
||||
skipped: data.skipped,
|
||||
}))
|
||||
}
|
||||
open.value = false
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
</script>
|
||||
@@ -42,8 +42,8 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<ScrollArea
|
||||
class="max-h-64"
|
||||
<div
|
||||
class="max-h-64 overflow-y-auto"
|
||||
role="listbox"
|
||||
>
|
||||
<div
|
||||
@@ -112,7 +112,7 @@
|
||||
</slot>
|
||||
</button>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
@@ -123,7 +123,6 @@ import {
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
Button,
|
||||
ScrollArea,
|
||||
} from '@memoh/ui'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
|
||||
@@ -169,7 +169,12 @@
|
||||
"testOk": "OK",
|
||||
"testAuthError": "Auth Error",
|
||||
"testError": "Error",
|
||||
"testFailed": "Test failed"
|
||||
"testFailed": "Test failed",
|
||||
"importModels": "Import Models",
|
||||
"importSuccess": "Successfully imported {created} models, skipped {skipped}",
|
||||
"importFailed": "Failed to import models",
|
||||
"importClientType": "Model Client Type",
|
||||
"importClientTypeHint": "Set default client type for imported models"
|
||||
},
|
||||
"provider": {
|
||||
"add": "Add Provider",
|
||||
@@ -185,7 +190,9 @@
|
||||
"testConnection": "Test Connection",
|
||||
"reachable": "Reachable",
|
||||
"unreachable": "Unreachable",
|
||||
"testFailed": "Test failed"
|
||||
"testFailed": "Test failed",
|
||||
"autoImport": "Auto Import Models",
|
||||
"autoImportHint": "Automatically fetch and import models from the provider after creation"
|
||||
},
|
||||
"searchProvider": {
|
||||
"title": "Search Providers",
|
||||
|
||||
@@ -165,7 +165,12 @@
|
||||
"testOk": "正常",
|
||||
"testAuthError": "认证失败",
|
||||
"testError": "异常",
|
||||
"testFailed": "测试失败"
|
||||
"testFailed": "测试失败",
|
||||
"importModels": "导入模型",
|
||||
"importSuccess": "成功导入 {created} 个模型,跳过 {skipped} 个",
|
||||
"importFailed": "导入模型失败",
|
||||
"importClientType": "模型客户端类型",
|
||||
"importClientTypeHint": "为导入的模型设置默认客户端类型"
|
||||
},
|
||||
"provider": {
|
||||
"add": "添加服务商",
|
||||
@@ -181,7 +186,9 @@
|
||||
"testConnection": "测试连接",
|
||||
"reachable": "可连接",
|
||||
"unreachable": "不可连接",
|
||||
"testFailed": "测试失败"
|
||||
"testFailed": "测试失败",
|
||||
"autoImport": "自动导入模型",
|
||||
"autoImportHint": "创建后自动从服务商获取并导入模型"
|
||||
},
|
||||
"searchProvider": {
|
||||
"title": "搜索提供方",
|
||||
|
||||
@@ -91,7 +91,7 @@ import {
|
||||
import ConfirmPopover from '@/components/confirm-popover/index.vue'
|
||||
import { postModelsByIdTest } from '@memoh/sdk'
|
||||
import type { ModelsGetResponse, ModelsTestResponse } from '@memoh/sdk'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
model: ModelsGetResponse
|
||||
@@ -132,7 +132,4 @@ async function runTest() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
runTest()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
<h4 class="scroll-m-20 font-semibold tracking-tight">
|
||||
{{ $t('models.title') }}
|
||||
</h4>
|
||||
<CreateModel
|
||||
<div
|
||||
v-if="providerId"
|
||||
:id="providerId"
|
||||
/>
|
||||
class="flex items-center gap-2 ml-auto"
|
||||
>
|
||||
<ImportModelsDialog :provider-id="providerId" />
|
||||
<CreateModel :id="providerId" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
@@ -50,6 +53,7 @@ import {
|
||||
EmptyTitle,
|
||||
} from '@memoh/ui'
|
||||
import CreateModel from '@/components/create-model/index.vue'
|
||||
import ImportModelsDialog from '@/components/import-models-dialog/index.vue'
|
||||
import ModelItem from './model-item.vue'
|
||||
import type { ModelsGetResponse } from '@memoh/sdk'
|
||||
|
||||
|
||||
@@ -73,9 +73,8 @@
|
||||
:disabled="!props.provider?.id"
|
||||
@click="runTest"
|
||||
>
|
||||
<Spinner v-if="testLoading" />
|
||||
<FontAwesomeIcon
|
||||
v-else
|
||||
v-if="!testLoading"
|
||||
:icon="['fas', 'rotate']"
|
||||
/>
|
||||
{{ $t('provider.testConnection') }}
|
||||
@@ -197,9 +196,10 @@ async function runTest() {
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.provider?.id, (newId) => {
|
||||
if (newId) runTest()
|
||||
}, { immediate: true })
|
||||
watch(() => props.provider?.id, () => {
|
||||
testResult.value = null
|
||||
testError.value = ''
|
||||
})
|
||||
|
||||
const providerSchema = toTypedSchema(z.object({
|
||||
name: z.string().min(1),
|
||||
|
||||
Reference in New Issue
Block a user