mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
Merge branch 'main' into refactor/channel-gateway
This commit is contained in:
+7
-1
@@ -20,10 +20,16 @@ export const createAgent = ({
|
||||
brave,
|
||||
language = 'Same as the user input',
|
||||
allowedActions = allActions,
|
||||
identity,
|
||||
channels = [],
|
||||
mcpConnections = [],
|
||||
currentChannel = 'Unknown Channel',
|
||||
identity = {
|
||||
botId: '',
|
||||
sessionId: '',
|
||||
containerId: '',
|
||||
contactId: '',
|
||||
contactName: '',
|
||||
},
|
||||
}: AgentParams, fetch: AuthFetcher) => {
|
||||
const model = createModel(modelConfig)
|
||||
|
||||
|
||||
@@ -41,10 +41,10 @@ export interface AgentParams {
|
||||
activeContextTime?: number
|
||||
allowedActions?: AgentAction[]
|
||||
brave?: BraveConfig
|
||||
identity: IdentityContext
|
||||
channels?: string[]
|
||||
currentChannel?: string
|
||||
mcpConnections?: MCPConnection[]
|
||||
identity?: IdentityContext
|
||||
}
|
||||
|
||||
export interface AgentInput {
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ host = "localhost"
|
||||
port = 5432
|
||||
user = "postgres"
|
||||
password = "1234"
|
||||
database = "postgres"
|
||||
database = "demo"
|
||||
sslmode = "disable"
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,15 @@ description = "Install CLI"
|
||||
depends = ["//:pnpm-install"]
|
||||
run = "cd packages/cli && npm install -g"
|
||||
|
||||
[tasks.build-cli]
|
||||
description = "Build Go CLI binary and install to local bin"
|
||||
run = """
|
||||
mkdir -p ~/.local/bin
|
||||
go build -trimpath -ldflags "-s -w" -o ~/.local/bin/container-cli ./cmd/cli
|
||||
chmod +x ~/.local/bin/container-cli
|
||||
echo "✓ CLI binary installed to ~/.local/bin/container-cli"
|
||||
"""
|
||||
|
||||
[tasks.mcp-image-up]
|
||||
description = "Build MCP container image"
|
||||
run = "scripts/mcp-image-up.sh"
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
words: string;
|
||||
filter?: boolean;
|
||||
duration?: number;
|
||||
delay?: number;
|
||||
class?: HTMLAttributes['class'];
|
||||
}>(),
|
||||
{ duration: 0.7, delay: 0, filter: true },
|
||||
)
|
||||
|
||||
const scope = ref(null)
|
||||
const wordsArray = computed(() => props.words.split(''))
|
||||
|
||||
const spanStyle = computed(() => ({
|
||||
opacity: 0,
|
||||
filter: props.filter ? 'blur(10px)' : 'none',
|
||||
transition: `opacity ${props.duration}s, filter ${props.duration}s`,
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
if (scope.value) {
|
||||
const spans = (scope.value as HTMLElement).querySelectorAll('span')
|
||||
|
||||
setTimeout(() => {
|
||||
spans.forEach((span: HTMLElement, index: number) => {
|
||||
setTimeout(() => {
|
||||
span.style.opacity = '1'
|
||||
span.style.filter = props.filter ? 'blur(0px)' : 'none'
|
||||
}, index * 200)
|
||||
})
|
||||
}, props.delay)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="leading-snug tracking-wide"
|
||||
:class="[props.class]"
|
||||
>
|
||||
<div ref="scope">
|
||||
<span
|
||||
v-for="(word, idx) in wordsArray"
|
||||
:key="word + idx"
|
||||
class="inline-block"
|
||||
:style="spanStyle"
|
||||
>
|
||||
{{ word }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as TextGenerateEffect } from "./TextGenerateEffect.vue";
|
||||
@@ -6,57 +6,18 @@ import {
|
||||
DropdownMenuItem,
|
||||
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'> = {
|
||||
dark: 'light',
|
||||
light: 'dark'
|
||||
}
|
||||
const toggleMode = () => {
|
||||
if (mode.value !== 'auto') {
|
||||
mode.value = modeToggleMap[mode.value]
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<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"
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger class="ml-auto mr-4 cursor-pointer">
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiTranslate"
|
||||
class="translate-icon opacity-30 hover:opacity-100"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem @click="$i18n.locale = 'zh'">
|
||||
中文
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="$i18n.locale = 'en'">
|
||||
English
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiBrightness6"
|
||||
class="translate-icon opacity-30 hover:opacity-100 cursor-pointer"
|
||||
@click="toggleMode"
|
||||
/>
|
||||
</div>
|
||||
<RouterView />
|
||||
<Toaster position="top-center" />
|
||||
</section>
|
||||
|
||||
@@ -10,11 +10,6 @@
|
||||
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>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
@@ -53,19 +48,6 @@
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem class="flex justify-center exist-btn">
|
||||
<Button
|
||||
class="flex-[0.7] mb-10"
|
||||
@click="exit"
|
||||
>
|
||||
{{ $t("login.exit") }}
|
||||
</Button>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -79,26 +61,25 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
CollapsibleTrigger,
|
||||
Collapsible,
|
||||
Button,
|
||||
|
||||
Toggle
|
||||
} from '@memoh/ui'
|
||||
import { computed } from 'vue'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiRobot, mdiChatOutline, mdiCogBox } from '@mdi/js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter,useRoute } from 'vue-router'
|
||||
import { useUserStore } from '@/store/User.ts'
|
||||
import i18n from '@/i18n'
|
||||
import { ref } from 'vue'
|
||||
|
||||
|
||||
const router = useRouter()
|
||||
const route=useRoute()
|
||||
|
||||
const { t } = i18n.global
|
||||
const curSlider = ref('chat')
|
||||
const curSlider = ref()
|
||||
const curSelectSlide = (cur: string) => computed(() => {
|
||||
return curSlider.value === cur
|
||||
return curSlider.value === cur||new RegExp(`^/main/${cur}$`).test(route.path)
|
||||
})
|
||||
const sidebarInfo = computed(() => [
|
||||
{
|
||||
@@ -131,9 +112,4 @@ const sidebarInfo = computed(() => [
|
||||
// }
|
||||
])
|
||||
|
||||
const { exitLogin } = useUserStore()
|
||||
const exit = () => {
|
||||
exitLogin()
|
||||
router.replace({ name: 'Login' })
|
||||
}
|
||||
</script>
|
||||
@@ -8,16 +8,15 @@
|
||||
<ChatList />
|
||||
</ScrollArea>
|
||||
</section>
|
||||
<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"
|
||||
<section class="flex-none relative m-auto">
|
||||
<section class="mb-20 logo-title">
|
||||
<h4
|
||||
class="scroll-m-20 text-3xl font-semibold tracking-tight text-center title-container"
|
||||
style="font-family: 'Source Han Serif CN', 'Noto Serif SC', 'STSong', 'SimSun', serif;"
|
||||
>
|
||||
<h4 class="scroll-m-20 text-xl font-semibold tracking-tight text-center text-muted-foreground title-container">
|
||||
Memoh
|
||||
<TextGenerateEffect
|
||||
words="您好!有什么能帮助您的?"
|
||||
/>
|
||||
</h4>
|
||||
</section>
|
||||
|
||||
@@ -54,15 +53,15 @@
|
||||
import {
|
||||
ScrollArea,
|
||||
Textarea,
|
||||
Button
|
||||
Button,
|
||||
TextGenerateEffect
|
||||
} from '@memoh/ui'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiSendOutline } from '@mdi/js'
|
||||
import ChatList from '@/components/ChatList/index.vue'
|
||||
import { provide, ref } from 'vue'
|
||||
import { useChatList } from '@/store/ChatList'
|
||||
import {storeToRefs} from 'pinia'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
const chatSay = ref('')
|
||||
const curInputSay = ref('')
|
||||
|
||||
|
||||
@@ -2,38 +2,38 @@
|
||||
<section class="w-screen h-screen flex *:m-auto bg-linear-to-t from-[#BFA4A0] to-[#7784AC] ">
|
||||
<section class="w-full max-w-sm flex flex-col gap-10 ">
|
||||
<section>
|
||||
<img
|
||||
src="../../../public/logo.png"
|
||||
width="100"
|
||||
alt="logo.png"
|
||||
class="m-auto"
|
||||
<h3
|
||||
class="scroll-m-20 text-3xl tracking-wide font-semibold text-white text-center"
|
||||
style="font-family: 'Source Han Serif CN', 'Noto Serif SC', 'STSong', 'SimSun', serif;"
|
||||
>
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight text-white text-center">
|
||||
Memoh
|
||||
欢迎使用
|
||||
</h3>
|
||||
</section>
|
||||
<form @submit="login">
|
||||
<form
|
||||
@submit="login"
|
||||
>
|
||||
<Card class="py-14">
|
||||
<CardContent class="flex flex-col [&_input]:py-5">
|
||||
<CardContent class="flex flex-col [&_input]:py-5 gap-4">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="username"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
<Label
|
||||
class="mb-2"
|
||||
for="username"
|
||||
>
|
||||
{{ $t("login.username") }}
|
||||
</FormLabel>
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
v-bind="componentField"
|
||||
id="username"
|
||||
type="text"
|
||||
:placeholder="$t('prompt.enter', { msg: $t(`login.username`).toLocaleLowerCase() })"
|
||||
v-bind="componentField"
|
||||
autocomplete="username"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
@@ -41,20 +41,21 @@
|
||||
name="password"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
<Label
|
||||
class="mb-2"
|
||||
for="password"
|
||||
>
|
||||
{{ $t('login.password') }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
id="password"
|
||||
type="password"
|
||||
:placeholder="$t('prompt.enter', { msg: $t(`login.password`).toLocaleLowerCase() })"
|
||||
autocomplete="password"
|
||||
autocomplete="new-password"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<div class="flex">
|
||||
@@ -66,6 +67,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter class="flex flex-col gap-4">
|
||||
<Button
|
||||
class="w-full"
|
||||
@@ -98,8 +100,7 @@ import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
Label,
|
||||
Spinner
|
||||
} from '@memoh/ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -108,15 +109,17 @@ import { useForm } from 'vee-validate'
|
||||
import * as z from 'zod'
|
||||
import request from '@/utils/request'
|
||||
import { useUserStore } from '@/store/User.ts'
|
||||
import { ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
const router = useRouter()
|
||||
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
}))
|
||||
const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
validationSchema: formSchema
|
||||
})
|
||||
|
||||
const { login: LoginHandle } = useUserStore()
|
||||
|
||||
@@ -52,10 +52,13 @@ watch(filterProvider, () => {
|
||||
key: ['provider']
|
||||
})
|
||||
}, {
|
||||
immediate: true
|
||||
immediate:true
|
||||
})
|
||||
|
||||
|
||||
const curProvider = ref<Partial<ProviderInfo> & { id: string }>()
|
||||
provide('curProvider', curProvider)
|
||||
|
||||
const selectProvider = (value: string) => computed(() => {
|
||||
return curProvider.value?.name === value
|
||||
})
|
||||
@@ -198,4 +201,4 @@ const openStatus = reactive({
|
||||
</SidebarProvider>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
@@ -128,12 +128,10 @@
|
||||
>
|
||||
<ItemContent>
|
||||
<ItemTitle>
|
||||
{{ modelData.name }}
|
||||
{{ modelData.name }}
|
||||
</ItemTitle>
|
||||
<ItemDescription class="gap-2 flex flex-wrap items-center mt-3 ">
|
||||
<Badge
|
||||
variant="outline"
|
||||
>
|
||||
<Badge variant="outline">
|
||||
{{ modelData.type }}
|
||||
</Badge>
|
||||
</ItemDescription>
|
||||
@@ -147,7 +145,7 @@
|
||||
as: value as string === 'empty' ? '' : value as string,
|
||||
model_id: modelData.model_id
|
||||
})
|
||||
|
||||
|
||||
}"
|
||||
>
|
||||
<SelectTrigger class="w-full">
|
||||
@@ -244,7 +242,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Switch, Separator, Spinner, Input, Button,
|
||||
Separator, Spinner, Input, Button,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
@@ -296,7 +294,7 @@ provide('openModel', toRef(openModel, 'state'))
|
||||
provide('openModelTitle', toRef(openModel, 'title'))
|
||||
provide('openModelState', toRef(openModel, 'curState'))
|
||||
|
||||
const deleteEnableAd = (value:ModelInfo) => {
|
||||
const deleteEnableAd = (value: ModelInfo) => {
|
||||
const copyModelData = { ...value }
|
||||
if ('enable_as' in copyModelData) {
|
||||
delete copyModelData['enable_as']
|
||||
|
||||
@@ -1,177 +1,186 @@
|
||||
<template>
|
||||
<section>
|
||||
<section class="max-w-187 m-auto">
|
||||
<Card>
|
||||
<form @submit="changeSetting">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-2xl font-semibold tracking-tight">
|
||||
Settings
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Model Settings
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent class="mt-4">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="defaultChatModel"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Chat Model
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('prompt.select',{msg:'Client Type'})" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="(modelItem, index) in modelType.chat"
|
||||
:key="modelItem.id"
|
||||
:value="(modelItem.model.apiKey + index)"
|
||||
>
|
||||
{{ modelItem.model.name }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="defaultEmbeddingModel"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Embedding Model
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('prompt.select',{msg:'Embedding Type'})" />
|
||||
</SelectTrigger>
|
||||
<SelectContent v-if="modelType.embedding.length > 0">
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="(modelItem, index) in modelType.embedding"
|
||||
:key="modelItem.id"
|
||||
:value="(modelItem.model.apiKey + index)"
|
||||
>
|
||||
{{ modelItem.model.name }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<form
|
||||
|
||||
@submit="changeSetting"
|
||||
>
|
||||
<section>
|
||||
<section class="flex flex-col">
|
||||
<h6 class=" mt-2 mb-2 align-middle">
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiRobotOutline"
|
||||
class="inline mr-2 align-[-5px]"
|
||||
/>模型设置
|
||||
</h6>
|
||||
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</formcontrol>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<Separator />
|
||||
</section>
|
||||
|
||||
<section class="flex flex-col [&_:has(label)]:py-2">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="defaultSummaryModel"
|
||||
name="chat_model_id"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
<!-- defaultSummaryModel -->
|
||||
Summary Model
|
||||
</FormLabel>
|
||||
<Label class="mb-2">
|
||||
Chat Model ID
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('prompt.select', { msg: 'Summary Type' })" />
|
||||
</SelectTrigger>
|
||||
<SelectContent v-if="modelType.embedding.length > 0">
|
||||
<SelectGroup>
|
||||
<SelectItem
|
||||
v-for="(modelItem, index) in modelType.summary"
|
||||
:key="modelItem.id"
|
||||
:value="(modelItem.model.apiKey + index)"
|
||||
>
|
||||
{{ modelItem.model.name }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input v-bind="componentField" />
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="embedding_model_id"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
Embedding Model ID
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input v-bind="componentField" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="memory_model_id"
|
||||
>
|
||||
<FormItem>
|
||||
<Label class="mb-2">
|
||||
Memory Model ID
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input v-bind="componentField" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="max_context_load_time"
|
||||
>
|
||||
<FormItem class="**:[input]:max-w-40!">
|
||||
<Label class="mb-2">
|
||||
Timeout
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input v-bind="componentField" />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</section>
|
||||
</section>
|
||||
<section class="mt-4">
|
||||
<section class="flex flex-col">
|
||||
<h6 class=" mt-2 mb-2 align-middle">
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiCog"
|
||||
class="inline mr-2 align-[-5px]"
|
||||
/>显示设置
|
||||
</h6>
|
||||
|
||||
<Separator />
|
||||
</section>
|
||||
<section class="flex flex-col [&_:has(label)]:py-2">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="language"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Language
|
||||
</FormLabel>
|
||||
<FormItem class="**:[button]:min-w-40! flex justify-between items-ceneter">
|
||||
<Label class="mb-2">
|
||||
语言
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue
|
||||
:placeholder="$t('prompt.select', { msg: 'Language' })"
|
||||
/>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="ch">
|
||||
<SelectItem
|
||||
value="zh"
|
||||
@click="$i18n.locale = 'zh'"
|
||||
>
|
||||
中文
|
||||
</SelectItem>
|
||||
<SelectItem value="en">
|
||||
<SelectItem
|
||||
value="en"
|
||||
@click="$i18n.locale = 'en'"
|
||||
>
|
||||
English
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="maxContextLoadTime"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
<!-- defaultSummaryModel -->
|
||||
Timeout
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
:placeholder="$t('prompt.enter',{msg:'Timeout'})"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</CardContent>
|
||||
<CardFooter class="flex">
|
||||
<Button
|
||||
class="ml-auto"
|
||||
type="submit"
|
||||
:disabled="diabeld"
|
||||
>
|
||||
Change
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
<Separator />
|
||||
<section class="flex justify-between items-baseline">
|
||||
<Label class="mb-4">
|
||||
主题
|
||||
</Label>
|
||||
|
||||
<Switch
|
||||
:model-value="curDark()"
|
||||
@update:model-value="() => {
|
||||
toggleMode()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
<section class="mt-4 flex gap-3">
|
||||
<Popover>
|
||||
<template #default="{ close }">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
class="ml-auto"
|
||||
variant="outline"
|
||||
>
|
||||
{{ $t("login.exit") }}
|
||||
</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"
|
||||
@click="() => { close() }"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button @click="() => { exit(); close() }">
|
||||
确定
|
||||
</Button>
|
||||
</section>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<Button
|
||||
|
||||
:disabled="settingLoading"
|
||||
type="submit"
|
||||
>
|
||||
<Spinner v-if="settingLoading" />
|
||||
更改
|
||||
</Button>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
@@ -179,116 +188,121 @@
|
||||
<script setup lang="ts">
|
||||
import { useMutation, useQuery, useQueryCache } from '@pinia/colada'
|
||||
import request from '@/utils/request'
|
||||
import { watch, reactive, computed } from 'vue'
|
||||
import { watch } from 'vue'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import z from 'zod'
|
||||
import { useForm, useFormValues } from 'vee-validate'
|
||||
import { useForm } from 'vee-validate'
|
||||
import {
|
||||
Input,
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormItem,
|
||||
FormControl,
|
||||
Button,
|
||||
FormMessage,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectValue,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
CardFooter
|
||||
Label,
|
||||
Separator,
|
||||
Switch,
|
||||
Spinner,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@memoh/ui'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiRobotOutline, mdiCog } from '@mdi/js'
|
||||
import { toast } from 'vue-sonner'
|
||||
import i18n from '@/i18n'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '../../store/User'
|
||||
const router=useRouter()
|
||||
|
||||
type ModelList = {
|
||||
id: ModelTable['id'],
|
||||
model: Omit<ModelTable, 'id' | 'defaultChatModel' | 'defaultEmbeddingModel' | 'defaultSummaryModel'>
|
||||
};
|
||||
const { exitLogin } = useUserStore()
|
||||
const exit = () => {
|
||||
exitLogin()
|
||||
router.replace({ name: 'Login' })
|
||||
}
|
||||
|
||||
const mode = useColorMode()
|
||||
const modeToggleMap: Record<'dark' | 'light', 'dark' | 'light'> = {
|
||||
dark: 'light',
|
||||
light: 'dark'
|
||||
}
|
||||
const toggleMode = () => {
|
||||
if (mode.value !== 'auto') {
|
||||
mode.value = modeToggleMap[mode.value]
|
||||
}
|
||||
}
|
||||
|
||||
const curDark = () => {
|
||||
return mode.value==='dark'?true:false
|
||||
}
|
||||
|
||||
const modelType = reactive<{
|
||||
chat: ModelList[],
|
||||
embedding: ModelList[],
|
||||
summary: ModelList[]
|
||||
}>({
|
||||
chat: [],
|
||||
embedding: [],
|
||||
summary: []
|
||||
})
|
||||
|
||||
const { data: settingData } = useQuery({
|
||||
key: ['Setting'],
|
||||
query: async () => {
|
||||
const modelData = await request({
|
||||
url: '/model/',
|
||||
method: 'get'
|
||||
})
|
||||
for (const modelItems of modelData.data.items) {
|
||||
let type = modelItems.model.type as keyof typeof modelType
|
||||
modelType[type].push(modelItems)
|
||||
query: async () => request({
|
||||
url: '/settings',
|
||||
method: 'get'
|
||||
}).then(fetchSetting => {
|
||||
// if(f)
|
||||
|
||||
if (fetchSetting?.data?.language&&!i18n.global.availableLocales.includes(fetchSetting?.data?.language)) {
|
||||
|
||||
fetchSetting.data.language='zh'
|
||||
}
|
||||
return await request({
|
||||
url: '/settings/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
return fetchSetting?.data
|
||||
})
|
||||
})
|
||||
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
defaultChatModel: z.any(),
|
||||
defaultEmbeddingModel: z.any(),
|
||||
defaultSummaryModel: z.any(),
|
||||
maxContextLoadTime: z.coerce.number().min(1500),
|
||||
language: z.literal(['ch', 'en'])
|
||||
chat_model_id: z.coerce.string(),
|
||||
embedding_model_id: z.coerce.string(),
|
||||
language: z.coerce.string(),
|
||||
max_context_load_time: z.coerce.number().min(1000),
|
||||
memory_model_id: z.coerce.string()
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: formSchema
|
||||
})
|
||||
|
||||
const currentSetting = useFormValues()
|
||||
|
||||
const diabeld = computed(() => {
|
||||
return Object.keys(currentSetting.value).every((property) => {
|
||||
const curKey = currentSetting.value[property]
|
||||
const cacheKey = settingData.value?.data?.data?.[property]
|
||||
if (curKey === cacheKey || Number(curKey) === Number(cacheKey)) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
watch(settingData, () => {
|
||||
form.setValues({
|
||||
...(settingData.value?.data.data ?? {})
|
||||
})
|
||||
}, {
|
||||
immediate: true
|
||||
watch(settingData, () => {
|
||||
form.setValues(settingData.value)
|
||||
form.values=settingData.value
|
||||
})
|
||||
|
||||
|
||||
const cacheQuery=useQueryCache()
|
||||
const { mutate: fetchSetting } = useMutation({
|
||||
mutation: (data:typeof currentSetting.value) => request({
|
||||
url: '/settings/',
|
||||
data
|
||||
const cacheQuery = useQueryCache()
|
||||
const { mutate: fetchSetting,isLoading:settingLoading,status } = useMutation({
|
||||
mutation: (data:typeof form.values) => request({
|
||||
url: '/settings',
|
||||
data,
|
||||
method:'POST'
|
||||
}),
|
||||
onSettled: () => {
|
||||
cacheQuery.invalidateQueries({
|
||||
key:['Setting']
|
||||
key: ['Setting']
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
watch(status, () => {
|
||||
if (status.value === 'error') {
|
||||
toast.error('保存失败')
|
||||
}
|
||||
})
|
||||
const changeSetting = form.handleSubmit(async (value) => {
|
||||
|
||||
try {
|
||||
await fetchSetting(value)
|
||||
|
||||
try {
|
||||
await fetchSetting(value)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
+45
-44
@@ -21,71 +21,72 @@
|
||||
|
||||
:root {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--background: oklch(98.7% 0.3% 290deg);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.488 0.243 264.376);
|
||||
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.488 0.243 264.376);
|
||||
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
|
||||
Reference in New Issue
Block a user