diff --git a/.env.example b/.env.example
index 020338d3..12b0de80 100644
--- a/.env.example
+++ b/.env.example
@@ -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
# ==================================
diff --git a/packages/api/src/modules/auth/service.ts b/packages/api/src/modules/auth/service.ts
new file mode 100644
index 00000000..b8e91f3b
--- /dev/null
+++ b/packages/api/src/modules/auth/service.ts
@@ -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,
+ }
+}
+
diff --git a/packages/api/src/modules/user/index.ts b/packages/api/src/modules/user/index.ts
new file mode 100644
index 00000000..02ab1488
--- /dev/null
+++ b/packages/api/src/modules/user/index.ts
@@ -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)
+
diff --git a/packages/shared/src/chatInfo.ts b/packages/shared/src/chatInfo.ts
index fb5aaae2..f852f9fa 100644
--- a/packages/shared/src/chatInfo.ts
+++ b/packages/shared/src/chatInfo.ts
@@ -2,9 +2,13 @@ export interface robot{
description: string
time: Date,
id: string | number,
- type: string
+ type: string,
+ action:'robot'
}
export interface user{
- description: string, time: Date, id: number | string
+ description: string,
+ time: Date,
+ id: number | string,
+ action:'user'
}
\ No newline at end of file
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 352848db..f7ede7ae 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -20,6 +20,7 @@
},
"dependencies": {
"@tailwindcss/vite": "^4.1.18",
+ "@vee-validate/zod": "^4.15.1",
"@vueuse/core": "^14.1.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -27,7 +28,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"
diff --git a/packages/ui/src/components/form/FormControl.vue b/packages/ui/src/components/form/FormControl.vue
new file mode 100644
index 00000000..b1bc4bfa
--- /dev/null
+++ b/packages/ui/src/components/form/FormControl.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/form/FormDescription.vue b/packages/ui/src/components/form/FormDescription.vue
new file mode 100644
index 00000000..b5a32558
--- /dev/null
+++ b/packages/ui/src/components/form/FormDescription.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/form/FormItem.vue b/packages/ui/src/components/form/FormItem.vue
new file mode 100644
index 00000000..18ceb6ac
--- /dev/null
+++ b/packages/ui/src/components/form/FormItem.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/packages/ui/src/components/form/FormLabel.vue b/packages/ui/src/components/form/FormLabel.vue
new file mode 100644
index 00000000..57eae984
--- /dev/null
+++ b/packages/ui/src/components/form/FormLabel.vue
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/packages/ui/src/components/form/FormMessage.vue b/packages/ui/src/components/form/FormMessage.vue
new file mode 100644
index 00000000..02290243
--- /dev/null
+++ b/packages/ui/src/components/form/FormMessage.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/packages/ui/src/components/form/index.ts b/packages/ui/src/components/form/index.ts
new file mode 100644
index 00000000..1eb05f11
--- /dev/null
+++ b/packages/ui/src/components/form/index.ts
@@ -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"
diff --git a/packages/ui/src/components/form/injectionKeys.ts b/packages/ui/src/components/form/injectionKeys.ts
new file mode 100644
index 00000000..42542a96
--- /dev/null
+++ b/packages/ui/src/components/form/injectionKeys.ts
@@ -0,0 +1,4 @@
+import type { InjectionKey } from "vue"
+
+export const FORM_ITEM_INJECTION_KEY
+ = Symbol() as InjectionKey
diff --git a/packages/ui/src/components/form/useFormField.ts b/packages/ui/src/components/form/useFormField.ts
new file mode 100644
index 00000000..62d86c26
--- /dev/null
+++ b/packages/ui/src/components/form/useFormField.ts
@@ -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 ")
+
+ 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,
+ }
+}
diff --git a/packages/ui/src/components/label/Label.vue b/packages/ui/src/components/label/Label.vue
index 3799c16f..c3f0c6a1 100644
--- a/packages/ui/src/components/label/Label.vue
+++ b/packages/ui/src/components/label/Label.vue
@@ -1,13 +1,13 @@
@@ -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,
)
"
diff --git a/packages/ui/src/components/label/index.ts b/packages/ui/src/components/label/index.ts
index 572c2f01..036e35c3 100644
--- a/packages/ui/src/components/label/index.ts
+++ b/packages/ui/src/components/label/index.ts
@@ -1 +1 @@
-export { default as Label } from './Label.vue'
+export { default as Label } from "./Label.vue"
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 3633263c..1a1015de 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -11,6 +11,7 @@ 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'
diff --git a/packages/web/package.json b/packages/web/package.json
index e5555319..06f0da6f 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -13,14 +13,20 @@
"@mdi/js": "^7.4.47",
"@memoh/shared": "workspace:*",
"@memoh/ui": "workspace:*",
+ "@pinia/colada": "^0.21.1",
"@tailwindcss/vite": "^4.1.18",
+ "@vee-validate/zod": "^4.15.1",
+ "axios": "^1.13.2",
"dotenv": "^17.2.3",
"pinia": "^3.0.4",
+ "pinia-plugin-persistedstate": "^4.7.1",
"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",
diff --git a/packages/web/src/components/Sidebar/index.vue b/packages/web/src/components/Sidebar/index.vue
index 2bf45b4a..64363fab 100644
--- a/packages/web/src/components/Sidebar/index.vue
+++ b/packages/web/src/components/Sidebar/index.vue
@@ -49,7 +49,18 @@
-
+
+
+
+
+
+
+
@@ -67,13 +78,15 @@ import {
SidebarMenuItem,
SidebarRail,
CollapsibleTrigger,
- Collapsible
-
+ Collapsible,
+ Button
} from '@memoh/ui'
import { reactive } 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'
+
const router = useRouter()
@@ -102,5 +115,11 @@ const sidebarInfo = reactive([{
title: '平台',
name: 'platform',
icon: mdiBookArrowDown
-}])
+ }])
+
+ const {exitLogin}=useUserStore()
+const exit = () => {
+ exitLogin()
+ router.replace({name:'Login'})
+}
\ No newline at end of file
diff --git a/packages/web/src/main.ts b/packages/web/src/main.ts
index 23d5578d..8c83933d 100644
--- a/packages/web/src/main.ts
+++ b/packages/web/src/main.ts
@@ -1,12 +1,16 @@
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 { PiniaColada } from '@pinia/colada'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
createApp(App)
- .use(createPinia())
+ .use(createPinia().use(piniaPluginPersistedstate))
+ .use(PiniaColada)
.use(router)
.use(i18n)
.mount('#app')
diff --git a/packages/web/src/pages/login/index.vue b/packages/web/src/pages/login/index.vue
index 3e997de5..6a603305 100644
--- a/packages/web/src/pages/login/index.vue
+++ b/packages/web/src/pages/login/index.vue
@@ -1,8 +1,9 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
@@ -75,23 +107,57 @@ import {
CardContent,
CardFooter,
Input,
- Label,
- Button
+
+ 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'
+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 = async () => {
- // 先模拟一下数据
- localStorage.setItem('token','afewfewf')
- await router.push('/main')
- console.log('登录')
-}
-
-
+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
+ }
+
+})
\ No newline at end of file
diff --git a/packages/web/src/router.ts b/packages/web/src/router.ts
index 52efcfbf..f2828b26 100644
--- a/packages/web/src/router.ts
+++ b/packages/web/src/router.ts
@@ -75,7 +75,7 @@ router.beforeEach((to) => {
if (to.fullPath !== '/login') {
return token ? true : { name: 'Login' }
} else {
- return token ? { name: 'Main' } : true
+ return token ? { path:'Main' } : true
}
})
diff --git a/packages/web/src/store/User.ts b/packages/web/src/store/User.ts
new file mode 100644
index 00000000..3fc3a1ca
--- /dev/null
+++ b/packages/web/src/store/User.ts
@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { reactive } from 'vue'
+
+
+type user={
+ 'id': string,
+ 'username': string,
+ 'role': string,
+ 'displayName': string
+}
+export const useUserStore = defineStore('user', () => {
+ const userInfo = reactive({
+ 'id': '',
+ 'username': '',
+ 'role': '',
+ 'displayName': ''
+ })
+
+
+ const login = (userData: user,token:string) => {
+ localStorage.setItem('token',token)
+ for (const key of Object.keys(userData) as (keyof user)[]) {
+ userInfo[key] = userData[key]
+ }
+ }
+
+ const exitLogin = () => {
+ localStorage.removeItem('token')
+ for (const key of Object.keys(userInfo) as (keyof user)[]) {
+ userInfo[key]=''
+ }
+ }
+ return {
+ userInfo,
+ login,
+ exitLogin
+ }
+}, {
+ persist:true
+})
diff --git a/packages/web/src/utils/index.ts b/packages/web/src/utils/index.ts
deleted file mode 100644
index e69de29b..00000000
diff --git a/packages/web/src/utils/request.ts b/packages/web/src/utils/request.ts
new file mode 100644
index 00000000..1174924d
--- /dev/null
+++ b/packages/web/src/utils/request.ts
@@ -0,0 +1,29 @@
+import axios, { type AxiosRequestConfig } from 'axios'
+import { useRouter } from 'vue-router'
+
+const router=useRouter()
+export default (function () {
+ const axiosInstance = axios.create({
+ baseURL:'http://localhost:7002/'
+ })
+
+ axiosInstance.interceptors.request.use((config) => {
+ return config
+ }, (error) => Promise.reject(error))
+
+ axiosInstance.interceptors.response.use((response) => {
+
+ return response
+ }, (error) => {
+ if (error?.status === 401) {
+ router.replace({
+ name:'Login'
+ })
+ }
+ return Promise.reject(error)
+ })
+ return (params: AxiosRequestConfig,isToken=true) => {
+
+ return axiosInstance(params)
+ }
+}())
\ No newline at end of file
diff --git a/packages/web/tsconfig.app.json b/packages/web/tsconfig.app.json
index de5b78af..856416f7 100644
--- a/packages/web/tsconfig.app.json
+++ b/packages/web/tsconfig.app.json
@@ -10,7 +10,12 @@
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
+ "noUncheckedSideEffectImports": true,
+ "typeRoots": ["./node_modules/@types", "./type.d.ts"],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
},
- "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
+ "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "type.d.ts"],
}
diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json
index 1ffef600..f50a26b7 100644
--- a/packages/web/tsconfig.json
+++ b/packages/web/tsconfig.json
@@ -3,5 +3,6 @@
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
- ]
+ ],
+
}
diff --git a/packages/web/type.d.ts b/packages/web/type.d.ts
new file mode 100644
index 00000000..f21acc1e
--- /dev/null
+++ b/packages/web/type.d.ts
@@ -0,0 +1 @@
+declare module '@jamescoyle/vue-icon'
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b545fa1b..12a6dea0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,16 @@ importers:
version: 10.2.0(eslint@9.39.2(jiti@2.6.1))
agent:
+ docs:
+ devDependencies:
+ vitepress:
+ specifier: ^1.6.0
+ version: 1.6.4(@algolia/client-search@5.46.2)(@types/node@25.0.3)(axios@1.13.2)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3)
+ vue:
+ specifier: ^3.5.0
+ version: 3.5.26(typescript@5.9.3)
+
+ packages/agent:
dependencies:
'@ai-sdk/anthropic':
specifier: ^3.0.9
@@ -133,6 +143,9 @@ importers:
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
+ '@vee-validate/zod':
+ specifier: ^4.15.1
+ version: 4.15.1(vue@3.5.26(typescript@5.9.3))(zod@3.25.76)
'@vueuse/core':
specifier: ^14.1.0
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
@@ -154,12 +167,18 @@ importers:
tailwindcss:
specifier: ^4.1.18
version: 4.1.18
+ vee-validate:
+ specifier: ^4.15.1
+ version: 4.15.1(vue@3.5.26(typescript@5.9.3))
vue:
specifier: ^3.5.26
version: 3.5.26(typescript@5.9.3)
vue-sonner:
specifier: ^2.0.9
version: 2.0.9
+ zod:
+ specifier: 3.25.76
+ version: 3.25.76
devDependencies:
'@microsoft/api-extractor':
specifier: ^7.55.2
@@ -218,21 +237,36 @@ importers:
'@memoh/ui':
specifier: workspace:*
version: link:../ui
+ '@pinia/colada':
+ specifier: ^0.21.1
+ version: 0.21.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))
'@tailwindcss/vite':
specifier: ^4.1.18
version: 4.1.18(vite@7.3.0(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0))
+ '@vee-validate/zod':
+ specifier: ^4.15.1
+ version: 4.15.1(vue@3.5.26(typescript@5.9.3))(zod@4.3.5)
+ axios:
+ specifier: ^1.13.2
+ version: 1.13.2
dotenv:
specifier: ^17.2.3
version: 17.2.3
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
+ pinia-plugin-persistedstate:
+ specifier: ^4.7.1
+ version: 4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))
tailwindcss:
specifier: ^4.1.18
version: 4.1.18
tw-animate-css:
specifier: ^1.4.0
version: 1.4.0
+ vee-validate:
+ specifier: ^4.15.1
+ version: 4.15.1(vue@3.5.26(typescript@5.9.3))
vue:
specifier: ^3.5.24
version: 3.5.26(typescript@5.9.3)
@@ -242,6 +276,9 @@ importers:
vue-router:
specifier: ^4.6.4
version: 4.6.4(vue@3.5.26(typescript@5.9.3))
+ zod:
+ specifier: ^4.3.5
+ version: 4.3.5
devDependencies:
'@types/node':
specifier: ^24.10.1
@@ -1465,6 +1502,16 @@ packages:
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'}
+ '@pinia/colada@0.21.1':
+ resolution: {integrity: sha512-VU/XBJ6ayFB2hDltE2hM/T+3gy0i/5tiej4VQLFjOic0svVjmQb7m9re5MEd9ixfvM6s8+MCexVNe9x5bgjbZg==}
+ peerDependencies:
+ pinia: ^2.2.6 || ^3.0.0
+ vue: ^3.5.17
+
+ '@pkgjs/parseargs@0.11.0':
+ resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
+ engines: {node: '>=14'}
+
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
@@ -1871,6 +1918,11 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ '@vee-validate/zod@4.15.1':
+ resolution: {integrity: sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==}
+ peerDependencies:
+ zod: ^3.24.0
+
'@vercel/oidc@3.1.0':
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
engines: {node: '>= 20'}
@@ -2168,6 +2220,9 @@ packages:
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+ axios@1.13.2:
+ resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
+
axios@1.7.7:
resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==}
@@ -3269,6 +3324,20 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
+ pinia-plugin-persistedstate@4.7.1:
+ resolution: {integrity: sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ==}
+ peerDependencies:
+ '@nuxt/kit': '>=3.0.0'
+ '@pinia/nuxt': '>=0.10.0'
+ pinia: '>=3.0.0'
+ peerDependenciesMeta:
+ '@nuxt/kit':
+ optional: true
+ '@pinia/nuxt':
+ optional: true
+ pinia:
+ optional: true
+
pinia@3.0.4:
resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==}
peerDependencies:
@@ -3626,6 +3695,9 @@ packages:
type-is@2.0.1:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
+ type-fest@4.41.0:
+ resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
+ engines: {node: '>=16'}
typescript-eslint@8.52.0:
resolution: {integrity: sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA==}
@@ -3733,6 +3805,10 @@ packages:
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
+ vee-validate@4.15.1:
+ resolution: {integrity: sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==}
+ peerDependencies:
+ vue: ^3.4.26
vfile-message@4.0.3:
resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==}
@@ -5077,6 +5153,14 @@ snapshots:
'@opentelemetry/api@1.9.0': {}
+ '@pinia/colada@0.21.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)))(vue@3.5.26(typescript@5.9.3))':
+ dependencies:
+ pinia: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
+ vue: 3.5.26(typescript@5.9.3)
+
+ '@pkgjs/parseargs@0.11.0':
+ optional: true
+
'@polka/url@1.0.0-next.29': {}
'@rolldown/pluginutils@1.0.0-beta.53': {}
@@ -5471,6 +5555,22 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
+ '@vee-validate/zod@4.15.1(vue@3.5.26(typescript@5.9.3))(zod@3.25.76)':
+ dependencies:
+ type-fest: 4.41.0
+ vee-validate: 4.15.1(vue@3.5.26(typescript@5.9.3))
+ zod: 3.25.76
+ transitivePeerDependencies:
+ - vue
+
+ '@vee-validate/zod@4.15.1(vue@3.5.26(typescript@5.9.3))(zod@4.3.5)':
+ dependencies:
+ type-fest: 4.41.0
+ vee-validate: 4.15.1(vue@3.5.26(typescript@5.9.3))
+ zod: 4.3.5
+ transitivePeerDependencies:
+ - vue
+
'@vercel/oidc@3.1.0': {}
'@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.0.3)(lightningcss@1.30.2))(vue@3.5.26(typescript@5.9.3))':
@@ -5695,13 +5795,13 @@ snapshots:
'@vueuse/shared': 14.1.0(vue@3.5.26(typescript@5.9.3))
vue: 3.5.26(typescript@5.9.3)
- '@vueuse/integrations@12.8.2(axios@1.7.7)(focus-trap@7.8.0)(typescript@5.9.3)':
+ '@vueuse/integrations@12.8.2(axios@1.13.2)(focus-trap@7.8.0)(typescript@5.9.3)':
dependencies:
'@vueuse/core': 12.8.2(typescript@5.9.3)
'@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.26(typescript@5.9.3)
optionalDependencies:
- axios: 1.7.7
+ axios: 1.13.2
focus-trap: 7.8.0
transitivePeerDependencies:
- typescript
@@ -5827,6 +5927,14 @@ snapshots:
asynckit@0.4.0:
optional: true
+ axios@1.13.2:
+ dependencies:
+ follow-redirects: 1.15.11
+ form-data: 4.0.5
+ proxy-from-env: 1.1.0
+ transitivePeerDependencies:
+ - debug
+
axios@1.7.7:
dependencies:
follow-redirects: 1.15.11
@@ -6941,6 +7049,12 @@ snapshots:
pidtree@0.6.0: {}
+ pinia-plugin-persistedstate@4.7.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))):
+ dependencies:
+ defu: 6.1.4
+ optionalDependencies:
+ pinia: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
+
pinia@3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 7.7.9
@@ -7338,6 +7452,7 @@ snapshots:
content-type: 1.0.5
media-typer: 1.1.0
mime-types: 3.0.2
+ type-fest@4.41.0: {}
typescript-eslint@8.52.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies:
@@ -7434,6 +7549,11 @@ snapshots:
util-deprecate@1.0.2: {}
vary@1.1.2: {}
+ vee-validate@4.15.1(vue@3.5.26(typescript@5.9.3)):
+ dependencies:
+ '@vue/devtools-api': 7.7.9
+ type-fest: 4.41.0
+ vue: 3.5.26(typescript@5.9.3)
vfile-message@4.0.3:
dependencies:
@@ -7539,7 +7659,7 @@ snapshots:
lightningcss: 1.30.2
tsx: 4.21.0
- vitepress@1.6.4(@algolia/client-search@5.46.2)(@types/node@25.0.3)(axios@1.7.7)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3):
+ vitepress@1.6.4(@algolia/client-search@5.46.2)(@types/node@25.0.3)(axios@1.13.2)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3)(typescript@5.9.3):
dependencies:
'@docsearch/css': 3.8.2
'@docsearch/js': 3.8.2(@algolia/client-search@5.46.2)(search-insights@2.17.3)
@@ -7552,7 +7672,7 @@ snapshots:
'@vue/devtools-api': 7.7.9
'@vue/shared': 3.5.26
'@vueuse/core': 12.8.2(typescript@5.9.3)
- '@vueuse/integrations': 12.8.2(axios@1.7.7)(focus-trap@7.8.0)(typescript@5.9.3)
+ '@vueuse/integrations': 12.8.2(axios@1.13.2)(focus-trap@7.8.0)(typescript@5.9.3)
focus-trap: 7.8.0
mark.js: 8.11.1
minisearch: 7.2.0