🐛 fix: layout recent locale and support dismiss banner (#13739)

* fix: CN locale for rencents

* fix: community profile setup modal

* feat: support skill banner dismiss
This commit is contained in:
Rdmclin2 2026-04-11 23:27:21 +08:00 committed by GitHub
parent 732a3ae54a
commit 08769e5bf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 74 additions and 24 deletions

View file

@ -564,6 +564,7 @@
"skillDetail.tabs.tools": "Capabilities",
"skillDetail.tools": "Tools",
"skillDetail.trustWarning": "Only use connectors from developers you trust. LobeHub does not control which tools developers make available and cannot verify that they will work as intended or that they won't change.",
"skillInstallBanner.dismiss": "Dismiss",
"skillInstallBanner.title": "Add skills to Lobe AI",
"store.actions.cancel": "Cancel",
"store.actions.configure": "Configure",

View file

@ -376,7 +376,7 @@
"promptTransform.actions.translate": "翻译",
"promptTransform.status.rewrite": "正在丰富细节...",
"promptTransform.status.translate": "正在翻译...",
"recents": "Recents",
"recents": "最近",
"regenerate": "重新生成",
"releaseNotes": "版本详情",
"rename": "重命名",

View file

@ -564,6 +564,7 @@
"skillDetail.tabs.tools": "技能功能",
"skillDetail.tools": "工具",
"skillDetail.trustWarning": "请仅使用您信任的开发者提供的连接器。LobeHub 无法控制开发者提供哪些工具,也无法验证这些工具是否按预期工作或是否会发生变化。",
"skillInstallBanner.dismiss": "不再显示",
"skillInstallBanner.title": "为 Lobe AI 添加技能",
"store.actions.cancel": "取消安装",
"store.actions.configure": "配置",

View file

@ -1,6 +1,7 @@
'use client';
import { Center, Flexbox, Icon, Input, Modal, Text, TextArea, Tooltip } from '@lobehub/ui';
import { Center, Flexbox, Icon, Input, Text, TextArea, Tooltip } from '@lobehub/ui';
import { Modal } from '@lobehub/ui/base-ui';
import { type UploadProps } from 'antd';
import { App, Form, Modal as AntModal, Upload } from 'antd';
import { cssVar } from 'antd-style';

View file

@ -576,6 +576,7 @@ export default {
'skillDetail.tools': 'Tools',
'skillDetail.trustWarning':
"Only use connectors from developers you trust. LobeHub does not control which tools developers make available and cannot verify that they will work as intended or that they won't change.",
'skillInstallBanner.dismiss': 'Dismiss',
'skillInstallBanner.title': 'Add skills to Lobe AI',
'store.actions.cancel': 'Cancel',
'store.actions.configure': 'Configure',

View file

@ -1,16 +1,21 @@
'use client';
import { getKlavisServerByServerIdentifier, getLobehubSkillProviderById } from '@lobechat/const';
import { Flexbox, Icon } from '@lobehub/ui';
import { ActionIcon, Flexbox, Icon } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { Blocks } from 'lucide-react';
import { Blocks, X } from 'lucide-react';
import React, { createElement, memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { createSkillStoreModal } from '@/features/SkillStore';
import { useGlobalStore } from '@/store/global';
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
import { useToolStore } from '@/store/tool';
// Bump this id when the banner content changes so dismissing the old
// variant does not hide the new one.
export const SKILL_INSTALL_BANNER_ID = 'skill-install-v1';
const ICON_SIZE = 16;
const AVATAR_SIZE = 24;
@ -45,7 +50,7 @@ const styles = createStaticStyles(({ css, cssVar }) => ({
margin-block-end: -6px;
padding-block: 42px 10px;
padding-inline: 16px;
padding-inline: 16px 12px;
border: 1px solid ${cssVar.colorFillSecondary};
border-radius: 20px;
@ -80,6 +85,8 @@ const SkillInstallBanner = memo(() => {
const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
const isKlavisEnabled = useServerConfigStore(serverConfigSelectors.enableKlavis);
const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
// Prefetch skill connections data so SkillStore opens faster
const [useFetchLobehubSkillConnections, useFetchUserKlavisServers] = useToolStore((s) => [
s.useFetchLobehubSkillConnections,
@ -112,29 +119,49 @@ const SkillInstallBanner = memo(() => {
createSkillStoreModal();
}, []);
const handleDismiss = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
const current = useGlobalStore.getState().status.dismissedBannerIds || [];
if (current.includes(SKILL_INSTALL_BANNER_ID)) return;
updateSystemStatus({
dismissedBannerIds: [...current, SKILL_INSTALL_BANNER_ID],
});
},
[updateSystemStatus],
);
return (
<div className={styles.banner} data-testid="skill-install-banner" onClick={handleOpenStore}>
<Flexbox horizontal align="center" gap={4}>
<Icon className={styles.icon} icon={Blocks} size={18} />
<span className={styles.text}>{t('skillInstallBanner.title')}</span>
</Flexbox>
{skillIcons.length > 0 && (
<div className={styles.iconGroup}>
{skillIcons.map(({ icon, key }, index) => (
<div
className={styles.avatar}
key={key}
style={{ marginLeft: index === 0 ? 0 : -6, zIndex: index }}
>
{typeof icon === 'string' ? (
<img alt={key} height={ICON_SIZE} src={icon} width={ICON_SIZE} />
) : (
createElement(icon, { size: ICON_SIZE })
)}
</div>
))}
</div>
)}
<Flexbox horizontal align="center" gap={8}>
{skillIcons.length > 0 && (
<div className={styles.iconGroup}>
{skillIcons.map(({ icon, key }, index) => (
<div
className={styles.avatar}
key={key}
style={{ marginLeft: index === 0 ? 0 : -6, zIndex: index }}
>
{typeof icon === 'string' ? (
<img alt={key} height={ICON_SIZE} src={icon} width={ICON_SIZE} />
) : (
createElement(icon, { size: ICON_SIZE })
)}
</div>
))}
</div>
)}
<ActionIcon
icon={X}
size="small"
title={t('skillInstallBanner.dismiss')}
onClick={handleDismiss}
/>
</Flexbox>
</div>
);
});

View file

@ -8,13 +8,15 @@ import { ChatInputProvider, DesktopChatInput } from '@/features/ChatInput';
import { useAgentStore } from '@/store/agent';
import { agentByIdSelectors } from '@/store/agent/selectors';
import { useChatStore } from '@/store/chat';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';
import { useHomeStore } from '@/store/home';
import { serverConfigSelectors, useServerConfigStore } from '@/store/serverConfig';
import CommunityRecommend from '../CommunityRecommend';
import SuggestQuestions from '../SuggestQuestions';
import ModeTag from './ModeTag';
import SkillInstallBanner from './SkillInstallBanner';
import SkillInstallBanner, { SKILL_INSTALL_BANNER_ID } from './SkillInstallBanner';
import StarterList from './StarterList';
import { useSend } from './useSend';
@ -25,7 +27,10 @@ const InputArea = () => {
const inputActiveMode = useHomeStore((s) => s.inputActiveMode);
const isLobehubSkillEnabled = useServerConfigStore(serverConfigSelectors.enableLobehubSkill);
const isKlavisEnabled = useServerConfigStore(serverConfigSelectors.enableKlavis);
const showSkillBanner = isLobehubSkillEnabled || isKlavisEnabled;
const isSkillBannerDismissed = useGlobalStore(
systemStatusSelectors.isBannerDismissed(SKILL_INSTALL_BANNER_ID),
);
const showSkillBanner = (isLobehubSkillEnabled || isKlavisEnabled) && !isSkillBannerDismissed;
const chatInputRef = useRef<HTMLDivElement>(null);
// When a starter mode is activated (e.g. Create Agent / Create Group / Write),

View file

@ -99,6 +99,11 @@ export interface SystemStatus {
chatInputHeight?: number;
disabledModelProvidersSortType?: string;
disabledModelsSortType?: string;
/**
* IDs of banners/ads the user has dismissed. New banners use a new ID
* so dismissing the current one does not hide future ones.
*/
dismissedBannerIds?: string[];
expandInputActionbar?: boolean;
// which sessionGroup should expand
expandSessionGroupKeys: string[];
@ -237,6 +242,7 @@ export const INITIAL_STATUS = {
recentPageSize: 5,
disabledModelProvidersSortType: 'default',
disabledModelsSortType: 'default',
dismissedBannerIds: [],
expandInputActionbar: true,
expandSessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default],
fileManagerViewMode: 'list' as const,

View file

@ -76,6 +76,13 @@ const isNotificationRead =
const slugs = s.status.readNotificationSlugs || [];
return slugs.includes(slug);
};
const isBannerDismissed =
(bannerId: string) =>
(s: GlobalState): boolean => {
const ids = s.status.dismissedBannerIds || [];
return ids.includes(bannerId);
};
const tokenDisplayFormatShort = (s: GlobalState) =>
s.status.tokenDisplayFormatShort !== undefined ? s.status.tokenDisplayFormatShort : true;
@ -95,6 +102,7 @@ export const systemStatusSelectors = {
imageTopicViewMode,
imageTopicPanelWidth,
inZenMode,
isBannerDismissed,
isNotificationRead,
isShowCredit,
isStatusInit,