From 5d19dbf430f7df6103f0cce6ad84c4338ae8a201 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Wang?=
<52880665+rivertwilight@users.noreply.github.com>
Date: Wed, 4 Mar 2026 19:37:31 +0800
Subject: [PATCH] fix: Move email contact (#12323)
* fix: Move email contact
* style: profile
* fix: urk
* fix: urk
* feat: loading indicator
* fix: build error
* fix: sort
* fix: sort
* fix: sort
* fix: sort
* fix: sort
---
.github/ISSUE_TEMPLATE/1_bug_report.yml | 8 +-
locales/en-US/common.json | 5 +-
locales/zh-CN/common.json | 5 +-
.../database/src/repositories/search/index.ts | 57 +++-
src/components/FeedbackModal/index.tsx | 5 +
src/features/CommandMenu/SearchResults.tsx | 19 ++
src/features/CommandMenu/index.tsx | 4 +-
src/features/CommandMenu/utils/queryParser.ts | 1 +
src/locales/default/auth.ts | 4 -
src/locales/default/common.ts | 7 +-
.../(home)/features/CreatorRewardBanner.tsx | 2 +-
.../(main)/home/_layout/Footer/index.tsx | 14 +-
.../settings/profile/features/AvatarRow.tsx | 75 +++--
.../settings/profile/features/FullNameRow.tsx | 112 ++-----
.../profile/features/InterestsRow.tsx | 295 +++++++-----------
.../settings/profile/features/PasswordRow.tsx | 41 +--
.../settings/profile/features/ProfileRow.tsx | 10 +-
.../settings/profile/features/UsernameRow.tsx | 160 +++++-----
src/routes/(main)/settings/profile/index.tsx | 14 +-
src/server/routers/lambda/search.ts | 3 +-
20 files changed, 397 insertions(+), 444 deletions(-)
diff --git a/.github/ISSUE_TEMPLATE/1_bug_report.yml b/.github/ISSUE_TEMPLATE/1_bug_report.yml
index 4bb6a2b745..c10a040dad 100644
--- a/.github/ISSUE_TEMPLATE/1_bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/1_bug_report.yml
@@ -68,19 +68,19 @@ body:
- type: textarea
attributes:
- label: '🐛 Bug Description'
+ label: '🐛 What happened?'
description: A clear and concise description of the bug, if the above option is `Other`, please also explain in detail.
validations:
required: true
- type: textarea
attributes:
- label: '📷 Recurrence Steps'
- description: A clear and concise description of how to recurrence.
+ label: '📷 How to reproduce it?'
+ description: A clear and concise description of how to reproduce.
- type: textarea
attributes:
- label: '🚦 Expected Behavior'
+ label: '🚦 What it should be?'
description: A clear and concise description of what you expected to happen.
- type: textarea
diff --git a/locales/en-US/common.json b/locales/en-US/common.json
index 70ed841a3a..679ad317be 100644
--- a/locales/en-US/common.json
+++ b/locales/en-US/common.json
@@ -166,6 +166,8 @@
"cmdk.search.files": "Files",
"cmdk.search.folder": "Folder",
"cmdk.search.folders": "Folders",
+ "cmdk.search.knowledgeBase": "Library",
+ "cmdk.search.knowledgeBases": "Libraries",
"cmdk.search.loading": "Searching...",
"cmdk.search.market": "Community",
"cmdk.search.mcp": "MCP Server",
@@ -221,6 +223,7 @@
"exportType.allAgentWithMessage": "Export All Agents and Messages",
"exportType.globalSetting": "Export Global Settings",
"feedback": "Feedback",
+ "feedback.emailContact": "You can also email us at {{email}}",
"feedback.errors.fileTooLarge": "File exceeds 5MB",
"feedback.errors.submitFailed": "Submit failed. Try again.",
"feedback.errors.teamNotFound": "Configuration error",
@@ -423,7 +426,7 @@
"userPanel.community": "Community",
"userPanel.data": "Data Storage",
"userPanel.defaultNickname": "Community User",
- "userPanel.discord": "Community Support",
+ "userPanel.discord": "Discord",
"userPanel.docs": "Documentation",
"userPanel.email": "Email Support",
"userPanel.feedback": "Contact Us",
diff --git a/locales/zh-CN/common.json b/locales/zh-CN/common.json
index 0d42486dca..3367fe0a7e 100644
--- a/locales/zh-CN/common.json
+++ b/locales/zh-CN/common.json
@@ -166,6 +166,8 @@
"cmdk.search.files": "文件",
"cmdk.search.folder": "文件夹",
"cmdk.search.folders": "文件夹",
+ "cmdk.search.knowledgeBase": "知识库",
+ "cmdk.search.knowledgeBases": "知识库",
"cmdk.search.loading": "搜索中…",
"cmdk.search.market": "社区",
"cmdk.search.mcp": "MCP 服务器",
@@ -221,6 +223,7 @@
"exportType.allAgentWithMessage": "导出所有助理和消息",
"exportType.globalSetting": "导出全局设置",
"feedback": "反馈与建议",
+ "feedback.emailContact": "您也可以发送邮件至 {{email}}",
"feedback.errors.fileTooLarge": "文件大小超过 5MB 限制",
"feedback.errors.submitFailed": "提交失败,请重试",
"feedback.errors.teamNotFound": "配置错误",
@@ -423,7 +426,7 @@
"userPanel.community": "社区版",
"userPanel.data": "数据存储",
"userPanel.defaultNickname": "社区版用户",
- "userPanel.discord": "社区支持",
+ "userPanel.discord": "Discord",
"userPanel.docs": "使用文档",
"userPanel.email": "邮件支持",
"userPanel.feedback": "联系我们",
diff --git a/packages/database/src/repositories/search/index.ts b/packages/database/src/repositories/search/index.ts
index be18cb2abd..d0277801e3 100644
--- a/packages/database/src/repositories/search/index.ts
+++ b/packages/database/src/repositories/search/index.ts
@@ -5,6 +5,7 @@ import {
documents,
files,
knowledgeBaseFiles,
+ knowledgeBases,
messages,
topics,
userMemories,
@@ -22,7 +23,8 @@ export type SearchResultType =
| 'message'
| 'mcp'
| 'plugin'
- | 'communityAgent';
+ | 'communityAgent'
+ | 'knowledgeBase';
export interface BaseSearchResult {
// 1=exact, 2=prefix, 3=contains
@@ -111,6 +113,11 @@ export interface PluginSearchResult extends BaseSearchResult {
type: 'plugin';
}
+export interface KnowledgeBaseSearchResult extends BaseSearchResult {
+ avatar: string | null;
+ type: 'knowledgeBase';
+}
+
export interface AssistantSearchResult extends BaseSearchResult {
author: string;
avatar?: string | null;
@@ -131,7 +138,8 @@ export type SearchResult =
| MemorySearchResult
| MCPSearchResult
| PluginSearchResult
- | AssistantSearchResult;
+ | AssistantSearchResult
+ | KnowledgeBaseSearchResult;
export interface SearchOptions {
agentId?: string;
@@ -192,6 +200,9 @@ export class SearchRepo {
if ((!type || type === 'memory') && limits.memory > 0) {
searchPromises.push(this.searchMemories(trimmedQuery, limits.memory));
}
+ if ((!type || type === 'knowledgeBase') && limits.knowledgeBase > 0) {
+ searchPromises.push(this.searchKnowledgeBases(trimmedQuery, limits.knowledgeBase));
+ }
const results = await Promise.all(searchPromises);
@@ -213,6 +224,7 @@ export class SearchRepo {
agent: number;
file: number;
folder: number;
+ knowledgeBase: number;
memory: number;
message: number;
page: number;
@@ -225,6 +237,7 @@ export class SearchRepo {
agent: type === 'agent' ? baseLimit : 0,
file: type === 'file' ? baseLimit : 0,
folder: type === 'folder' ? baseLimit : 0,
+ knowledgeBase: type === 'knowledgeBase' ? baseLimit : 0,
memory: type === 'memory' ? baseLimit : 0,
message: type === 'message' ? baseLimit : 0,
page: type === 'page' ? baseLimit : 0,
@@ -239,6 +252,7 @@ export class SearchRepo {
agent: 3,
file: 3,
folder: 3,
+ knowledgeBase: 3,
memory: 3,
message: 3,
page: 6,
@@ -253,6 +267,7 @@ export class SearchRepo {
agent: 3,
file: 6,
folder: 6,
+ knowledgeBase: 6,
memory: 3,
message: 3,
page: 3,
@@ -267,6 +282,7 @@ export class SearchRepo {
agent: 3,
file: 3,
folder: 3,
+ knowledgeBase: 3,
memory: 3,
message: 6,
page: 3,
@@ -280,6 +296,7 @@ export class SearchRepo {
agent: 3,
file: 3,
folder: 3,
+ knowledgeBase: 3,
memory: 3,
message: 3,
page: 3,
@@ -614,4 +631,40 @@ export class SearchRepo {
updatedAt: row.updatedAt,
}));
}
+
+ /**
+ * Search knowledge bases by name and description
+ */
+ private async searchKnowledgeBases(
+ query: string,
+ limit: number,
+ ): Promise {
+ const searchTerm = `%${query}%`;
+
+ const rows = await this.db
+ .select()
+ .from(knowledgeBases)
+ .where(
+ and(
+ eq(knowledgeBases.userId, this.userId),
+ or(
+ ilike(knowledgeBases.name, searchTerm),
+ ilike(sql`COALESCE(${knowledgeBases.description}, '')`, searchTerm),
+ ),
+ ),
+ )
+ .orderBy(desc(knowledgeBases.updatedAt))
+ .limit(limit);
+
+ return rows.map((row) => ({
+ avatar: row.avatar,
+ createdAt: row.createdAt,
+ description: row.description,
+ id: row.id,
+ relevance: this.calculateRelevance(row.name, query),
+ title: row.name,
+ type: 'knowledgeBase' as const,
+ updatedAt: row.updatedAt,
+ }));
+ }
}
diff --git a/src/components/FeedbackModal/index.tsx b/src/components/FeedbackModal/index.tsx
index bf70ddfd3b..f41a788ba5 100644
--- a/src/components/FeedbackModal/index.tsx
+++ b/src/components/FeedbackModal/index.tsx
@@ -1,5 +1,6 @@
'use client';
+import { BRANDING_EMAIL } from '@lobechat/business-const';
import { Button, Flexbox, Icon, Modal } from '@lobehub/ui';
import { App, Form, Input, Upload } from 'antd';
import { ImagePlus, Send } from 'lucide-react';
@@ -126,6 +127,10 @@ const FeedbackModal = memo(({ initialValues, onClose, open }
}
onCancel={handleCancel}
>
+
+ {t('feedback.emailContact', { email: BRANDING_EMAIL.business })}
+
+
(
navigate(`/memory/preferences?preferenceId=${result.id}`);
break;
}
+ case 'knowledgeBase': {
+ navigate(`/resource/library/${result.id}`);
+ break;
+ }
}
onClose();
};
@@ -147,6 +152,9 @@ const SearchResults = memo(
case 'memory': {
return ;
}
+ case 'knowledgeBase': {
+ return ;
+ }
}
};
@@ -182,6 +190,9 @@ const SearchResults = memo(
case 'memory': {
return t('cmdk.search.memory');
}
+ case 'knowledgeBase': {
+ return t('cmdk.search.knowledgeBase');
+ }
}
};
@@ -232,6 +243,7 @@ const SearchResults = memo(
const memoryResults = results.filter((r) => r.type === 'memory');
const mcpResults = results.filter((r) => r.type === 'mcp');
const pluginResults = results.filter((r) => r.type === 'plugin');
+ const knowledgeBaseResults = results.filter((r) => r.type === 'knowledgeBase');
const assistantResults = results.filter((r) => r.type === 'communityAgent');
// Don't render anything if no results and not loading
@@ -361,6 +373,13 @@ const SearchResults = memo(
)}
+ {knowledgeBaseResults.length > 0 && (
+
+ {knowledgeBaseResults.map((result) => renderResultItem(result))}
+ {renderSearchMore('knowledgeBase', knowledgeBaseResults.length)}
+
+ )}
+
{mcpResults.length > 0 && (
{mcpResults.map((result) => renderResultItem(result))}
diff --git a/src/features/CommandMenu/index.tsx b/src/features/CommandMenu/index.tsx
index c3fdfa4bea..8be61fcee2 100644
--- a/src/features/CommandMenu/index.tsx
+++ b/src/features/CommandMenu/index.tsx
@@ -114,7 +114,9 @@ const CommandMenuContent = memo(({ isClosing, onClose }
- {!(hasSearch && (isSearching || searchResults.length > 0)) && (
+ {/* Hide cmdk's Empty when we have search results or are loading them,
+ since force-mounted items aren't counted by cmdk's internal filter */}
+ {!(hasSearch && (searchResults.length > 0 || isSearching)) && (
{t('cmdk.noResults')}
)}
diff --git a/src/features/CommandMenu/utils/queryParser.ts b/src/features/CommandMenu/utils/queryParser.ts
index a1b89d973c..3107a5bee1 100644
--- a/src/features/CommandMenu/utils/queryParser.ts
+++ b/src/features/CommandMenu/utils/queryParser.ts
@@ -22,6 +22,7 @@ const VALID_TYPES = [
'mcp',
'plugin',
'communityAgent',
+ 'knowledgeBase',
] as const;
export type ValidSearchType = (typeof VALID_TYPES)[number];
diff --git a/src/locales/default/auth.ts b/src/locales/default/auth.ts
index 77673bb6a5..ed993efccf 100644
--- a/src/locales/default/auth.ts
+++ b/src/locales/default/auth.ts
@@ -177,10 +177,7 @@ export default {
'profile.emailInvalid': 'Please enter a valid email address',
'profile.emailPlaceholder': 'new-email@example.com',
'profile.fullName': 'Fullname',
- 'profile.fullNameInputHint': 'Please enter your new fullname',
'profile.interests': 'Interests',
- 'profile.interestsAdd': 'Add',
- 'profile.interestsPlaceholder': 'Enter an interest',
'profile.password': 'Password',
'profile.resetPasswordError': 'Failed to send password reset link',
'profile.resetPasswordSent': 'Password reset link sent, please check your email',
@@ -202,7 +199,6 @@ export default {
'profile.updateUsername': 'Update username',
'profile.username': 'Username',
'profile.usernameDuplicate': 'Username is already taken',
- 'profile.usernameInputHint': 'Please enter your new username',
'profile.usernamePlaceholder': 'Enter a username with letters, numbers, or underscores',
'profile.usernameRequired': 'Username cannot be empty',
'profile.usernameRule': 'Username can only contain letters, numbers, or underscores',
diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts
index ea2b525fc1..19cc9688b1 100644
--- a/src/locales/default/common.ts
+++ b/src/locales/default/common.ts
@@ -229,6 +229,10 @@ export default {
'cmdk.search.folders': 'Folders',
+ 'cmdk.search.knowledgeBase': 'Library',
+
+ 'cmdk.search.knowledgeBases': 'Libraries',
+
'cmdk.search.loading': 'Searching...',
'cmdk.search.market': 'Community',
@@ -290,6 +294,7 @@ export default {
'exportType.allAgentWithMessage': 'Export All Agents and Messages',
'exportType.globalSetting': 'Export Global Settings',
'feedback': 'Feedback',
+ 'feedback.emailContact': 'You can also email us at {{email}}',
'feedback.errors.fileTooLarge': 'File exceeds 5MB',
'feedback.errors.submitFailed': 'Submit failed. Try again.',
'feedback.errors.teamNotFound': 'Configuration error',
@@ -499,7 +504,7 @@ export default {
'userPanel.community': 'Community',
'userPanel.data': 'Data Storage',
'userPanel.defaultNickname': 'Community User',
- 'userPanel.discord': 'Community Support',
+ 'userPanel.discord': 'Discord',
'userPanel.docs': 'Documentation',
'userPanel.email': 'Email Support',
'userPanel.feedback': 'Contact Us',
diff --git a/src/routes/(main)/community/(list)/(home)/features/CreatorRewardBanner.tsx b/src/routes/(main)/community/(list)/(home)/features/CreatorRewardBanner.tsx
index 3860a5ba89..2bf3fd7d05 100644
--- a/src/routes/(main)/community/(list)/(home)/features/CreatorRewardBanner.tsx
+++ b/src/routes/(main)/community/(list)/(home)/features/CreatorRewardBanner.tsx
@@ -96,7 +96,7 @@ const CreatorRewardBanner = memo(() => {
{t('home.creatorReward.subtitle')}
diff --git a/src/routes/(main)/home/_layout/Footer/index.tsx b/src/routes/(main)/home/_layout/Footer/index.tsx
index 488ae358e0..aa2f57fee1 100644
--- a/src/routes/(main)/home/_layout/Footer/index.tsx
+++ b/src/routes/(main)/home/_layout/Footer/index.tsx
@@ -1,6 +1,6 @@
'use client';
-import { BRANDING_EMAIL, SOCIAL_URL } from '@lobechat/business-const';
+import { SOCIAL_URL } from '@lobechat/business-const';
import { useAnalytics } from '@lobehub/analytics/react';
import { type MenuProps } from '@lobehub/ui';
import { ActionIcon, DropdownMenu, Flexbox, Icon } from '@lobehub/ui';
@@ -12,7 +12,6 @@ import {
FileClockIcon,
FlaskConical,
Github,
- Mail,
Rocket,
} from 'lucide-react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
@@ -22,7 +21,7 @@ import { Link } from 'react-router-dom';
import ChangelogModal from '@/components/ChangelogModal';
import HighlightNotification from '@/components/HighlightNotification';
import LabsModal from '@/components/LabsModal';
-import { DOCUMENTS_REFER_URL, GITHUB, mailTo } from '@/const/url';
+import { DOCUMENTS_REFER_URL, GITHUB } from '@/const/url';
import ThemeButton from '@/features/User/UserPanel/ThemeButton';
import { useFeedbackModal } from '@/hooks/useFeedbackModal';
import { useGlobalStore } from '@/store/global';
@@ -156,15 +155,6 @@ const Footer = memo(() => {
),
},
- {
- icon: ,
- key: 'email',
- label: (
-
- {t('userPanel.email')}
-
- ),
- },
{
type: 'divider',
},
diff --git a/src/routes/(main)/settings/profile/features/AvatarRow.tsx b/src/routes/(main)/settings/profile/features/AvatarRow.tsx
index b32d68613a..420681d39c 100644
--- a/src/routes/(main)/settings/profile/features/AvatarRow.tsx
+++ b/src/routes/(main)/settings/profile/features/AvatarRow.tsx
@@ -1,8 +1,10 @@
'use client';
import { LoadingOutlined } from '@ant-design/icons';
-import { Flexbox, Text } from '@lobehub/ui';
+import { Flexbox, Icon, Text } from '@lobehub/ui';
import { Spin, Upload } from 'antd';
+import { createStaticStyles, cssVar } from 'antd-style';
+import { PencilIcon } from 'lucide-react';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -15,6 +17,36 @@ import { createUploadImageHandler } from '@/utils/uploadFIle';
import { labelStyle, rowStyle } from './ProfileRow';
+const styles = createStaticStyles(({ css }) => ({
+ overlay: css`
+ cursor: pointer;
+
+ position: absolute;
+ z-index: 1;
+ inset: 0;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ opacity: 0;
+ background: ${cssVar.colorBgMask};
+ border-radius: 8px;
+
+ transition: opacity ${cssVar.motionDurationMid} ease;
+ `,
+ wrapper: css`
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+ border-radius: 8px;
+
+ &:hover .avatar-edit-overlay {
+ opacity: 1;
+ }
+ `,
+}));
+
interface AvatarRowProps {
mobile?: boolean;
}
@@ -56,42 +88,33 @@ const AvatarRow = ({ mobile }: AvatarRowProps) => {
const canUpload = isLogin;
const avatarContent = canUpload ? (
- } spinning={uploading}>
- void 0} maxCount={1}>
-
-
-
+ void 0} maxCount={1}>
+ } spinning={uploading}>
+
+
+
) : (
);
- const updateAction = canUpload ? (
- void 0} maxCount={1}>
-
- {t('profile.updateAvatar')}
-
-
- ) : null;
-
if (mobile) {
return (
-
-
- {t('profile.avatar')}
- {updateAction}
-
- {avatarContent}
+
+ {t('profile.avatar')}
+ {avatarContent}
);
}
return (
-
-
- {t('profile.avatar')}
- {avatarContent}
-
- {updateAction}
+
+ {t('profile.avatar')}
+ {avatarContent}
);
};
diff --git a/src/routes/(main)/settings/profile/features/FullNameRow.tsx b/src/routes/(main)/settings/profile/features/FullNameRow.tsx
index 03fbe94db1..b586a21971 100644
--- a/src/routes/(main)/settings/profile/features/FullNameRow.tsx
+++ b/src/routes/(main)/settings/profile/features/FullNameRow.tsx
@@ -1,15 +1,16 @@
'use client';
-import { Button, Flexbox, Input, Text } from '@lobehub/ui';
-import { AnimatePresence, m as motion } from 'motion/react';
-import { useCallback, useState } from 'react';
+import { LoadingOutlined } from '@ant-design/icons';
+import { Flexbox, Input, Text } from '@lobehub/ui';
+import { type InputRef, Spin } from 'antd';
+import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { fetchErrorNotification } from '@/components/Error/fetchErrorNotification';
import { useUserStore } from '@/store/user';
import { userProfileSelectors } from '@/store/user/selectors';
-import { labelStyle, rowStyle } from './ProfileRow';
+import { INPUT_WIDTH, labelStyle, rowStyle } from './ProfileRow';
interface FullNameRowProps {
mobile?: boolean;
@@ -19,27 +20,16 @@ const FullNameRow = ({ mobile }: FullNameRowProps) => {
const { t } = useTranslation('auth');
const fullName = useUserStore(userProfileSelectors.fullName);
const updateFullName = useUserStore((s) => s.updateFullName);
- const [isEditing, setIsEditing] = useState(false);
- const [editValue, setEditValue] = useState('');
const [saving, setSaving] = useState(false);
-
- const handleStartEdit = () => {
- setEditValue(fullName || '');
- setIsEditing(true);
- };
-
- const handleCancel = () => {
- setIsEditing(false);
- setEditValue('');
- };
+ const inputRef = useRef(null);
const handleSave = useCallback(async () => {
- if (!editValue.trim()) return;
+ const value = inputRef.current?.input?.value?.trim();
+ if (!value || value === fullName) return;
try {
setSaving(true);
- await updateFullName(editValue.trim());
- setIsEditing(false);
+ await updateFullName(value);
} catch (error) {
console.error('Failed to update fullName:', error);
fetchErrorNotification.error({
@@ -49,81 +39,39 @@ const FullNameRow = ({ mobile }: FullNameRowProps) => {
} finally {
setSaving(false);
}
- }, [editValue, updateFullName]);
+ }, [fullName, updateFullName]);
- const editingContent = (
-
-
- {!mobile && {t('profile.fullNameInputHint')}}
- setEditValue(e.target.value)}
- onPressEnter={handleSave}
- />
-
-
-
-
-
-
- );
-
- const displayContent = (
-
- {mobile ? (
- {fullName || '--'}
- ) : (
-
- {fullName || '--'}
-
- {t('profile.updateFullName')}
-
-
- )}
-
+ const input = (
+
+ {saving && } size="small" />}
+
+
);
if (mobile) {
return (
-
- {t('profile.fullName')}
- {!isEditing && (
-
- {t('profile.updateFullName')}
-
- )}
-
- {isEditing ? editingContent : displayContent}
+ {t('profile.fullName')}
+ {input}
);
}
return (
-
+
{t('profile.fullName')}
-
- {isEditing ? editingContent : displayContent}
+
+ {input}
);
diff --git a/src/routes/(main)/settings/profile/features/InterestsRow.tsx b/src/routes/(main)/settings/profile/features/InterestsRow.tsx
index 3cdaa527d3..866d1e7e0b 100644
--- a/src/routes/(main)/settings/profile/features/InterestsRow.tsx
+++ b/src/routes/(main)/settings/profile/features/InterestsRow.tsx
@@ -1,9 +1,8 @@
'use client';
-import { Block, Button, Flexbox, Icon, Input, Tag, Text } from '@lobehub/ui';
+import { Block, Flexbox, Icon, Input, Text } from '@lobehub/ui';
import { cssVar } from 'antd-style';
import { BriefcaseIcon } from 'lucide-react';
-import { AnimatePresence, m as motion } from 'motion/react';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -23,8 +22,6 @@ const InterestsRow = ({ mobile }: InterestsRowProps) => {
const { t: tOnboarding } = useTranslation('onboarding');
const interests = useUserStore(userProfileSelectors.interests);
const updateInterests = useUserStore((s) => s.updateInterests);
- const [isEditing, setIsEditing] = useState(false);
- const [selectedInterests, setSelectedInterests] = useState([]);
const [customInput, setCustomInput] = useState('');
const [showCustomInput, setShowCustomInput] = useState(false);
const [saving, setSaving] = useState(false);
@@ -38,50 +35,38 @@ const InterestsRow = ({ mobile }: InterestsRowProps) => {
[tOnboarding],
);
- const handleStartEdit = () => {
- setSelectedInterests([...interests]);
- setIsEditing(true);
- };
+ const toggleInterest = useCallback(
+ async (label: string) => {
+ const updated = interests.includes(label)
+ ? interests.filter((i) => i !== label)
+ : [...interests, label];
- const handleCancel = () => {
- setIsEditing(false);
- setSelectedInterests([]);
- setCustomInput('');
- setShowCustomInput(false);
- };
+ try {
+ setSaving(true);
+ await updateInterests(updated);
+ } catch (error) {
+ console.error('Failed to update interests:', error);
+ fetchErrorNotification.error({
+ errorMessage: error instanceof Error ? error.message : String(error),
+ status: 500,
+ });
+ } finally {
+ setSaving(false);
+ }
+ },
+ [interests, updateInterests],
+ );
- const toggleInterest = useCallback((label: string) => {
- setSelectedInterests((prev) =>
- prev.includes(label) ? prev.filter((i) => i !== label) : [...prev, label],
- );
- }, []);
-
- const handleAddCustom = useCallback(() => {
+ const handleAddCustom = useCallback(async () => {
const trimmed = customInput.trim();
- if (trimmed && !selectedInterests.includes(trimmed)) {
- setSelectedInterests((prev) => [...prev, trimmed]);
- setCustomInput('');
- }
- }, [customInput, selectedInterests]);
+ if (!trimmed || interests.includes(trimmed)) return;
- const handleSave = useCallback(async () => {
- // Include custom input if has content
- const finalInterests = [...selectedInterests];
- const trimmedCustom = customInput.trim();
- if (showCustomInput && trimmedCustom && !finalInterests.includes(trimmedCustom)) {
- finalInterests.push(trimmedCustom);
- }
-
- // Deduplicate
- const uniqueInterests = [...new Set(finalInterests)];
+ const updated = [...interests, trimmed];
+ setCustomInput('');
try {
setSaving(true);
- await updateInterests(uniqueInterests);
- setIsEditing(false);
- setSelectedInterests([]);
- setCustomInput('');
- setShowCustomInput(false);
+ await updateInterests(updated);
} catch (error) {
console.error('Failed to update interests:', error);
fetchErrorNotification.error({
@@ -91,155 +76,97 @@ const InterestsRow = ({ mobile }: InterestsRowProps) => {
} finally {
setSaving(false);
}
- }, [selectedInterests, customInput, showCustomInput, updateInterests]);
+ }, [customInput, interests, updateInterests]);
- const editingContent = (
-
-
-
- {areas.map((item) => {
- const isSelected = selectedInterests.includes(item.label);
- return (
- toggleInterest(item.label)}
- >
-
-
- {item.label}
-
-
- );
- })}
- {/* Render custom interests with same Block style but no icon */}
- {selectedInterests
- .filter((i) => !areas.some((a) => a.label === i))
- .map((interest) => (
- toggleInterest(interest)}
- >
-
- {interest}
-
-
- ))}
- setShowCustomInput(!showCustomInput)}
- >
-
-
- {tOnboarding('interests.area.other')}
-
-
-
- {showCustomInput && (
- setCustomInput(e.target.value)}
- onPressEnter={handleAddCustom}
- />
- )}
-
-
-
-
-
-
- );
-
- const displayContent = (
-
- {mobile ? (
- interests.length > 0 ? (
-
- {interests.map((interest) => (
- {interest}
- ))}
-
- ) : (
- --
- )
- ) : (
-
- {interests.length > 0 ? (
-
- {interests.map((interest) => (
- {interest}
- ))}
-
- ) : (
- --
- )}
-
- {t('profile.updateInterests')}
+ const content = (
+
+
+ {areas.map((item) => {
+ const isSelected = interests.includes(item.label);
+ return (
+ !saving && toggleInterest(item.label)}
+ >
+
+
+ {item.label}
+
+
+ );
+ })}
+ {/* Render custom interests */}
+ {interests
+ .filter((i) => !areas.some((a) => a.label === i))
+ .map((interest) => (
+ !saving && toggleInterest(interest)}
+ >
+
+ {interest}
+
+
+ ))}
+ setShowCustomInput(!showCustomInput)}
+ >
+
+
+ {tOnboarding('interests.area.other')}
-
+
+
+ {showCustomInput && (
+ setCustomInput(e.target.value)}
+ onPressEnter={handleAddCustom}
+ />
)}
-
+
);
if (mobile) {
return (
-
- {t('profile.interests')}
- {!isEditing && (
-
- {t('profile.updateInterests')}
-
- )}
-
- {isEditing ? editingContent : displayContent}
+ {t('profile.interests')}
+ {content}
);
}
@@ -247,9 +174,7 @@ const InterestsRow = ({ mobile }: InterestsRowProps) => {
return (
{t('profile.interests')}
-
- {isEditing ? editingContent : displayContent}
-
+ {content}
);
};
diff --git a/src/routes/(main)/settings/profile/features/PasswordRow.tsx b/src/routes/(main)/settings/profile/features/PasswordRow.tsx
index afca5957ac..8a4d592fdd 100644
--- a/src/routes/(main)/settings/profile/features/PasswordRow.tsx
+++ b/src/routes/(main)/settings/profile/features/PasswordRow.tsx
@@ -1,6 +1,6 @@
'use client';
-import { Text } from '@lobehub/ui';
+import { Button, Flexbox, Text } from '@lobehub/ui';
import { useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,7 +8,7 @@ import { notification } from '@/components/AntdStaticMethods';
import { useUserStore } from '@/store/user';
import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
-import ProfileRow from './ProfileRow';
+import { labelStyle, rowStyle } from './ProfileRow';
interface PasswordRowProps {
mobile?: boolean;
@@ -43,25 +43,26 @@ const PasswordRow = ({ mobile }: PasswordRowProps) => {
}
}, [userProfile?.email, t]);
- return (
-
+ if (mobile) {
+ return (
+
+ {t('profile.password')}
+
+
+
+ );
+ }
+
+ return (
+
+ {t('profile.password')}
+
+
+
+
);
};
diff --git a/src/routes/(main)/settings/profile/features/ProfileRow.tsx b/src/routes/(main)/settings/profile/features/ProfileRow.tsx
index a9bac6ec2f..c06b988bb8 100644
--- a/src/routes/(main)/settings/profile/features/ProfileRow.tsx
+++ b/src/routes/(main)/settings/profile/features/ProfileRow.tsx
@@ -20,6 +20,8 @@ export const labelStyle: CSSProperties = {
width: 160,
};
+export const INPUT_WIDTH = 240;
+
const ProfileRow = ({ label, children, action, mobile }: ProfileRowProps) => {
if (mobile) {
return (
@@ -34,11 +36,9 @@ const ProfileRow = ({ label, children, action, mobile }: ProfileRowProps) => {
}
return (
-
-
- {label}
- {children}
-
+
+ {label}
+ {children}
{action && {action}}
);
diff --git a/src/routes/(main)/settings/profile/features/UsernameRow.tsx b/src/routes/(main)/settings/profile/features/UsernameRow.tsx
index 470a4cba26..3ca5279742 100644
--- a/src/routes/(main)/settings/profile/features/UsernameRow.tsx
+++ b/src/routes/(main)/settings/profile/features/UsernameRow.tsx
@@ -1,15 +1,16 @@
'use client';
+import { LoadingOutlined } from '@ant-design/icons';
import { Button, Flexbox, Input, Text } from '@lobehub/ui';
-import { AnimatePresence, m as motion } from 'motion/react';
+import { type InputRef, Spin } from 'antd';
import { type ChangeEvent } from 'react';
-import { useCallback, useState } from 'react';
+import { useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useUserStore } from '@/store/user';
import { userProfileSelectors } from '@/store/user/selectors';
-import { labelStyle, rowStyle } from './ProfileRow';
+import { INPUT_WIDTH, labelStyle, rowStyle } from './ProfileRow';
interface UsernameRowProps {
mobile?: boolean;
@@ -19,25 +20,13 @@ const UsernameRow = ({ mobile }: UsernameRowProps) => {
const { t } = useTranslation('auth');
const username = useUserStore(userProfileSelectors.username);
const updateUsername = useUserStore((s) => s.updateUsername);
- const [isEditing, setIsEditing] = useState(false);
- const [editValue, setEditValue] = useState('');
const [saving, setSaving] = useState(false);
const [error, setError] = useState('');
+ const [dirty, setDirty] = useState(false);
+ const inputRef = useRef(null);
const usernameRegex = /^\w+$/;
- const handleStartEdit = () => {
- setEditValue(username || '');
- setError('');
- setIsEditing(true);
- };
-
- const handleCancel = () => {
- setIsEditing(false);
- setEditValue('');
- setError('');
- };
-
const validateUsername = (value: string): string => {
const trimmed = value.trim();
if (!trimmed) return t('profile.usernameRequired');
@@ -47,7 +36,13 @@ const UsernameRow = ({ mobile }: UsernameRowProps) => {
};
const handleSave = useCallback(async () => {
- const validationError = validateUsername(editValue);
+ const value = inputRef.current?.input?.value?.trim();
+ if (!value || value === username) {
+ setError('');
+ return;
+ }
+
+ const validationError = validateUsername(value);
if (validationError) {
setError(validationError);
return;
@@ -56,11 +51,10 @@ const UsernameRow = ({ mobile }: UsernameRowProps) => {
try {
setSaving(true);
setError('');
- await updateUsername(editValue.trim());
- setIsEditing(false);
+ await updateUsername(value);
+ setDirty(false);
} catch (err: any) {
console.error('Failed to update username:', err);
- // Handle duplicate username error
if (err?.data?.code === 'CONFLICT' || err?.message === 'USERNAME_TAKEN') {
setError(t('profile.usernameDuplicate'));
} else {
@@ -69,104 +63,94 @@ const UsernameRow = ({ mobile }: UsernameRowProps) => {
} finally {
setSaving(false);
}
- }, [editValue, updateUsername, t]);
+ }, [username, updateUsername, t]);
- const handleInputChange = (e: ChangeEvent) => {
+ const handleChange = (e: ChangeEvent) => {
const value = e.target.value;
- setEditValue(value);
-
+ setDirty(value.trim() !== (username || ''));
if (!value.trim()) {
setError('');
return;
}
-
if (!usernameRegex.test(value)) {
setError(t('profile.usernameRule'));
return;
}
-
setError('');
};
- const editingContent = (
-
-
- {!mobile && {t('profile.usernameInputHint')}}
-
+ const handleCancel = useCallback(() => {
+ if (inputRef.current?.input) {
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
+ HTMLInputElement.prototype,
+ 'value',
+ )?.set;
+ nativeInputValueSetter?.call(inputRef.current.input, username || '');
+ inputRef.current.input.dispatchEvent(new Event('input', { bubbles: true }));
+ }
+ setError('');
+ setDirty(false);
+ inputRef.current?.blur();
+ }, [username]);
+
+ const input = (
+
+
+ {saving && } size="small" />}
{error && (
-
+
{error}
)}
-
-
+ )}
+ {
+ if (e.key === 'Escape') {
+ e.preventDefault();
+ handleCancel();
+ }
+ }}
+ onPressEnter={handleSave}
+ />
-
- );
-
- const displayContent = (
-
- {mobile ? (
- {username || '--'}
- ) : (
-
- {username || '--'}
-
- {t('profile.updateUsername')}
-
-
- )}
-
+
);
if (mobile) {
return (
-
- {t('profile.username')}
- {!isEditing && (
-
- {t('profile.updateUsername')}
-
- )}
-
- {isEditing ? editingContent : displayContent}
+ {t('profile.username')}
+ {input}
);
}
return (
-
+
{t('profile.username')}
-
- {isEditing ? editingContent : displayContent}
+
+ {input}
);
diff --git a/src/routes/(main)/settings/profile/index.tsx b/src/routes/(main)/settings/profile/index.tsx
index 74a05c8442..e6a606f47a 100644
--- a/src/routes/(main)/settings/profile/index.tsx
+++ b/src/routes/(main)/settings/profile/index.tsx
@@ -28,21 +28,15 @@ const SkeletonRow = ({ mobile }: { mobile?: boolean }) => {
if (mobile) {
return (
-
-
-
-
+
);
}
return (
-
-
-
-
-
-
+
+
+
);
};
diff --git a/src/server/routers/lambda/search.ts b/src/server/routers/lambda/search.ts
index 8a118c601a..9342af81c4 100644
--- a/src/server/routers/lambda/search.ts
+++ b/src/server/routers/lambda/search.ts
@@ -56,6 +56,7 @@ export const searchRouter = router({
'mcp',
'plugin',
'communityAgent',
+ 'knowledgeBase',
])
.optional(),
}),
@@ -70,7 +71,7 @@ export const searchRouter = router({
const searchPromises: Promise[] = [];
// Database searches (agent, topic, file, folder, message, page, memory)
- if (!type || ['agent', 'topic', 'file', 'folder', 'message', 'page', 'memory'].includes(type)) {
+ if (!type || ['agent', 'topic', 'file', 'folder', 'message', 'page', 'memory', 'knowledgeBase'].includes(type)) {
searchPromises.push(ctx.searchRepo.search(input));
}