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)); }