mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
Merge branch 'feat/chat' into 'main'
feat: Chat See merge request acbox/memohome!1
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
||||
# PostgreSQL database connection URL
|
||||
# Format: postgresql://username:password@host:port/database
|
||||
# Example: postgresql://postgres:password@localhost:5432/memohome
|
||||
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
||||
DATABASE_URL=postgresql://postgres:1234@localhost:5432/database_name
|
||||
|
||||
|
||||
# ==================================
|
||||
|
||||
Vendored
+7
-3
@@ -1,9 +1,14 @@
|
||||
{
|
||||
"editor.insertSpaces": true,
|
||||
"editor.detectIndentation": false,
|
||||
|
||||
"editor.formatOnSave": false,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports": "never"
|
||||
},
|
||||
|
||||
"editor.tabSize": 2,
|
||||
|
||||
|
||||
"[typescript]": {
|
||||
"editor.tabSize": 2,
|
||||
"editor.insertSpaces": true
|
||||
@@ -33,4 +38,3 @@
|
||||
"editor.insertSpaces": true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -38,5 +38,5 @@
|
||||
"@algolia/client-search"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { db } from '@memoh/db'
|
||||
import { users, settings } from '@memoh/db/schema'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
/**
|
||||
* 验证用户凭据
|
||||
* 优先检查是否为 ROOT 用户,否则查询数据库
|
||||
*/
|
||||
export const validateUser = async (username: string, password: string) => {
|
||||
// 检查是否为 ROOT 用户
|
||||
const rootUser = process.env.ROOT_USER
|
||||
const rootPassword = process.env.ROOT_USER_PASSWORD
|
||||
|
||||
let userId: string | null = null
|
||||
|
||||
if (rootUser && rootPassword && username === rootUser) {
|
||||
if (password === rootPassword) {
|
||||
// 检查 root 用户是否存在于数据库中
|
||||
const [existingUser] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, rootUser))
|
||||
|
||||
userId = existingUser?.id
|
||||
|
||||
if (!existingUser) {
|
||||
// 为 root 用户创建数据库记录
|
||||
// 使用占位符密码哈希,因为实际密码在环境变量中
|
||||
|
||||
const [newUser] = await db
|
||||
.insert(users)
|
||||
.values({
|
||||
username: rootUser,
|
||||
passwordHash: 'ENV_BASED_AUTH', // 占位符,实际使用环境变量验证
|
||||
role: 'admin',
|
||||
displayName: 'Root User',
|
||||
email: null,
|
||||
avatarUrl: null,
|
||||
isActive: true,
|
||||
})
|
||||
.onConflictDoNothing() // 避免并发创建导致的冲突
|
||||
.returning({
|
||||
id: users.id,
|
||||
})
|
||||
|
||||
userId = newUser.id
|
||||
}
|
||||
|
||||
// 检查 root 用户的 settings 是否存在,不存在则创建
|
||||
const [existingSettings] = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(eq(settings.userId, userId))
|
||||
|
||||
if (!existingSettings) {
|
||||
// 为 root 用户创建默认 settings
|
||||
await db
|
||||
.insert(settings)
|
||||
.values({
|
||||
userId: userId,
|
||||
defaultChatModel: null,
|
||||
defaultEmbeddingModel: null,
|
||||
defaultSummaryModel: null,
|
||||
maxContextLoadTime: 60,
|
||||
language: 'Same as user input',
|
||||
})
|
||||
.onConflictDoNothing() // 避免并发创建导致的冲突
|
||||
}
|
||||
|
||||
// 返回 ROOT 用户信息
|
||||
return {
|
||||
id: userId,
|
||||
username: rootUser,
|
||||
role: 'admin' as const,
|
||||
displayName: 'Root User',
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 查询数据库中的用户(使用 username 而不是 id)
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.username, username))
|
||||
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 验证密码 (这里使用简单的 Bun.password.verify)
|
||||
const isValid = await Bun.password.verify(password, user.passwordHash)
|
||||
|
||||
if (!isValid) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 检查账户是否激活
|
||||
if (!user.isActive) {
|
||||
return null
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
await db
|
||||
.update(users)
|
||||
.set({
|
||||
lastLoginAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, user.id))
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
displayName: user.displayName || user.username,
|
||||
email: user.email,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import Elysia from 'elysia'
|
||||
import { adminMiddleware } from '../../middlewares'
|
||||
import {
|
||||
GetUserByIdModel,
|
||||
CreateUserModel,
|
||||
UpdateUserModel,
|
||||
DeleteUserModel,
|
||||
UpdatePasswordModel,
|
||||
} from './model'
|
||||
import {
|
||||
getUsers,
|
||||
getUserById,
|
||||
createUser,
|
||||
updateUser,
|
||||
deleteUser,
|
||||
updateUserPassword,
|
||||
} from './service'
|
||||
|
||||
export const userModule = new Elysia({
|
||||
prefix: '/user',
|
||||
})
|
||||
// 使用管理员中间件保护所有路由
|
||||
.use(adminMiddleware)
|
||||
// Get all users
|
||||
.get('/', async ({ query }) => {
|
||||
try {
|
||||
const page = parseInt(query.page as string) || 1
|
||||
const limit = parseInt(query.limit as string) || 10
|
||||
const sortBy = query.sortBy as string || 'createdAt'
|
||||
const sortOrder = (query.sortOrder as string) || 'desc'
|
||||
|
||||
const result = await getUsers({
|
||||
page,
|
||||
limit,
|
||||
sortBy,
|
||||
sortOrder: sortOrder as 'asc' | 'desc',
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch users',
|
||||
}
|
||||
}
|
||||
})
|
||||
// Get user by ID
|
||||
.get('/:id', async ({ params, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const user = await getUserById(id)
|
||||
|
||||
if (!user) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: user,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch user',
|
||||
}
|
||||
}
|
||||
}, GetUserByIdModel)
|
||||
// Create new user
|
||||
.post('/', async ({ body, set }) => {
|
||||
try {
|
||||
const newUser = await createUser(body)
|
||||
set.status = 201
|
||||
return {
|
||||
success: true,
|
||||
data: newUser,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && (
|
||||
error.message.includes('already exists')
|
||||
)) {
|
||||
set.status = 409
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create user',
|
||||
}
|
||||
}
|
||||
}, CreateUserModel)
|
||||
// Update user
|
||||
.put('/:id', async ({ params, body, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const updatedUser = await updateUser(id, body)
|
||||
|
||||
if (!updatedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedUser,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('already exists')) {
|
||||
set.status = 409
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update user',
|
||||
}
|
||||
}
|
||||
}, UpdateUserModel)
|
||||
// Delete user
|
||||
.delete('/:id', async ({ params, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const deletedUser = await deleteUser(id)
|
||||
|
||||
if (!deletedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: deletedUser,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete user',
|
||||
}
|
||||
}
|
||||
}, DeleteUserModel)
|
||||
// Update user password
|
||||
.patch('/:id/password', async ({ params, body, set }) => {
|
||||
try {
|
||||
const { id } = params
|
||||
const updatedUser = await updateUserPassword(id, body.password)
|
||||
|
||||
if (!updatedUser) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'User not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedUser,
|
||||
message: 'Password updated successfully',
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update password',
|
||||
}
|
||||
}
|
||||
}, UpdatePasswordModel)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface robot{
|
||||
description: string
|
||||
time: Date,
|
||||
id: string | number,
|
||||
type: string,
|
||||
action: 'robot',
|
||||
state:'thinking'|'generate'|'complete'
|
||||
}
|
||||
|
||||
export interface user{
|
||||
description: string,
|
||||
time: Date,
|
||||
id: number | string,
|
||||
action:'user'
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './model'
|
||||
export * from './schedule'
|
||||
export * from './platform'
|
||||
export * from './mcp'
|
||||
export * from './mcp'
|
||||
export * from './chatInfo'
|
||||
|
||||
@@ -28,3 +28,21 @@ export type MCPConnection =
|
||||
| StdioMCPConnection
|
||||
| HTTPMCPConnection
|
||||
| SSEMCPConnection
|
||||
|
||||
|
||||
export interface MCPListItem{
|
||||
id: string;
|
||||
type: string;
|
||||
name: string;
|
||||
config: {
|
||||
cwd: string;
|
||||
env: Record<string, string>;
|
||||
args: string[];
|
||||
type: string;
|
||||
command: string;
|
||||
};
|
||||
active: boolean;
|
||||
user: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -63,3 +63,18 @@ export interface ChatModel extends BaseModel {
|
||||
}
|
||||
|
||||
export type Model = EmbeddingModel | ChatModel
|
||||
|
||||
|
||||
// 表格当中model的类型
|
||||
export interface ModelTable {
|
||||
apiKey: string,
|
||||
baseUrl: string,
|
||||
clientType: 'OpenAI' | 'Anthropic' | 'Google',
|
||||
modelId: string,
|
||||
name: string,
|
||||
type: 'chat' | 'embedding',
|
||||
id: string,
|
||||
defaultChatModel: boolean,
|
||||
defaultEmbeddingModel: boolean,
|
||||
defaultSummaryModel: boolean
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "new-york",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/style.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "#/components",
|
||||
"composables": "#/composables",
|
||||
"utils": "#/lib/utils",
|
||||
"ui": "#/components",
|
||||
"lib": "#/lib"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
".": "./src/index.ts",
|
||||
"./style.css": "./src/style.css"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
@@ -19,6 +20,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -26,7 +29,9 @@
|
||||
"reka-ui": "^2.7.0",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"vue-sonner": "^2.0.9"
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.26"
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const rootDir = path.resolve(import.meta.dirname, '../src')
|
||||
const readDir = path.resolve(rootDir, './components')
|
||||
const outputDir = path.resolve(rootDir, './index.ts')
|
||||
|
||||
async function readDirName(){
|
||||
const pathList:Awaited<string[]> = await new Promise((resolve, reject) => {
|
||||
fs.readdir(readDir, (err, data) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
resolve(data)
|
||||
})
|
||||
})
|
||||
return pathList
|
||||
}
|
||||
|
||||
async function writeExportFile(pathList: string[]) {
|
||||
const pathListStr = pathList.map(fileName => {
|
||||
return `export * from './components/${fileName}/index'`
|
||||
})
|
||||
await new Promise((resolve, reject) => {
|
||||
fs.writeFile(outputDir, pathListStr.join('\r\n'), (err) => {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
resolve(undefined)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
try {
|
||||
const list = await readDirName()
|
||||
writeExportFile(list)
|
||||
} catch(error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
generate()
|
||||
@@ -1,18 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { PrimitiveProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import type { BadgeVariants } from '.'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import type { PrimitiveProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { BadgeVariants } from "."
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Primitive } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { badgeVariants } from '.'
|
||||
import { badgeVariants } from "."
|
||||
|
||||
const props = defineProps<PrimitiveProps & {
|
||||
variant?: BadgeVariants['variant']
|
||||
class?: HTMLAttributes['class']
|
||||
variant?: BadgeVariants["variant"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
aria-label="breadcrumb"
|
||||
data-slot="breadcrumb"
|
||||
:class="props.class"
|
||||
>
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { MoreHorizontal } from "lucide-vue-next"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('flex size-9 items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal class="size-4" />
|
||||
</slot>
|
||||
<span class="sr-only">More</span>
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
:class="cn('inline-flex items-center gap-1.5', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PrimitiveProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { Primitive } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<PrimitiveProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
as: "a",
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="breadcrumb-link"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn('hover:text-foreground transition-colors', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
:class="cn('text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</ol>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
:class="cn('text-foreground font-normal', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { ChevronRight } from "lucide-vue-next"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
:class="cn('[&>svg]:size-3.5', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<ChevronRight />
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as Breadcrumb } from "./Breadcrumb.vue"
|
||||
export { default as BreadcrumbEllipsis } from "./BreadcrumbEllipsis.vue"
|
||||
export { default as BreadcrumbItem } from "./BreadcrumbItem.vue"
|
||||
export { default as BreadcrumbLink } from "./BreadcrumbLink.vue"
|
||||
export { default as BreadcrumbList } from "./BreadcrumbList.vue"
|
||||
export { default as BreadcrumbPage } from "./BreadcrumbPage.vue"
|
||||
export { default as BreadcrumbSeparator } from "./BreadcrumbSeparator.vue"
|
||||
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { CollapsibleRootEmits, CollapsibleRootProps } from "reka-ui"
|
||||
import { CollapsibleRoot, useForwardPropsEmits } from "reka-ui"
|
||||
|
||||
const props = defineProps<CollapsibleRootProps>()
|
||||
const emits = defineEmits<CollapsibleRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="collapsible"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</CollapsibleRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { CollapsibleContentProps } from "reka-ui"
|
||||
import { CollapsibleContent } from "reka-ui"
|
||||
|
||||
const props = defineProps<CollapsibleContentProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleContent
|
||||
data-slot="collapsible-content"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</CollapsibleContent>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { CollapsibleTriggerProps } from "reka-ui"
|
||||
import { CollapsibleTrigger } from "reka-ui"
|
||||
|
||||
const props = defineProps<CollapsibleTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CollapsibleTrigger
|
||||
data-slot="collapsible-trigger"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</CollapsibleTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as Collapsible } from './Collapsible.vue'
|
||||
export { default as CollapsibleContent } from './CollapsibleContent.vue'
|
||||
export { default as CollapsibleTrigger } from './CollapsibleTrigger.vue'
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts" setup>
|
||||
import { Slot } from "reka-ui"
|
||||
import { useFormField } from "./useFormField"
|
||||
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Slot
|
||||
:id="formItemId"
|
||||
data-slot="form-control"
|
||||
:aria-describedby="!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`"
|
||||
:aria-invalid="!!error"
|
||||
>
|
||||
<slot />
|
||||
</Slot>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { useFormField } from "./useFormField"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const { formDescriptionId } = useFormField()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p
|
||||
:id="formDescriptionId"
|
||||
data-slot="form-description"
|
||||
:class="cn('text-muted-foreground text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</p>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { useId } from "reka-ui"
|
||||
import { provide } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const id = useId()
|
||||
provide(FORM_ITEM_INJECTION_KEY, id)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
data-slot="form-item"
|
||||
:class="cn('grid gap-2', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LabelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { Label } from '#/components/label'
|
||||
import { useFormField } from "./useFormField"
|
||||
|
||||
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const { error, formItemId } = useFormField()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Label
|
||||
data-slot="form-label"
|
||||
:data-error="!!error"
|
||||
:class="cn(
|
||||
'data-[error=true]:text-destructive',
|
||||
props.class,
|
||||
)"
|
||||
:for="formItemId"
|
||||
>
|
||||
<slot />
|
||||
</Label>
|
||||
</template>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { ErrorMessage } from "vee-validate"
|
||||
import { toValue } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { useFormField } from "./useFormField"
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
|
||||
const { name, formMessageId } = useFormField()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ErrorMessage
|
||||
:id="formMessageId"
|
||||
data-slot="form-message"
|
||||
as="p"
|
||||
:name="toValue(name)"
|
||||
:class="cn('text-destructive text-sm', props.class)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
export { default as FormControl } from "./FormControl.vue"
|
||||
export { default as FormDescription } from "./FormDescription.vue"
|
||||
export { default as FormItem } from "./FormItem.vue"
|
||||
export { default as FormLabel } from "./FormLabel.vue"
|
||||
export { default as FormMessage } from "./FormMessage.vue"
|
||||
export { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||
export { Form, Field as FormField, FieldArray as FormFieldArray } from "vee-validate"
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { InjectionKey } from "vue"
|
||||
|
||||
export const FORM_ITEM_INJECTION_KEY
|
||||
= Symbol() as InjectionKey<string>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FieldContextKey } from "vee-validate"
|
||||
import { computed, inject } from "vue"
|
||||
import { FORM_ITEM_INJECTION_KEY } from "./injectionKeys"
|
||||
|
||||
export function useFormField() {
|
||||
const fieldContext = inject(FieldContextKey)
|
||||
const fieldItemContext = inject(FORM_ITEM_INJECTION_KEY)
|
||||
|
||||
if (!fieldContext)
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
|
||||
const { name, errorMessage: error, meta } = fieldContext
|
||||
const id = fieldItemContext
|
||||
|
||||
const fieldState = {
|
||||
valid: computed(() => meta.valid),
|
||||
isDirty: computed(() => meta.dirty),
|
||||
isTouched: computed(() => meta.touched),
|
||||
error,
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
...fieldState,
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { LabelProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { Label } from 'reka-ui'
|
||||
import type { LabelProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { Label } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<LabelProps & { class?: HTMLAttributes['class'] }>()
|
||||
const props = defineProps<LabelProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -16,9 +16,7 @@ const delegatedProps = reactiveOmit(props, 'class')
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn(
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none text-gray-900 dark:text-gray-100',
|
||||
'group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50',
|
||||
'peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
|
||||
@@ -1 +1 @@
|
||||
export { default as Label } from './Label.vue'
|
||||
export { default as Label } from "./Label.vue"
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import type { AcceptableValue } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit, useVModel } from "@vueuse/core"
|
||||
import { ChevronDownIcon } from "lucide-vue-next"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": AcceptableValue
|
||||
}>()
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emit, {
|
||||
passive: true,
|
||||
defaultValue: "",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="group/native-select relative w-fit has-[select:disabled]:opacity-50"
|
||||
data-slot="native-select-wrapper"
|
||||
>
|
||||
<select
|
||||
v-bind="{ ...$attrs, ...delegatedProps }"
|
||||
v-model="modelValue"
|
||||
data-slot="native-select"
|
||||
:class="cn(
|
||||
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</select>
|
||||
<ChevronDownIcon
|
||||
class="text-muted-foreground pointer-events-none absolute top-1/2 right-3.5 size-4 -translate-y-1/2 opacity-50 select-none"
|
||||
aria-hidden="true"
|
||||
data-slot="native-select-icon"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!-- @fallthroughAttributes true -->
|
||||
<!-- @strictTemplates true -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<optgroup data-slot="native-select-optgroup" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||
<slot />
|
||||
</optgroup>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<!-- @fallthroughAttributes true -->
|
||||
<!-- @strictTemplates true -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<option data-slot="native-select-option" :class="cn('bg-popover text-popover-foreground', props.class)">
|
||||
<slot />
|
||||
</option>
|
||||
</template>
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as NativeSelect } from "./NativeSelect.vue"
|
||||
export { default as NativeSelectOptGroup } from "./NativeSelectOptGroup.vue"
|
||||
export { default as NativeSelectOption } from "./NativeSelectOption.vue"
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationRootEmits, PaginationRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { PaginationRoot, useForwardPropsEmits } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationRootProps & {
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
const emits = defineEmits<PaginationRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="pagination"
|
||||
v-bind="forwarded"
|
||||
:class="cn('mx-auto flex w-full justify-center', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PaginationRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationListProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { PaginationList } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationListProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationList
|
||||
v-slot="slotProps"
|
||||
data-slot="pagination-content"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('flex flex-row items-center gap-1', props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</PaginationList>
|
||||
</template>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationEllipsisProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { MoreHorizontal } from "lucide-vue-next"
|
||||
import { PaginationEllipsis } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<PaginationEllipsisProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationEllipsis
|
||||
data-slot="pagination-ellipsis"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('flex size-9 items-center justify-center', props.class)"
|
||||
>
|
||||
<slot>
|
||||
<MoreHorizontal class="size-4" />
|
||||
<span class="sr-only">More pages</span>
|
||||
</slot>
|
||||
</PaginationEllipsis>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationFirstProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ButtonVariants } from '#/components/button'
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ChevronLeftIcon } from "lucide-vue-next"
|
||||
import { PaginationFirst, useForwardProps } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { buttonVariants } from '#/components/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationFirstProps & {
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>(), {
|
||||
size: "default",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationFirst
|
||||
data-slot="pagination-first"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<ChevronLeftIcon />
|
||||
<span class="hidden sm:block">First</span>
|
||||
</slot>
|
||||
</PaginationFirst>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationListItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ButtonVariants } from '#/components/button'
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { PaginationListItem } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { buttonVariants } from '#/components/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationListItemProps & {
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
isActive?: boolean
|
||||
}>(), {
|
||||
size: "icon",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size", "isActive")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationListItem
|
||||
data-slot="pagination-item"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn(
|
||||
buttonVariants({
|
||||
variant: isActive ? 'outline' : 'ghost',
|
||||
size,
|
||||
}),
|
||||
props.class)"
|
||||
>
|
||||
<slot />
|
||||
</PaginationListItem>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationLastProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ButtonVariants } from '#/components/button'
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ChevronRightIcon } from "lucide-vue-next"
|
||||
import { PaginationLast, useForwardProps } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { buttonVariants } from '#/components/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationLastProps & {
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>(), {
|
||||
size: "default",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationLast
|
||||
data-slot="pagination-last"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<span class="hidden sm:block">Last</span>
|
||||
<ChevronRightIcon />
|
||||
</slot>
|
||||
</PaginationLast>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationNextProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ButtonVariants } from '#/components/button'
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ChevronRightIcon } from "lucide-vue-next"
|
||||
import { PaginationNext, useForwardProps } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { buttonVariants } from '#/components/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationNextProps & {
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>(), {
|
||||
size: "default",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationNext
|
||||
data-slot="pagination-next"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<span class="hidden sm:block">Next</span>
|
||||
<ChevronRightIcon />
|
||||
</slot>
|
||||
</PaginationNext>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { PaginationPrevProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import type { ButtonVariants } from '#/components/button'
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ChevronLeftIcon } from "lucide-vue-next"
|
||||
import { PaginationPrev, useForwardProps } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
import { buttonVariants } from '#/components/button'
|
||||
|
||||
const props = withDefaults(defineProps<PaginationPrevProps & {
|
||||
size?: ButtonVariants["size"]
|
||||
class?: HTMLAttributes["class"]
|
||||
}>(), {
|
||||
size: "default",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||
const forwarded = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PaginationPrev
|
||||
data-slot="pagination-previous"
|
||||
:class="cn(buttonVariants({ variant: 'ghost', size }), 'gap-1 px-2.5 sm:pr-2.5', props.class)"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot>
|
||||
<ChevronLeftIcon />
|
||||
<span class="hidden sm:block">Previous</span>
|
||||
</slot>
|
||||
</PaginationPrev>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as Pagination } from "./Pagination.vue"
|
||||
export { default as PaginationContent } from "./PaginationContent.vue"
|
||||
export { default as PaginationEllipsis } from "./PaginationEllipsis.vue"
|
||||
export { default as PaginationFirst } from "./PaginationFirst.vue"
|
||||
export { default as PaginationItem } from "./PaginationItem.vue"
|
||||
export { default as PaginationLast } from "./PaginationLast.vue"
|
||||
export { default as PaginationNext } from "./PaginationNext.vue"
|
||||
export { default as PaginationPrevious } from "./PaginationPrevious.vue"
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollAreaRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import {
|
||||
ScrollAreaCorner,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaViewport,
|
||||
} from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
import ScrollBar from "./ScrollBar.vue"
|
||||
|
||||
const props = defineProps<ScrollAreaRootProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaRoot
|
||||
data-slot="scroll-area"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('relative', props.class)"
|
||||
>
|
||||
<ScrollAreaViewport
|
||||
data-slot="scroll-area-viewport"
|
||||
class="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
<slot />
|
||||
</ScrollAreaViewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaCorner />
|
||||
</ScrollAreaRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import type { ScrollAreaScrollbarProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { ScrollAreaScrollbar, ScrollAreaThumb } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = withDefaults(defineProps<ScrollAreaScrollbarProps & { class?: HTMLAttributes["class"] }>(), {
|
||||
orientation: "vertical",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn('flex touch-none p-px transition-colors select-none',
|
||||
orientation === 'vertical'
|
||||
&& 'h-full w-2.5 border-l border-l-transparent',
|
||||
orientation === 'horizontal'
|
||||
&& 'h-2.5 flex-col border-t border-t-transparent',
|
||||
props.class)"
|
||||
>
|
||||
<ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
class="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaScrollbar>
|
||||
</template>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as ScrollArea } from './ScrollArea.vue'
|
||||
export { default as ScrollBar } from './ScrollBar.vue'
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="table-container" class="relative w-full overflow-auto">
|
||||
<table data-slot="table" :class="cn('w-full caption-bottom text-sm', props.class)">
|
||||
<slot />
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
:class="cn('[&_tr:last-child]:border-0', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</tbody>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
:class="cn('text-muted-foreground mt-4 text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</caption>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
:class="
|
||||
cn(
|
||||
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</td>
|
||||
</template>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { cn } from '#/lib/utils'
|
||||
import TableCell from "./TableCell.vue"
|
||||
import TableRow from "./TableRow.vue"
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
colspan?: number
|
||||
}>(), {
|
||||
colspan: 1,
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
:class="
|
||||
cn(
|
||||
'p-4 whitespace-nowrap align-middle text-sm text-foreground',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<div class="flex items-center justify-center py-10">
|
||||
<slot />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
:class="cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</tfoot>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<th
|
||||
data-slot="table-head"
|
||||
:class="cn('text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</th>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
:class="cn('[&_tr]:border-b', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</thead>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
:class="cn('hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</tr>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as Table } from "./Table.vue"
|
||||
export { default as TableBody } from "./TableBody.vue"
|
||||
export { default as TableCaption } from "./TableCaption.vue"
|
||||
export { default as TableCell } from "./TableCell.vue"
|
||||
export { default as TableEmpty } from "./TableEmpty.vue"
|
||||
export { default as TableFooter } from "./TableFooter.vue"
|
||||
export { default as TableHead } from "./TableHead.vue"
|
||||
export { default as TableHeader } from "./TableHeader.vue"
|
||||
export { default as TableRow } from "./TableRow.vue"
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { Updater } from "@tanstack/vue-table"
|
||||
|
||||
import type { Ref } from "vue"
|
||||
import { isFunction } from "@tanstack/vue-table"
|
||||
|
||||
export function valueUpdater<T>(updaterOrValue: Updater<T>, ref: Ref<T>) {
|
||||
ref.value = isFunction(updaterOrValue)
|
||||
? updaterOrValue(ref.value)
|
||||
: updaterOrValue
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { TagsInputRootEmits, TagsInputRootProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TagsInputRoot, useForwardPropsEmits } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputRootProps & { class?: HTMLAttributes["class"] }>()
|
||||
const emits = defineEmits<TagsInputRootEmits>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputRoot
|
||||
v-slot="slotProps" v-bind="forwarded" :class="cn(
|
||||
'flex flex-wrap gap-2 items-center rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none',
|
||||
'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
props.class)"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
</TagsInputRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { TagsInputInputProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TagsInputInput, useForwardProps } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputInputProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputInput v-bind="forwardedProps" :class="cn('text-sm min-h-5 focus:outline-none flex-1 bg-transparent px-1', props.class)" />
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import type { TagsInputItemProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TagsInputItem, useForwardProps } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputItemProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputItem v-bind="forwardedProps" :class="cn('flex h-5 items-center rounded-md bg-secondary data-[state=active]:ring-ring data-[state=active]:ring-2 data-[state=active]:ring-offset-2 ring-offset-background', props.class)">
|
||||
<slot />
|
||||
</TagsInputItem>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import type { TagsInputItemDeleteProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { X } from "lucide-vue-next"
|
||||
import { TagsInputItemDelete, useForwardProps } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputItemDeleteProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputItemDelete v-bind="forwardedProps" :class="cn('flex rounded bg-transparent mr-1', props.class)">
|
||||
<slot>
|
||||
<X class="w-4 h-4" />
|
||||
</slot>
|
||||
</TagsInputItemDelete>
|
||||
</template>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import type { TagsInputItemTextProps } from "reka-ui"
|
||||
import type { HTMLAttributes } from "vue"
|
||||
import { reactiveOmit } from "@vueuse/core"
|
||||
import { TagsInputItemText, useForwardProps } from "reka-ui"
|
||||
import { cn } from '#/lib/utils'
|
||||
|
||||
const props = defineProps<TagsInputItemTextProps & { class?: HTMLAttributes["class"] }>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TagsInputItemText v-bind="forwardedProps" :class="cn('py-0.5 px-2 text-sm rounded bg-transparent', props.class)" />
|
||||
</template>
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as TagsInput } from "./TagsInput.vue"
|
||||
export { default as TagsInputInput } from "./TagsInputInput.vue"
|
||||
export { default as TagsInputItem } from "./TagsInputItem.vue"
|
||||
export { default as TagsInputItemDelete } from "./TagsInputItemDelete.vue"
|
||||
export { default as TagsInputItemText } from "./TagsInputItemText.vue"
|
||||
+36
-30
@@ -1,30 +1,36 @@
|
||||
// Generated by scripts/gen-entry.ts. Do not edit manually.
|
||||
|
||||
export * from './components/alert/index'
|
||||
export * from './components/avatar/index'
|
||||
export * from './components/badge/index'
|
||||
export * from './components/button/index'
|
||||
export * from './components/button-group/index'
|
||||
export * from './components/card/index'
|
||||
export * from './components/checkbox/index'
|
||||
export * from './components/combobox/index'
|
||||
export * from './components/context-menu/index'
|
||||
export * from './components/dialog/index'
|
||||
export * from './components/dropdown-menu/index'
|
||||
export * from './components/input/index'
|
||||
export * from './components/input-group/index'
|
||||
export * from './components/kbd/index'
|
||||
export * from './components/label/index'
|
||||
export * from './components/radio-group/index'
|
||||
export * from './components/select/index'
|
||||
export * from './components/separator/index'
|
||||
export * from './components/sheet/index'
|
||||
export * from './components/sidebar/index'
|
||||
export * from './components/skeleton/index'
|
||||
export * from './components/slider/index'
|
||||
export * from './components/sonner/index'
|
||||
export * from './components/spinner/index'
|
||||
export * from './components/switch/index'
|
||||
export * from './components/tabs/index'
|
||||
export * from './components/textarea/index'
|
||||
export * from './components/tooltip/index'
|
||||
export * from './components/alert/index'
|
||||
export * from './components/avatar/index'
|
||||
export * from './components/badge/index'
|
||||
export * from './components/breadcrumb/index'
|
||||
export * from './components/button/index'
|
||||
export * from './components/button-group/index'
|
||||
export * from './components/card/index'
|
||||
export * from './components/checkbox/index'
|
||||
export * from './components/collapsible/index'
|
||||
export * from './components/combobox/index'
|
||||
export * from './components/context-menu/index'
|
||||
export * from './components/dialog/index'
|
||||
export * from './components/dropdown-menu/index'
|
||||
export * from './components/form/index'
|
||||
export * from './components/input/index'
|
||||
export * from './components/input-group/index'
|
||||
export * from './components/kbd/index'
|
||||
export * from './components/label/index'
|
||||
export * from './components/native-select/index'
|
||||
export * from './components/pagination/index'
|
||||
export * from './components/radio-group/index'
|
||||
export * from './components/scroll-area/index'
|
||||
export * from './components/select/index'
|
||||
export * from './components/separator/index'
|
||||
export * from './components/sheet/index'
|
||||
export * from './components/sidebar/index'
|
||||
export * from './components/skeleton/index'
|
||||
export * from './components/slider/index'
|
||||
export * from './components/sonner/index'
|
||||
export * from './components/spinner/index'
|
||||
export * from './components/switch/index'
|
||||
export * from './components/table/index'
|
||||
export * from './components/tabs/index'
|
||||
export * from './components/tags-input/index'
|
||||
export * from './components/textarea/index'
|
||||
export * from './components/tooltip/index'
|
||||
@@ -1,144 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 1rem;
|
||||
/* Base colors - gray-50 / gray-900 */
|
||||
--background: oklch(0.985 0 0); /* gray-50 */
|
||||
--foreground: oklch(0.224 0 0); /* gray-900 */
|
||||
/* Card - white with gray border */
|
||||
--card: oklch(1 0 0); /* white */
|
||||
--card-foreground: oklch(0.224 0 0); /* gray-900 */
|
||||
/* Popover - white */
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.224 0 0);
|
||||
/* Primary - blue-500 */
|
||||
--primary: oklch(0.572 0.188 255.29); /* blue-500 */
|
||||
--primary-foreground: oklch(1 0 0); /* white */
|
||||
/* Secondary - gray-100 */
|
||||
--secondary: oklch(0.965 0 0); /* gray-100 */
|
||||
--secondary-foreground: oklch(0.224 0 0); /* gray-900 */
|
||||
/* Muted - gray-100 / gray-500 */
|
||||
--muted: oklch(0.965 0 0); /* gray-100 */
|
||||
--muted-foreground: oklch(0.539 0 0); /* gray-500 */
|
||||
/* Accent - gray-100 */
|
||||
--accent: oklch(0.965 0 0); /* gray-100 */
|
||||
--accent-foreground: oklch(0.224 0 0); /* gray-900 */
|
||||
/* Destructive - red-500 */
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
/* Border - gray-200 */
|
||||
--border: oklch(0.922 0 0); /* gray-200 */
|
||||
--input: oklch(0.922 0 0); /* gray-200 */
|
||||
/* Ring - blue-500 */
|
||||
--ring: oklch(0.572 0.188 255.29); /* blue-500 */
|
||||
/* Charts */
|
||||
--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);
|
||||
/* Sidebar - white */
|
||||
--sidebar: oklch(1 0 0);
|
||||
--sidebar-foreground: oklch(0.224 0 0);
|
||||
--sidebar-primary: oklch(0.572 0.188 255.29); /* blue-500 */
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.965 0 0); /* gray-100 */
|
||||
--sidebar-accent-foreground: oklch(0.224 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0); /* gray-200 */
|
||||
--sidebar-ring: oklch(0.572 0.188 255.29); /* blue-500 */
|
||||
}
|
||||
|
||||
.dark {
|
||||
/* Base colors - gray-800 / gray-100 */
|
||||
--background: oklch(0.298 0 0); /* gray-800 */
|
||||
--foreground: oklch(0.965 0 0); /* gray-100 */
|
||||
/* Card - gray-900 with gray-700 border */
|
||||
--card: oklch(0.224 0 0); /* gray-900 */
|
||||
--card-foreground: oklch(0.965 0 0); /* gray-100 */
|
||||
/* Popover - gray-900 */
|
||||
--popover: oklch(0.224 0 0);
|
||||
--popover-foreground: oklch(0.965 0 0);
|
||||
/* Primary - blue-500 */
|
||||
--primary: oklch(0.572 0.188 255.29); /* blue-500 */
|
||||
--primary-foreground: oklch(1 0 0); /* white */
|
||||
/* Secondary - gray-700 */
|
||||
--secondary: oklch(0.427 0 0); /* gray-700 */
|
||||
--secondary-foreground: oklch(0.965 0 0); /* gray-100 */
|
||||
/* Muted - gray-700 / gray-400 */
|
||||
--muted: oklch(0.427 0 0); /* gray-700 */
|
||||
--muted-foreground: oklch(0.642 0 0); /* gray-400 */
|
||||
/* Accent - gray-700 */
|
||||
--accent: oklch(0.427 0 0); /* gray-700 */
|
||||
--accent-foreground: oklch(0.965 0 0); /* gray-100 */
|
||||
/* Destructive - red-500 */
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
/* Border - gray-700 */
|
||||
--border: oklch(0.427 0 0); /* gray-700 */
|
||||
--input: oklch(0.427 0 0); /* gray-700 */
|
||||
/* Ring - blue-500 */
|
||||
--ring: oklch(0.572 0.188 255.29); /* blue-500 */
|
||||
/* Charts */
|
||||
--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 - gray-900 */
|
||||
--sidebar: oklch(0.224 0 0);
|
||||
--sidebar-foreground: oklch(0.965 0 0);
|
||||
--sidebar-primary: oklch(0.572 0.188 255.29); /* blue-500 */
|
||||
--sidebar-primary-foreground: oklch(1 0 0);
|
||||
--sidebar-accent: oklch(0.427 0 0); /* gray-700 */
|
||||
--sidebar-accent-foreground: oklch(0.965 0 0);
|
||||
--sidebar-border: oklch(0.427 0 0); /* gray-700 */
|
||||
--sidebar-ring: oklch(0.572 0.188 255.29); /* blue-500 */
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"#/*": ["./src/*"]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import path from 'node:path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
tailwindcss()
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'#': path.resolve(__dirname, './src')
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
# Markdown 语法测试文档
|
||||
|
||||
## 1. 标题层级
|
||||
### 一级标题
|
||||
#### 二级标题
|
||||
##### 三级标题
|
||||
###### 六级标题
|
||||
|
||||
## 2. 文本格式
|
||||
**粗体文本**
|
||||
*斜体文本*
|
||||
~~删除线文本~~
|
||||
`代码片段`
|
||||
|
||||
> 引用文本
|
||||
> 可以多行
|
||||
|
||||
## 3. 列表
|
||||
|
||||
### 无序列表
|
||||
- 项目一
|
||||
- 项目二
|
||||
- 子项目
|
||||
- 项目三
|
||||
|
||||
### 有序列表
|
||||
1. 第一步
|
||||
2. 第二步
|
||||
3. 第三步
|
||||
|
||||
## 4. 链接与图片
|
||||
[百度](https://www.baidu.com)
|
||||

|
||||
|
||||
## 5. 表格
|
||||
| 姓名 | 年龄 | 职业 |
|
||||
|------|------|------|
|
||||
| 张三 | 25 | 开发 |
|
||||
| 李四 | 30 | 测试 |
|
||||
|
||||
## 6. 代码块
|
||||
```python
|
||||
def hello_world():
|
||||
print("Hello, World!")
|
||||
@@ -9,16 +9,33 @@
|
||||
"start": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jamescoyle/vue-icon": "^0.1.2",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@memoh/shared": "workspace:*",
|
||||
"@memoh/ui": "workspace:*",
|
||||
"@pinia/colada": "^0.21.1",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"axios": "^1.13.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"katex": "^0.16.28",
|
||||
"markstream-vue": "0.0.7-beta.2",
|
||||
"mermaid": "^11.12.2",
|
||||
"modern-css-reset": "^1.4.0",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"shiki": "^3.21.0",
|
||||
"stream-markdown": "^0.0.13",
|
||||
"stream-monaco": "^0.0.18",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4"
|
||||
"vue-router": "^4.6.4",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.1",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -1,10 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterView } from 'vue-router'
|
||||
import { Alert, Button } from '@memoh/ui'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from '@memoh/ui'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiTranslate, mdiBrightness6 } from '@mdi/js'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
|
||||
const mode = useColorMode()
|
||||
const modeToggleMap:Record<'dark'|'light','dark'|'light'> = {
|
||||
dark: 'light',
|
||||
light:'dark'
|
||||
}
|
||||
console.log(mode.value)
|
||||
const toggleMode = () => {
|
||||
if (mode.value !== 'auto') {
|
||||
mode.value = modeToggleMap[mode.value]
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
<Alert>Hello</Alert>
|
||||
<Button>Click me</Button>
|
||||
<section>
|
||||
<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 />
|
||||
</section>
|
||||
</template>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<section class="flex">
|
||||
<Dialog v-model:open="open ">
|
||||
<DialogTrigger as-child>
|
||||
<Button
|
||||
variant="default"
|
||||
class="ml-auto my-4"
|
||||
>
|
||||
添加平台
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="sm:max-w-106.25">
|
||||
<form @submit="addPlatform">
|
||||
<DialogHeader>
|
||||
<DialogTitle>添加平台</DialogTitle>
|
||||
<DialogDescription class="mb-4">
|
||||
为模型添加使用平台
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="name"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="请输入Name"
|
||||
v-bind="componentField"
|
||||
autocomplete="name"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="config"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Config
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TagsInput
|
||||
:add-on-blur="true"
|
||||
:model-value="configList"
|
||||
:convert-value="tagStr => {
|
||||
if (/^\w+\:\w+$/.test(tagStr)) {
|
||||
return tagStr
|
||||
}
|
||||
return ''
|
||||
}"
|
||||
@update:model-value="(env) => {
|
||||
configList = env.filter(Boolean) as string[]
|
||||
const curConfig: Record<string,string> = {}
|
||||
configList.forEach(envItem => {
|
||||
const [key, value] = envItem.split(`:`);
|
||||
if (key && value) {
|
||||
curConfig[key] = value
|
||||
}
|
||||
})
|
||||
componentField['onUpdate:modelValue']?.(curConfig)
|
||||
}"
|
||||
>
|
||||
<TagsInputItem
|
||||
v-for="(value, index) in configList"
|
||||
:key="index"
|
||||
:value="value as string"
|
||||
>
|
||||
<TagsInputItemText />
|
||||
<TagsInputItemDelete />
|
||||
</TagsInputItem>
|
||||
<TagsInputInput
|
||||
placeholder="请输入Env"
|
||||
class="w-full py-1"
|
||||
/>
|
||||
</TagsInput>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="active"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
是否立即使用
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
id="airplane-mode"
|
||||
:model-value="componentField.modelValue"
|
||||
@update:model-value="componentField['onUpdate:modelValue']"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
<DialogFooter class="mt-4">
|
||||
<DialogClose as-child>
|
||||
<Button variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">
|
||||
添加MCP
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
FormField,
|
||||
FormControl,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
TagsInput,
|
||||
TagsInputInput,
|
||||
TagsInputItem,
|
||||
TagsInputItemDelete,
|
||||
TagsInputItemText,
|
||||
Switch,
|
||||
} from '@memoh/ui'
|
||||
import z from 'zod'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { ref, inject } from 'vue'
|
||||
import { useMutation } from '@pinia/colada'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const configList=ref<string[]>([])
|
||||
const validataSchema = toTypedSchema(z.object({
|
||||
name: z.string().min(1),
|
||||
config: z.looseObject({}),
|
||||
active:z.coerce.boolean()
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema:validataSchema
|
||||
})
|
||||
|
||||
|
||||
const {mutate:addFetchPlatform}=useMutation({
|
||||
mutation: (data: Parameters<(Parameters<typeof form.handleSubmit>)[0]>[0]) => request({
|
||||
url: '/platform/',
|
||||
data,
|
||||
method:'post'
|
||||
})
|
||||
})
|
||||
const addPlatform = form.handleSubmit(async (value) => {
|
||||
try {
|
||||
await addFetchPlatform(value)
|
||||
open.value=false
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
const open=inject('open',ref<boolean>(false))
|
||||
</script>
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class=" p-2 rounded-full bg-[#F9F9F9] dark:bg-[#666] ">
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiRobotOutline"
|
||||
/>
|
||||
</div>
|
||||
<section class="w-[90%]">
|
||||
<sup class="font-semibold">
|
||||
{{ robotSay.type }}
|
||||
</sup>
|
||||
<p class="leading-7 text-muted-foreground break-all">
|
||||
<template v-if="robotSay.state==='thinking'">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
cx="4"
|
||||
cy="12"
|
||||
r="3"
|
||||
fill="currentColor"
|
||||
>
|
||||
<animate
|
||||
id="SVG7x14Dcom"
|
||||
fill="freeze"
|
||||
attributeName="opacity"
|
||||
begin="0;SVGqSjG0dUp.end-0.25s"
|
||||
dur="0.75s"
|
||||
values="1;0.2"
|
||||
/>
|
||||
</circle>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="3"
|
||||
fill="currentColor"
|
||||
opacity="0.4"
|
||||
>
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="opacity"
|
||||
begin="SVG7x14Dcom.begin+0.15s"
|
||||
dur="0.75s"
|
||||
values="1;0.2"
|
||||
/>
|
||||
</circle>
|
||||
<circle
|
||||
cx="20"
|
||||
cy="12"
|
||||
r="3"
|
||||
fill="currentColor"
|
||||
opacity="0.3"
|
||||
>
|
||||
<animate
|
||||
id="SVGqSjG0dUp"
|
||||
fill="freeze"
|
||||
attributeName="opacity"
|
||||
begin="SVG7x14Dcom.begin+0.3s"
|
||||
dur="0.75s"
|
||||
values="1;0.2"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MarkdownRender
|
||||
:content="robotSay.description"
|
||||
custom-id="chat-answer"
|
||||
/>
|
||||
</template>
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiRobotOutline } from '@mdi/js'
|
||||
import type { robot } from '@memoh/shared'
|
||||
import MarkdownRender,{enableKatex,enableMermaid} from 'markstream-vue'
|
||||
enableKatex()
|
||||
enableMermaid()
|
||||
const {robotSay}=defineProps<{
|
||||
robotSay: robot
|
||||
}>()
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<p
|
||||
class="leading-7 not-first:mt-6 max-w-[90%] ml-auto text-muted-foreground bg-[#F9F9F9] p-4 rounded-xl rounded-tr-none break-all dark:bg-[#1C1917]
|
||||
"
|
||||
>
|
||||
{{ userSay.description }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { user } from '@memoh/shared'
|
||||
const { userSay } = defineProps<{
|
||||
userSay: user
|
||||
}>()
|
||||
</script>
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div
|
||||
ref="displayContainer"
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<template
|
||||
v-for="chatItem in chatList"
|
||||
:key="chatItem.id"
|
||||
>
|
||||
<UserChat
|
||||
v-if="chatItem.action === 'user'"
|
||||
:user-say="chatItem"
|
||||
/>
|
||||
<RobotChat
|
||||
v-if="chatItem.action === 'robot'"
|
||||
:robot-say="chatItem"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserChat from './UserChat/index.vue'
|
||||
import RobotChat from './RobotChat/index.vue'
|
||||
import { inject, nextTick, ref, watch } from 'vue'
|
||||
import { useElementBounding } from '@vueuse/core'
|
||||
import { useChatList } from '@/store/ChatList'
|
||||
import { onBeforeRouteLeave } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
// 模拟一下数据
|
||||
const {chatList,add} = useChatList()
|
||||
const { loading}=storeToRefs(useChatList())
|
||||
const chatSay = inject('chatSay', ref(''))
|
||||
// 模拟一下对话
|
||||
watch(chatSay, () => {
|
||||
if (chatSay.value) {
|
||||
add({
|
||||
description: chatSay.value,
|
||||
time: new Date(),
|
||||
action: 'user',
|
||||
id: 1
|
||||
})
|
||||
|
||||
add({
|
||||
description: '',
|
||||
time: new Date(),
|
||||
action: 'robot',
|
||||
id: 2,
|
||||
type: 'Openai Gpt5',
|
||||
state:'thinking'
|
||||
})
|
||||
chatSay.value=''
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const displayContainer = ref()
|
||||
const { height,top } = useElementBounding(displayContainer)
|
||||
|
||||
let prevScroll = 0, curScroll = 0, autoScroll = true,cacheScroll=0
|
||||
|
||||
watch(top, () => {
|
||||
const container = displayContainer.value?.parentElement?.parentElement
|
||||
if (height.value === 0) {
|
||||
autoScroll = false
|
||||
prevScroll = curScroll=0
|
||||
}
|
||||
if ((container?.scrollHeight - container.clientHeight - container.scrollTop) < 1) {
|
||||
autoScroll = true
|
||||
prevScroll=curScroll=container.scrollTop
|
||||
}
|
||||
})
|
||||
|
||||
watch(height, (newVal,oldVal) => {
|
||||
const container = displayContainer.value?.parentElement?.parentElement
|
||||
if (container) {
|
||||
curScroll = container.scrollTop
|
||||
if (curScroll < prevScroll) {
|
||||
autoScroll = false
|
||||
}
|
||||
prevScroll = curScroll
|
||||
}
|
||||
|
||||
if (oldVal === 0 && newVal > container.clientHeight) {
|
||||
nextTick(() => {
|
||||
container.scrollTo({
|
||||
top: cacheScroll,
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!(container && (container?.scrollHeight - container.clientHeight - container.scrollTop) < 1) && autoScroll&&loading.value) {
|
||||
|
||||
container.scrollTo({
|
||||
top: container?.scrollHeight - container.clientHeight,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
const container = displayContainer.value?.parentElement?.parentElement
|
||||
if (container) {
|
||||
cacheScroll = container.scrollTop
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<section class="flex">
|
||||
<Dialog v-model:open="open">
|
||||
<DialogTrigger as-child>
|
||||
<Button
|
||||
variant="default"
|
||||
class="ml-auto my-4"
|
||||
>
|
||||
{{ $t("button.add",{msg:"MCP"}) }}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="sm:max-w-106.25">
|
||||
<form @submit="createMCP">
|
||||
<DialogHeader>
|
||||
<DialogTitle> {{ $t("button.add", { msg: "MCP" }) }}</DialogTitle>
|
||||
<DialogDescription class="mb-4">
|
||||
添加MCP完成操作
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="name"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="$t('prompt.enter', { msg: 'Name' })"
|
||||
v-bind="componentField"
|
||||
autocomplete="name"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="config.type"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Type
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue
|
||||
:placeholder="$t('prompt.select', { msg: 'Type' })"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="stdio">
|
||||
Stdio
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="config.cwd"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Cwd
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="$t('prompt.enter', { msg: 'cwd' })"
|
||||
v-bind="componentField"
|
||||
autocomplete="cwd"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="config.command"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Command
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
:placeholder="$t('prompt.enter', { msg: 'Command' })"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="config.args"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Arguments
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TagsInput
|
||||
v-model="componentField.modelValue"
|
||||
:add-on-blur="true"
|
||||
:duplicate="true"
|
||||
@update:model-value="componentField['onUpdate:modelValue']"
|
||||
>
|
||||
<TagsInputItem
|
||||
v-for="item in componentField.modelValue"
|
||||
:key="item"
|
||||
:value="item"
|
||||
>
|
||||
<TagsInputItemText />
|
||||
<TagsInputItemDelete />
|
||||
</TagsInputItem>
|
||||
<TagsInputInput
|
||||
:placeholder="$t('prompt.enter', { msg: 'Arguments' })"
|
||||
class="w-full py-1"
|
||||
/>
|
||||
</TagsInput>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="config.env"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Env
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<TagsInput
|
||||
:add-on-blur="true"
|
||||
:model-value="envList"
|
||||
:convert-value="tagStr => {
|
||||
if (/^\w+\:\w+$/.test(tagStr)) {
|
||||
return tagStr
|
||||
}
|
||||
return ''
|
||||
}"
|
||||
@update:model-value="(env) => {
|
||||
envList = env.filter(Boolean) as string[]
|
||||
const curEnvObject: { [key in string]: string } = {}
|
||||
envList.forEach(envItem => {
|
||||
const [key, value] = envItem.split(`:`);
|
||||
if (key && value) {
|
||||
curEnvObject[key] = value
|
||||
}
|
||||
})
|
||||
componentField['onUpdate:modelValue']?.(curEnvObject)
|
||||
}"
|
||||
>
|
||||
<TagsInputItem
|
||||
v-for="(value, index) in envList"
|
||||
:key="index"
|
||||
:value="value as string"
|
||||
>
|
||||
<TagsInputItemText />
|
||||
<TagsInputItemDelete />
|
||||
</TagsInputItem>
|
||||
<TagsInputInput
|
||||
:placeholder="$t('prompt.enter', { msg: 'Env' })"
|
||||
class="w-full py-1"
|
||||
/>
|
||||
</TagsInput>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="active"
|
||||
>
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<section class="flex gap-4">
|
||||
<Label for="airplane-mode">{{ $t('state.open') }}</Label>
|
||||
<Switch
|
||||
id="airplane-mode"
|
||||
:model-value="componentField.modelValue"
|
||||
@update:model-value="componentField['onUpdate:modelValue']"
|
||||
/>
|
||||
</section>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
<DialogFooter class="mt-4">
|
||||
<DialogClose as-child>
|
||||
<Button variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">
|
||||
{{ $t("button.add", { msg: "MCP" }) }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
FormField,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
TagsInput,
|
||||
TagsInputInput,
|
||||
TagsInputItem,
|
||||
TagsInputItemDelete,
|
||||
TagsInputItemText,
|
||||
Switch,
|
||||
Label
|
||||
} from '@memoh/ui'
|
||||
import z from 'zod'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { ref, inject, watch } from 'vue'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
import request from '@/utils/request'
|
||||
import { type MCPListItem as MCPType } from '@memoh/shared'
|
||||
|
||||
const validateSchema = toTypedSchema(z.object({
|
||||
name: z.string().min(1),
|
||||
config: z.object({
|
||||
type: z.string().min(1),
|
||||
command: z.string().min(1),
|
||||
args: z.array(z.coerce.string().check(z.minLength(1))).min(1),
|
||||
env: z.looseObject({}),
|
||||
cwd: z.string().min(1)
|
||||
}),
|
||||
active: z.coerce.boolean()
|
||||
}))
|
||||
|
||||
const envList = ref<string[]>([])
|
||||
const form = useForm({
|
||||
validationSchema: validateSchema
|
||||
})
|
||||
|
||||
|
||||
const queryCache = useQueryCache()
|
||||
const { mutate: fetchMCP } = useMutation({
|
||||
mutation: (data: Parameters<(Parameters<typeof form.handleSubmit>)[0]>[0]) => request({
|
||||
url: mcpEditData.value?.id ? `/mcp/${mcpEditData.value.id}` : '/mcp/',
|
||||
method: mcpEditData.value?.id ? 'put' : 'post',
|
||||
data
|
||||
}),
|
||||
onSettled: () => queryCache.invalidateQueries({ key: ['mcp'] })
|
||||
})
|
||||
|
||||
const open = inject('open', ref(false))
|
||||
const mcpEditData = inject('mcpEditData', ref<{
|
||||
name: string,
|
||||
config: MCPType['config'],
|
||||
active: boolean
|
||||
id: string
|
||||
} | null>(null))
|
||||
|
||||
watch(open, () => {
|
||||
if (open.value && mcpEditData.value) {
|
||||
form.setValues(mcpEditData.value)
|
||||
}
|
||||
|
||||
if (!open.value) {
|
||||
mcpEditData.value = null
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
const createMCP = form.handleSubmit(async (value) => {
|
||||
try {
|
||||
console.log(mcpEditData.value)
|
||||
fetchMCP(value)
|
||||
open.value = false
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<section class="ml-auto">
|
||||
<Dialog v-model:open="open">
|
||||
<DialogTrigger as-child>
|
||||
<Button variant="default">
|
||||
{{ $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>
|
||||
<DialogDescription class="mb-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>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="name"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Display Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
:placeholder="$t('prompt.enter', { msg: 'Display Name' })"
|
||||
autocomplete="name"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="type"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Role
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue :placeholder="$t('prompt.select', { msg: 'Role' })" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="chat">
|
||||
Chat
|
||||
</SelectItem>
|
||||
<SelectItem value="embedding">
|
||||
embedding
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
<DialogFooter class="mt-4">
|
||||
<DialogClose as-child>
|
||||
<Button variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">
|
||||
{{ $t("button.add", { msg: "Model" }) }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
Input,
|
||||
Button,
|
||||
FormField,
|
||||
FormControl,
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from '@memoh/ui'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { inject, watch, type Ref,ref } from 'vue'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import z from 'zod'
|
||||
import request from '@/utils/request'
|
||||
import { useMutation, useQueryCache } from '@pinia/colada'
|
||||
|
||||
|
||||
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),
|
||||
}))
|
||||
|
||||
const form = useForm({
|
||||
validationSchema: formSchema
|
||||
})
|
||||
|
||||
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}`,
|
||||
data: {
|
||||
...modelInfo,
|
||||
},
|
||||
method: 'PUT'
|
||||
}),
|
||||
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)
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const open = inject<Ref<boolean>>('open',ref(false))
|
||||
const editInfo = inject('editModelInfo',ref<null|(ModelInfoType&{id:string})>(null))
|
||||
watch(open, () => {
|
||||
if (open.value && editInfo?.value) {
|
||||
form.setValues(editInfo.value)
|
||||
}
|
||||
}, {
|
||||
immediate:true
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts" generic="TData, TValue">
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
import {
|
||||
FlexRender,
|
||||
getCoreRowModel,
|
||||
useVueTable,
|
||||
} from '@tanstack/vue-table'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@memoh/ui'
|
||||
|
||||
const props = defineProps<{
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
data: TData[]
|
||||
}>()
|
||||
|
||||
const table = useVueTable({
|
||||
get data() { return props.data },
|
||||
get columns() { return props.columns },
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow
|
||||
v-for="headerGroup in table.getHeaderGroups()"
|
||||
:key="headerGroup.id"
|
||||
>
|
||||
<TableHead
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
>
|
||||
<FlexRender
|
||||
v-if="!header.isPlaceholder"
|
||||
:render="header.column.columnDef.header"
|
||||
:props="header.getContext()"
|
||||
/>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody class="[&_td]:py-4!">
|
||||
<template v-if="table.getRowModel().rows?.length">
|
||||
<TableRow
|
||||
v-for="row in table.getRowModel().rows"
|
||||
:key="row.id"
|
||||
:data-state="row.getIsSelected() ? 'selected' : undefined"
|
||||
>
|
||||
<TableCell
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
:key="cell.id"
|
||||
>
|
||||
<FlexRender
|
||||
:render="cell.column.columnDef.cell"
|
||||
:props="cell.getContext()"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
<template v-else>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
:colspan="columns.length"
|
||||
class="h-24 text-center"
|
||||
>
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<SidebarInset>
|
||||
<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">
|
||||
<SidebarTrigger class="-ml-1" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
class="mr-2 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
<template
|
||||
v-for="(breadcrumbItem, index) in curBreadcrumb"
|
||||
:key="breadcrumbItem"
|
||||
>
|
||||
<template v-if="(index + 1) !== curBreadcrumb.length">
|
||||
<BreadcrumbItem class="hidden md:block">
|
||||
<BreadcrumbLink :href="breadcrumbItem.path">
|
||||
{{ breadcrumbItem.breadcrumb }}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
<BreadcrumbSeparator />
|
||||
</template>
|
||||
|
||||
<BreadcrumbItem v-else>
|
||||
<BreadcrumbPage>
|
||||
{{ breadcrumbItem.breadcrumb }}
|
||||
</BreadcrumbPage>
|
||||
</BreadcrumbItem>
|
||||
</template>
|
||||
</BreadcrumbList>
|
||||
</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>
|
||||
</SidebarInset>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
SidebarTrigger, SidebarInset, Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
Separator,
|
||||
// DropdownMenu,
|
||||
// DropdownMenuContent,
|
||||
// DropdownMenuItem,
|
||||
// DropdownMenuTrigger,
|
||||
} 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()
|
||||
|
||||
const curBreadcrumb = computed(() => {
|
||||
return route.matched.map(routeItem => ({
|
||||
path: routeItem.path,
|
||||
breadcrumb: routeItem.meta['breadcrumb']
|
||||
}))
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<aside class="[&_[data-state=collapsed]_:is(.title-container,.exist-btn)]:hidden">
|
||||
<Sidebar collapsible="icon">
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<img
|
||||
src="../../../public/logo.png"
|
||||
width="80"
|
||||
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>
|
||||
<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>
|
||||
</SidebarMenu>
|
||||
</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>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
CollapsibleTrigger,
|
||||
Collapsible,
|
||||
Button
|
||||
} from '@memoh/ui'
|
||||
import { computed } from 'vue'
|
||||
import SvgIcon from '@jamescoyle/vue-icon'
|
||||
import { mdiRobot, mdiChatOutline, mdiCogBox, mdiListBox, mdiHome, mdiBookArrowDown } from '@mdi/js'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/store/User.ts'
|
||||
import i18n from '@/i18n'
|
||||
|
||||
const router=useRouter()
|
||||
|
||||
const { t } = i18n.global
|
||||
|
||||
const sidebarInfo = computed(() => [
|
||||
{
|
||||
title: t('slidebar.chat'),
|
||||
name: 'chat',
|
||||
icon: mdiChatOutline
|
||||
},
|
||||
// {
|
||||
// title: t('slidebar.home'),
|
||||
// name: 'home',
|
||||
// icon: mdiHome
|
||||
// },
|
||||
{
|
||||
title: t('slidebar.model_setting'),
|
||||
name: 'models',
|
||||
icon: mdiRobot
|
||||
}, {
|
||||
title: t('slidebar.setting'),
|
||||
name: 'settings',
|
||||
icon: mdiCogBox
|
||||
}, {
|
||||
title: 'MCP',
|
||||
name: 'mcp',
|
||||
icon: mdiListBox
|
||||
}, {
|
||||
title: t('slidebar.platform'),
|
||||
name: 'platform',
|
||||
icon: mdiBookArrowDown
|
||||
}
|
||||
])
|
||||
|
||||
const { exitLogin } = useUserStore()
|
||||
const exit = () => {
|
||||
exitLogin()
|
||||
router.replace({ name: 'Login' })
|
||||
}
|
||||
</script>
|
||||
@@ -1,7 +1,29 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from '@/i18n/locales/en.json'
|
||||
import zh from '@/i18n/locales/zh.json'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
type enMessageSchema = typeof en
|
||||
type zhMessageSchema = typeof zh;
|
||||
|
||||
|
||||
const i18n = createI18n<[enMessageSchema, zhMessageSchema], 'en' | 'zh'>({
|
||||
locale: 'zh',
|
||||
legacy: false,
|
||||
fallbackLocale: 'en',
|
||||
messages: {
|
||||
en,
|
||||
zh
|
||||
}
|
||||
})
|
||||
|
||||
export default i18n
|
||||
|
||||
export default i18n
|
||||
|
||||
const t = i18n.global.t
|
||||
|
||||
export const i18nRef = (arg:string) => {
|
||||
return computed(() => {
|
||||
return t(arg)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"login": {
|
||||
"username": "Username",
|
||||
"password": "Password",
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"forget": "Forgot your password?",
|
||||
"exit": "Sign Out"
|
||||
},
|
||||
"prompt": {
|
||||
"enter": "Please enter {msg}",
|
||||
"select": "Please select {msg}"
|
||||
},
|
||||
"slidebar": {
|
||||
"setting": "Settings",
|
||||
"platform": "Platform",
|
||||
"chat": "Create Chat",
|
||||
"model_setting": "Model Settings",
|
||||
"home": "Home"
|
||||
},
|
||||
"desc": {
|
||||
"question": "your question"
|
||||
},
|
||||
"chat": {
|
||||
"send": "Send",
|
||||
"chat": "Chat"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"main": "Main"
|
||||
},
|
||||
"button": {
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"add": "Add {msg}"
|
||||
},
|
||||
"state":{
|
||||
"open":"Open"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"login": {
|
||||
"username": "用户名",
|
||||
"password": "密码",
|
||||
"login": "登录",
|
||||
"register": "注册",
|
||||
"forget": "忘记密码?",
|
||||
"exit": "退出登录"
|
||||
},
|
||||
"desc": {
|
||||
"question": "您的问题"
|
||||
},
|
||||
"prompt": {
|
||||
"enter": "请输入{msg}",
|
||||
"select": "请选择{msg}"
|
||||
},
|
||||
"slidebar": {
|
||||
"setting": "设置",
|
||||
"platform": "平台",
|
||||
"chat": "创建对话",
|
||||
"model_setting": "模型配置",
|
||||
"home": "主页"
|
||||
},
|
||||
"chat": {
|
||||
"send": "发送",
|
||||
"chat": "对话"
|
||||
},
|
||||
"breadcrumb": {
|
||||
"main": "主菜单"
|
||||
},
|
||||
"button": {
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"add": "添加{msg}"
|
||||
},
|
||||
"state": {
|
||||
"open": "Open"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<section class="flex">
|
||||
<sidebar-provider>
|
||||
<slot name="sidebar" />
|
||||
<slot name="main" />
|
||||
</sidebar-provider>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { SidebarProvider } from '@memoh/ui'
|
||||
|
||||
</script>
|
||||
@@ -1,13 +1,20 @@
|
||||
import { createApp } from 'vue'
|
||||
// @ts-ignore
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
import i18n from './i18n'
|
||||
import '@memoh/ui/style.css'
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
// @ts-ignore
|
||||
import 'markstream-vue/index.css'
|
||||
// @ts-ignore
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
createApp(App)
|
||||
.use(createPinia())
|
||||
.use(createPinia().use(piniaPluginPersistedstate))
|
||||
.use(PiniaColada)
|
||||
.use(router)
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
.mount('#app')
|
||||
@@ -0,0 +1,82 @@
|
||||
<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">
|
||||
<ScrollArea
|
||||
ref="chat-container"
|
||||
class="max-h-full h-full w-full rounded-md border p-4 **:focus-visible:ring-0! "
|
||||
>
|
||||
<ChatList />
|
||||
</ScrollArea>
|
||||
</section>
|
||||
<section class="flex-none relative">
|
||||
<Textarea
|
||||
v-model="curInputSay"
|
||||
class="pb-16 pt-4"
|
||||
:placeholder="$t('prompt.enter', { msg: $t('desc.question') })"
|
||||
/>
|
||||
<section class="absolute bottom-0 h-14 px-2 inset-x-0 flex items-center">
|
||||
<Button
|
||||
variant="default"
|
||||
class="ml-auto"
|
||||
@click="send"
|
||||
>
|
||||
<template v-if="!loading">
|
||||
{{ $t('chat.send') }}
|
||||
<svg-icon
|
||||
type="mdi"
|
||||
:path="mdiSendOutline"
|
||||
/>
|
||||
</template>
|
||||
<img
|
||||
v-else
|
||||
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48Y2lyY2xlIGN4PSI0IiBjeT0iMTIiIHI9IjEuNSIgZmlsbD0iI2ZmZiI+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iciIgZHVyPSIwLjc1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMS41OzM7MS41Ii8+PC9jaXJjbGU+PGNpcmNsZSBjeD0iMTIiIGN5PSIxMiIgcj0iMyIgZmlsbD0iI2ZmZiI+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iciIgZHVyPSIwLjc1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMzsxLjU7MyIvPjwvY2lyY2xlPjxjaXJjbGUgY3g9IjIwIiBjeT0iMTIiIHI9IjEuNSIgZmlsbD0iI2ZmZiI+PGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iciIgZHVyPSIwLjc1cyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIHZhbHVlcz0iMS41OzM7MS41Ii8+PC9jaXJjbGU+PC9zdmc+"
|
||||
alt="loading"
|
||||
>
|
||||
</Button>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ScrollArea,
|
||||
Textarea,
|
||||
Button
|
||||
} 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'
|
||||
|
||||
const chatSay = ref('')
|
||||
const curInputSay = ref('')
|
||||
|
||||
const {loading}=storeToRefs(useChatList())
|
||||
provide('chatSay', chatSay)
|
||||
|
||||
|
||||
const send = () => {
|
||||
if (loading.value === false) {
|
||||
chatSay.value = curInputSay.value
|
||||
curInputSay.value = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<section>
|
||||
<h1>主页</h1>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,159 @@
|
||||
<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>
|
||||
<img
|
||||
src="../../../public/logo.png"
|
||||
width="100"
|
||||
alt="logo.png"
|
||||
class="m-auto"
|
||||
>
|
||||
<h3 class="scroll-m-20 text-2xl font-semibold tracking-tight text-white text-center">
|
||||
Memoh
|
||||
</h3>
|
||||
</section>
|
||||
<form @submit="login">
|
||||
<Card class="py-14">
|
||||
<CardContent class="flex flex-col [&_input]:py-5">
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="username"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
{{ $t("login.username") }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
:placeholder="$t('prompt.enter', { msg: $t(`login.username`).toLocaleLowerCase() })"
|
||||
v-bind="componentField"
|
||||
autocomplete="username"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="password"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
{{ $t('login.password') }}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
:placeholder="$t('prompt.enter',{msg:$t(`login.password`).toLocaleLowerCase()})"
|
||||
autocomplete="password"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<div class="flex">
|
||||
<a
|
||||
href="#"
|
||||
class="ml-auto inline-block text-sm underline mt-2"
|
||||
>
|
||||
{{ $t('login.forget') }}
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="flex flex-col gap-4">
|
||||
<Button
|
||||
class="w-full"
|
||||
type="submit"
|
||||
@click="login"
|
||||
>
|
||||
{{ $t("login.login") }}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
>
|
||||
{{ $t("login.register") }}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
Input,
|
||||
Button,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@memoh/ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
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'
|
||||
|
||||
const router = useRouter()
|
||||
const formSchema = toTypedSchema(z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
}))
|
||||
const form = useForm({
|
||||
validationSchema: formSchema,
|
||||
})
|
||||
|
||||
const { login: LoginHandle } = useUserStore()
|
||||
const loading=ref(false)
|
||||
const login = form.handleSubmit(async (values) => {
|
||||
try {
|
||||
loading.value=true
|
||||
const loginState = await request({
|
||||
url: '/auth/login',
|
||||
method: 'post',
|
||||
data: { ...values }
|
||||
},false)
|
||||
const data = loginState?.data?.data
|
||||
if (data?.token && data?.user) {
|
||||
LoginHandle(data.user, data.token)
|
||||
}
|
||||
router.replace({
|
||||
name:'Main'
|
||||
})
|
||||
} catch (error) {
|
||||
return error
|
||||
} finally {
|
||||
loading.value=false
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<section>
|
||||
<MainLayout>
|
||||
<template #sidebar>
|
||||
<SideBar />
|
||||
</template>
|
||||
<template #main>
|
||||
<MainContainer />
|
||||
</template>
|
||||
</MainLayout>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import MainLayout from '@/layout/mainLayout/index.vue'
|
||||
import SideBar from '@/components/Sidebar/index.vue'
|
||||
import MainContainer from '@/components/MainContainer/index.vue'
|
||||
import { provide,ref } from 'vue'
|
||||
|
||||
|
||||
provide('sideBarIsOpen',ref(true))
|
||||
</script>
|
||||
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<section class="[&_td:last-child]:w-40">
|
||||
<CreateMCP />
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data="mcpFormatData"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useQuery,useMutation,useQueryCache } from '@pinia/colada'
|
||||
import request from '@/utils/request'
|
||||
import { watch, h, provide,ref, computed,reactive } from 'vue'
|
||||
import DataTable from '@/components/DataTable/index.vue'
|
||||
import CreateMCP from '@/components/CreateMCP/index.vue'
|
||||
import { type ColumnDef } from '@tanstack/vue-table'
|
||||
import {
|
||||
Badge,
|
||||
Button
|
||||
} from '@memoh/ui'
|
||||
import { type MCPListItem as MCPType } from '@memoh/shared'
|
||||
import { i18nRef } from '@/i18n'
|
||||
|
||||
|
||||
const open = ref(false)
|
||||
const editMCPData = ref<{
|
||||
name: string,
|
||||
config: MCPType['config'],
|
||||
active: boolean
|
||||
id:string
|
||||
}|null>(null)
|
||||
provide('open', open)
|
||||
provide('mcpEditData',editMCPData)
|
||||
|
||||
const queryCache=useQueryCache()
|
||||
const { mutate:DeleteMCP}= useMutation({
|
||||
mutation: (id:string) => request({
|
||||
url: `/mcp/${id}`,
|
||||
method:'DELETE'
|
||||
}),
|
||||
onSettled() {
|
||||
queryCache.invalidateQueries({
|
||||
key:['mcp']
|
||||
})
|
||||
}
|
||||
})
|
||||
const columns:ColumnDef<MCPType>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: () => h('div', { class: 'text-left py-4' }, 'Name'),
|
||||
|
||||
},
|
||||
{
|
||||
accessorKey: 'type',
|
||||
header: () => h('div', { class: 'text-left' }, 'Type'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'config.command',
|
||||
header: () => h('div', { class: 'text-left' }, 'Command'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'config.cwd',
|
||||
header: () => h('div', { class: 'text-left' }, 'Cwd'),
|
||||
},
|
||||
{
|
||||
accessorKey: 'config.args',
|
||||
header: () => h('div', { class: 'text-left' }, 'Arguments'),
|
||||
cell: ({ row }) => h('div', {class:'flex gap-4'}, row.original.config.args.map((argTxt) => {
|
||||
return h(Badge, {
|
||||
variant:'default'
|
||||
},()=>argTxt)
|
||||
}))
|
||||
},
|
||||
{
|
||||
accessorKey: 'config.env',
|
||||
header: () => h('div', { class: 'text-left' }, 'Env'),
|
||||
cell: ({ row }) => h('div', { class: 'flex gap-4' }, Object.entries(row.original.config.env).map(([key,value]) => {
|
||||
return h(Badge, {
|
||||
variant: 'outline'
|
||||
}, ()=>`${key}:${value}`)
|
||||
}))
|
||||
},
|
||||
{
|
||||
accessorKey: 'control',
|
||||
header: () => h('div', { class: 'text-center' }, '操作'),
|
||||
cell: ({ row }) => h('div', {class:'flex gap-2'}, [
|
||||
h(Button, {
|
||||
onClick() {
|
||||
editMCPData.value = {
|
||||
name: row.original.name,
|
||||
config: {...row.original.config},
|
||||
active: row.original.active,
|
||||
id:row.original.id
|
||||
}
|
||||
open.value=true
|
||||
}
|
||||
}, ()=>i18nRef('button.edit').value),
|
||||
h(Button, {
|
||||
variant: 'destructive',
|
||||
async onClick() {
|
||||
try {
|
||||
await DeleteMCP(row.original.id)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
},()=>i18nRef('button.delete').value)
|
||||
])
|
||||
}
|
||||
]
|
||||
|
||||
const { data: mcpData } = useQuery({
|
||||
key: ['mcp'],
|
||||
query: () => request({
|
||||
url: '/mcp/'
|
||||
})
|
||||
})
|
||||
|
||||
const mcpFormatData = computed(() => {
|
||||
return mcpData.value?.data?.items??[]
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,230 @@
|
||||
<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 {
|
||||
Button,
|
||||
// Pagination,
|
||||
// PaginationContent,
|
||||
// PaginationEllipsis,
|
||||
// PaginationItem,
|
||||
// PaginationNext,
|
||||
// PaginationPrevious,
|
||||
Checkbox
|
||||
} from '@memoh/ui'
|
||||
import DataTable from '@/components/DataTable/index.vue'
|
||||
import request from '@/utils/request'
|
||||
import { type ColumnDef } from '@tanstack/vue-table'
|
||||
import {type ModelTable as ModelType} from '@memoh/shared'
|
||||
import { i18nRef } from '@/i18n'
|
||||
|
||||
const openDialogModel = ref(false)
|
||||
const editModelInfo = ref<ModelType & { id: string } | null>(null)
|
||||
provide('open', openDialogModel)
|
||||
provide('editModelInfo', editModelInfo)
|
||||
|
||||
watch(openDialogModel, () => {
|
||||
if (!openDialogModel.value) {
|
||||
editModelInfo.value = null
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
})
|
||||
|
||||
|
||||
const cacheQuery = useQueryCache()
|
||||
const {
|
||||
mutate: deleteModel,
|
||||
} = useMutation({
|
||||
mutation: (id: string) =>
|
||||
request({
|
||||
url: `model/${id}`,
|
||||
method: 'DELETE'
|
||||
}),
|
||||
onSettled: () => {
|
||||
cacheQuery.invalidateQueries({
|
||||
key: ['models']
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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 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'))
|
||||
}
|
||||
},
|
||||
{
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
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 ?? {}
|
||||
})
|
||||
|
||||
</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
|
||||
>
|
||||
<PaginationContent v-slot="{ items }">
|
||||
<PaginationPrevious />
|
||||
<template
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
>
|
||||
<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> -->
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<section>
|
||||
<AddPlatform />
|
||||
<div>
|
||||
<menu class="grid grid-cols-4 gap-4 [&_li>*]:h-full">
|
||||
<li>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-muted-foreground">
|
||||
平台:Telegram
|
||||
</CardTitle>
|
||||
<CardContent class="mt-4 p-0">
|
||||
<ol
|
||||
class=" [&>li]:mt-2"
|
||||
type="1"
|
||||
>
|
||||
<li>功能1</li>
|
||||
<li>功能2</li>
|
||||
<li>功能2</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<section class="flex" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex gap-4">
|
||||
<Switch />
|
||||
<Button
|
||||
class="ml-auto"
|
||||
@click="open = true"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</li>
|
||||
<li>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-muted-foreground">
|
||||
平台:Telegram
|
||||
</CardTitle>
|
||||
<CardContent class="mt-4 p-0">
|
||||
<ol
|
||||
class=" [&>li]:mt-2"
|
||||
type="1"
|
||||
>
|
||||
<li>功能1</li>
|
||||
<li>功能2</li>
|
||||
<li>功能2</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<section class="flex" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex gap-4">
|
||||
<Switch />
|
||||
<Button class="ml-auto">
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</li>
|
||||
<li>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-muted-foreground">
|
||||
平台:Telegram
|
||||
</CardTitle>
|
||||
<CardContent class="mt-4 p-0">
|
||||
<ol
|
||||
class=" [&>li]:mt-2"
|
||||
type="1"
|
||||
>
|
||||
<li>功能1</li>
|
||||
<li>功能2</li>
|
||||
<li>功能2</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<section class="flex" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex gap-4">
|
||||
<Switch />
|
||||
<Button class="ml-auto">
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</li>
|
||||
<li>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-muted-foreground">
|
||||
平台:Telegram
|
||||
</CardTitle>
|
||||
<CardContent class="mt-4 p-0">
|
||||
<ol
|
||||
class=" [&>li]:mt-2"
|
||||
type="1"
|
||||
>
|
||||
<li>功能1</li>
|
||||
<li>功能2</li>
|
||||
<li>功能2</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<section class="flex" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex gap-4">
|
||||
<Switch />
|
||||
<Button class="ml-auto">
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</li>
|
||||
<li>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-muted-foreground flex justify-between">
|
||||
<header>
|
||||
平台:Telegram
|
||||
</header>
|
||||
|
||||
<Badge variant="outline">
|
||||
运行中...
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
<CardContent class="mt-4 p-0">
|
||||
<ol
|
||||
class=" [&>li]:mt-2"
|
||||
type="1"
|
||||
>
|
||||
<li>功能1</li>
|
||||
<li>功能2</li>
|
||||
<li>功能2</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<section class="flex" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex gap-4">
|
||||
<Switch />
|
||||
<Button
|
||||
class="ml-auto"
|
||||
@click="open=true"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</li>
|
||||
<li>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-muted-foreground">
|
||||
平台:Telegram
|
||||
</CardTitle>
|
||||
<CardContent class="mt-4 p-0">
|
||||
<ol
|
||||
class=" [&>li]:mt-2 "
|
||||
type="1"
|
||||
>
|
||||
<li>1. 功能1</li>
|
||||
<li>2. 功能2</li>
|
||||
<li>3. 功能2</li>
|
||||
</ol>
|
||||
</CardContent>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<section class="flex" />
|
||||
</CardContent>
|
||||
<CardFooter class="flex gap-4">
|
||||
<Switch />
|
||||
<Button class="ml-auto">
|
||||
编辑
|
||||
</Button>
|
||||
<Button variant="destructive">
|
||||
删除
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</li>
|
||||
</menu>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMutation, useQuery } from '@pinia/colada'
|
||||
import request from '@/utils/request'
|
||||
import { watch, h, provide, ref } from 'vue'
|
||||
import AddPlatform from '@/components/AddPlatform/index.vue'
|
||||
import {Card,CardHeader,CardFooter,CardContent,CardTitle,Switch,Button, Badge } from '@memoh/ui'
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
provide('open', open)
|
||||
|
||||
const { data: platformData } = useQuery({
|
||||
key: ['platform'],
|
||||
query: () => request({
|
||||
url: '/platform/'
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
watch(platformData, () => {
|
||||
console.log(platformData.value?.data)
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,295 @@
|
||||
<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>
|
||||
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</formcontrol>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="defaultSummaryModel"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
<!-- defaultSummaryModel -->
|
||||
Summary Model
|
||||
</FormLabel>
|
||||
<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>
|
||||
</FormControl>
|
||||
<blockquote class="h-5">
|
||||
<FormMessage />
|
||||
</blockquote>
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField
|
||||
v-slot="{ componentField }"
|
||||
name="language"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel class="mb-2">
|
||||
Language
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger class="w-full">
|
||||
<SelectValue
|
||||
:placeholder="$t('prompt.select', { msg: 'Language' })"
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectItem value="ch">
|
||||
中文
|
||||
</SelectItem>
|
||||
<SelectItem value="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>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
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'
|
||||
import {
|
||||
Input,
|
||||
Card,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
Button,
|
||||
FormMessage,
|
||||
Select,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectValue,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
CardFooter
|
||||
} from '@memoh/ui'
|
||||
|
||||
type ModelList = {
|
||||
id: ModelTable['id'],
|
||||
model: Omit<ModelTable, 'id' | 'defaultChatModel' | 'defaultEmbeddingModel' | 'defaultSummaryModel'>
|
||||
};
|
||||
|
||||
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)
|
||||
}
|
||||
return await request({
|
||||
url: '/settings/',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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'])
|
||||
}))
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
|
||||
const cacheQuery=useQueryCache()
|
||||
const { mutate: fetchSetting } = useMutation({
|
||||
mutation: (data:typeof currentSetting.value) => request({
|
||||
url: '/settings/',
|
||||
data
|
||||
}),
|
||||
onSettled: () => {
|
||||
cacheQuery.invalidateQueries({
|
||||
key:['Setting']
|
||||
})
|
||||
}
|
||||
})
|
||||
const changeSetting = form.handleSubmit(async (value) => {
|
||||
|
||||
try {
|
||||
await fetchSetting(value)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user