mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
feat: finish slide
This commit is contained in:
parent
37ac79cb87
commit
4ec2291816
@ -265,6 +265,12 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@utility title-5xl-semi-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@utility title-5xl-bold {
|
||||
font-size: 30px;
|
||||
font-weight: 700;
|
||||
|
||||
696
pnpm-lock.yaml
generated
696
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -154,6 +154,7 @@ catalog:
|
||||
echarts-for-react: 3.0.6
|
||||
elkjs: 0.11.1
|
||||
embla-carousel-autoplay: 8.6.0
|
||||
embla-carousel-fade: 8.6.0
|
||||
embla-carousel-react: 8.6.0
|
||||
emoji-mart: 5.6.0
|
||||
es-toolkit: 1.46.1
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import type { UseEmblaCarouselType } from 'embla-carousel-react'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import Autoplay from 'embla-carousel-autoplay'
|
||||
import Fade from 'embla-carousel-fade'
|
||||
import useEmblaCarousel from 'embla-carousel-react'
|
||||
import * as React from 'react'
|
||||
|
||||
@ -215,6 +216,7 @@ CarouselDot.displayName = 'CarouselDot'
|
||||
|
||||
const CarouselPlugins = {
|
||||
Autoplay,
|
||||
Fade,
|
||||
}
|
||||
|
||||
Carousel.Content = CarouselContent
|
||||
|
||||
@ -108,17 +108,17 @@ describe('BannerItem', () => {
|
||||
expect(screen.getByText('Test banner description text')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders view more text', () => {
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('explore.banner.viewMore')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders banner image with correct src and alt', () => {
|
||||
renderBannerItem()
|
||||
const image = screen.getByRole('img')
|
||||
expect(image).toHaveAttribute('src', 'https://example.com/image.png')
|
||||
expect(image).toHaveAttribute('alt', 'Test Banner Title')
|
||||
})
|
||||
|
||||
it('renders view more text', () => {
|
||||
renderBannerItem()
|
||||
expect(screen.getByText('explore.banner.viewMore')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('click handling', () => {
|
||||
|
||||
@ -32,19 +32,27 @@ vi.mock('@/app/components/base/amplitude', () => ({
|
||||
trackEvent: (...args: unknown[]) => mockTrackEvent(...args),
|
||||
}))
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string, opts?: Record<string, unknown>) => {
|
||||
if (key === 'banner.greeting')
|
||||
return `Welcome back, ${opts?.name} 👋`
|
||||
if (key === 'banner.tagline')
|
||||
return 'What if… this is where your next idea begins.'
|
||||
return key
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/carousel', () => ({
|
||||
Carousel: Object.assign(
|
||||
({ children, onMouseEnter, onMouseLeave, className }: {
|
||||
({ children, className }: {
|
||||
children: React.ReactNode
|
||||
onMouseEnter?: () => void
|
||||
onMouseLeave?: () => void
|
||||
className?: string
|
||||
}) => (
|
||||
<div
|
||||
data-testid="carousel"
|
||||
className={className}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@ -58,6 +66,7 @@ vi.mock('@/app/components/base/carousel', () => ({
|
||||
),
|
||||
Plugin: {
|
||||
Autoplay: (config: Record<string, unknown>) => ({ type: 'autoplay', ...config }),
|
||||
Fade: () => ({ type: 'fade' }),
|
||||
},
|
||||
},
|
||||
),
|
||||
@ -113,7 +122,7 @@ const createMockBanner = (id: string, status: string = 'enabled', title: string
|
||||
'category': 'Featured',
|
||||
title,
|
||||
'description': 'Test description',
|
||||
'img-src': 'https://example.com/image.png',
|
||||
'img-src': `https://example.com/image-${id}.png`,
|
||||
},
|
||||
} as BannerType)
|
||||
|
||||
@ -126,6 +135,7 @@ describe('Banner', () => {
|
||||
mockUseSelector.mockImplementation(selector => selector({
|
||||
userProfile: {
|
||||
id: 'account-123',
|
||||
name: 'Evan',
|
||||
},
|
||||
}))
|
||||
})
|
||||
@ -218,6 +228,53 @@ describe('Banner', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('greeting section', () => {
|
||||
it('renders static greeting with user name', () => {
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
data: [createMockBanner('1', 'enabled')],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
})
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
expect(screen.getByText('Welcome back, Evan 👋')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders tagline', () => {
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
data: [createMockBanner('1', 'enabled')],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
})
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
expect(screen.getByText('What if… this is where your next idea begins.')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('greeting does not change when carousel slides', () => {
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
data: [
|
||||
createMockBanner('1', 'enabled', 'Banner 1'),
|
||||
createMockBanner('2', 'enabled', 'Banner 2'),
|
||||
],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
})
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
expect(screen.getByText('Welcome back, Evan 👋')).toBeInTheDocument()
|
||||
|
||||
act(() => {
|
||||
setMockSelectedIndex(1)
|
||||
})
|
||||
|
||||
expect(screen.getByText('Welcome back, Evan 👋')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('successful render', () => {
|
||||
it('renders carousel when enabled banners exist', () => {
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
@ -264,18 +321,6 @@ describe('Banner', () => {
|
||||
expect(bannerItem).toHaveAttribute('data-autoplay-delay', '5000')
|
||||
})
|
||||
|
||||
it('renders carousel with correct class', () => {
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
data: [createMockBanner('1', 'enabled')],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
})
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
expect(screen.getByTestId('carousel')).toHaveClass('rounded-2xl')
|
||||
})
|
||||
|
||||
it('tracks only the current banner impression and reports the next one after slide changes', () => {
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
data: [
|
||||
@ -322,6 +367,7 @@ describe('Banner', () => {
|
||||
mockUseSelector.mockImplementation(selector => selector({
|
||||
userProfile: {
|
||||
id: '',
|
||||
name: '',
|
||||
},
|
||||
}))
|
||||
mockUseGetBanners.mockReturnValue({
|
||||
@ -346,8 +392,8 @@ describe('Banner', () => {
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
const carousel = screen.getByTestId('carousel')
|
||||
fireEvent.mouseEnter(carousel)
|
||||
const wrapper = screen.getByText('Welcome back, Evan 👋').closest('.relative')!
|
||||
fireEvent.mouseEnter(wrapper)
|
||||
|
||||
const bannerItem = screen.getByTestId('banner-item')
|
||||
expect(bannerItem).toHaveAttribute('data-is-paused', 'true')
|
||||
@ -362,10 +408,10 @@ describe('Banner', () => {
|
||||
|
||||
render(<Banner />)
|
||||
|
||||
const carousel = screen.getByTestId('carousel')
|
||||
const wrapper = screen.getByText('Welcome back, Evan 👋').closest('.relative')!
|
||||
|
||||
fireEvent.mouseEnter(carousel)
|
||||
fireEvent.mouseLeave(carousel)
|
||||
fireEvent.mouseEnter(wrapper)
|
||||
fireEvent.mouseLeave(wrapper)
|
||||
|
||||
const bannerItem = screen.getByTestId('banner-item')
|
||||
expect(bannerItem).toHaveAttribute('data-is-paused', 'false')
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
/* eslint-disable react-hooks-extra/no-direct-set-state-in-use-effect */
|
||||
/* eslint-disable react/set-state-in-effect */
|
||||
import type { FC } from 'react'
|
||||
import type { Banner } from '@/models/app'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiArrowRightLine } from '@remixicon/react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
@ -22,7 +20,7 @@ const RESPONSIVE_BREAKPOINT = 1200
|
||||
const MAX_RESPONSIVE_WIDTH = 600
|
||||
const INDICATOR_WIDTH = 20
|
||||
const INDICATOR_GAP = 8
|
||||
const MIN_VIEW_MORE_WIDTH = 480
|
||||
const MIN_VIEW_MORE_WIDTH = 160
|
||||
|
||||
export const BannerItem: FC<BannerItemProps> = ({
|
||||
banner,
|
||||
@ -58,9 +56,10 @@ export const BannerItem: FC<BannerItemProps> = ({
|
||||
const viewMoreStyle = useMemo(() => {
|
||||
if (!maxWidth)
|
||||
return undefined
|
||||
const availableWidth = maxWidth - indicatorsWidth
|
||||
return {
|
||||
maxWidth: `${maxWidth}px`,
|
||||
minWidth: indicatorsWidth ? `${Math.min(maxWidth - indicatorsWidth, MIN_VIEW_MORE_WIDTH)}px` : undefined,
|
||||
minWidth: indicatorsWidth && availableWidth > 0 ? `${Math.min(availableWidth, MIN_VIEW_MORE_WIDTH)}px` : undefined,
|
||||
}
|
||||
}, [maxWidth, indicatorsWidth])
|
||||
|
||||
@ -100,6 +99,11 @@ export const BannerItem: FC<BannerItemProps> = ({
|
||||
incrementResetKey()
|
||||
}, [selectedIndex, incrementResetKey])
|
||||
|
||||
const handleIndicatorClick = useCallback((index: number) => {
|
||||
incrementResetKey()
|
||||
api?.scrollTo(index)
|
||||
}, [api, incrementResetKey])
|
||||
|
||||
const handleBannerClick = useCallback(() => {
|
||||
incrementResetKey()
|
||||
|
||||
@ -118,91 +122,79 @@ export const BannerItem: FC<BannerItemProps> = ({
|
||||
window.open(banner.link, '_blank', 'noopener,noreferrer')
|
||||
}, [accountId, banner, incrementResetKey, language, sort])
|
||||
|
||||
const handleIndicatorClick = useCallback((index: number) => {
|
||||
incrementResetKey()
|
||||
api?.scrollTo(index)
|
||||
}, [api, incrementResetKey])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative flex w-full min-w-[784px] cursor-pointer overflow-hidden rounded-2xl bg-components-panel-on-panel-item-bg pr-[288px] transition-shadow hover:shadow-md"
|
||||
className="flex min-h-[168px] w-full cursor-pointer items-center gap-2 overflow-hidden rounded-2xl px-8"
|
||||
onClick={handleBannerClick}
|
||||
>
|
||||
{/* Left content area */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex h-full flex-col gap-3 py-6 pr-0 pl-8">
|
||||
{/* Text section */}
|
||||
<div className="flex min-h-24 flex-wrap items-end gap-1 py-1">
|
||||
{/* Title area */}
|
||||
<div
|
||||
ref={textAreaRef}
|
||||
className="flex max-w-[680px] min-w-[480px] flex-[1_0_0] flex-col pr-4"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
<p className="line-clamp-1 title-4xl-semi-bold text-dify-logo-blue">
|
||||
{category}
|
||||
</p>
|
||||
<p className="line-clamp-2 title-4xl-semi-bold text-dify-logo-black">
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
{/* Description area */}
|
||||
<div
|
||||
className="max-w-[600px] min-w-60 flex-[1_0_0] self-end overflow-hidden py-1 pr-4"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
<p className="line-clamp-4 overflow-hidden body-sm-regular text-text-tertiary">
|
||||
{description}
|
||||
</p>
|
||||
<div className="flex h-[200px] min-w-px flex-1 flex-col items-end gap-3 rounded-2xl pt-4 pb-8">
|
||||
<div className="flex min-h-24 w-full flex-wrap items-end gap-1 py-1">
|
||||
<div
|
||||
ref={textAreaRef}
|
||||
className="flex max-w-[680px] min-w-[480px] flex-[1_1_480px] flex-col pr-4 max-xl:min-w-0"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
<p className="line-clamp-1 title-4xl-semi-bold text-dify-logo-blue">
|
||||
{category}
|
||||
</p>
|
||||
<p className="line-clamp-2 title-4xl-semi-bold text-dify-logo-black">
|
||||
{title}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="max-w-[600px] min-w-0 flex-[1_1_240px] self-end overflow-hidden py-1 pr-4"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
<p className="line-clamp-4 overflow-hidden body-sm-regular text-text-tertiary">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions section */}
|
||||
<div className="flex w-full flex-wrap items-center gap-1">
|
||||
{/* View more button */}
|
||||
<div
|
||||
className="flex max-w-[680px] min-w-[480px] flex-[1_1_480px] items-center gap-[6px] py-1 max-xl:min-w-0"
|
||||
style={viewMoreStyle}
|
||||
>
|
||||
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-text-accent p-[2px]">
|
||||
<span className="i-ri-arrow-right-line h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<span className="system-sm-semibold-uppercase text-text-accent">
|
||||
{t('banner.viewMore', { ns: 'explore' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Actions section */}
|
||||
<div className="flex items-center gap-1">
|
||||
{/* View more button */}
|
||||
<div
|
||||
className="flex max-w-[680px] min-w-[480px] flex-[1_0_0] items-center gap-[6px] py-1 pr-8"
|
||||
style={viewMoreStyle}
|
||||
>
|
||||
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-text-accent p-[2px]">
|
||||
<RiArrowRightLine className="h-3 w-3 text-text-primary-on-surface" />
|
||||
</div>
|
||||
<span className="system-sm-semibold-uppercase text-text-accent">
|
||||
{t('banner.viewMore', { ns: 'explore' })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn('flex max-w-[600px] flex-[1_0_0] items-center gap-2 py-1 pr-10', maxWidth ? '' : 'min-w-60')}
|
||||
style={responsiveStyle}
|
||||
>
|
||||
{/* Slide navigation indicators */}
|
||||
<div className="flex items-center gap-2">
|
||||
{slideInfo.slides.map((_: unknown, index: number) => (
|
||||
<IndicatorButton
|
||||
key={index}
|
||||
index={index}
|
||||
selectedIndex={selectedIndex}
|
||||
isNextSlide={index === slideInfo.nextIndex}
|
||||
autoplayDelay={autoplayDelay}
|
||||
resetKey={resetKey}
|
||||
isPaused={isPaused}
|
||||
onClick={() => handleIndicatorClick(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-divider-regular min-[1380px]:block" />
|
||||
<div
|
||||
className="flex max-w-[600px] min-w-60 flex-[1_1_240px] items-center gap-2 py-1 pr-10 max-xl:min-w-0"
|
||||
style={responsiveStyle}
|
||||
>
|
||||
{/* Slide navigation indicators */}
|
||||
<div className="flex items-center gap-1">
|
||||
{slideInfo.slides.map((_: unknown, index: number) => (
|
||||
<IndicatorButton
|
||||
key={index}
|
||||
index={index}
|
||||
selectedIndex={selectedIndex}
|
||||
isNextSlide={index === slideInfo.nextIndex}
|
||||
autoplayDelay={autoplayDelay}
|
||||
resetKey={resetKey}
|
||||
isPaused={isPaused}
|
||||
onClick={() => handleIndicatorClick(index)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="hidden h-px flex-1 bg-divider-regular min-[1380px]:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right image area */}
|
||||
<div className="absolute top-0 right-0 flex h-full items-center p-2">
|
||||
<div className="flex max-w-60 shrink-0 flex-col items-center justify-center p-4 max-lg:hidden">
|
||||
<img
|
||||
src={imgSrc}
|
||||
alt={title}
|
||||
className="aspect-4/3 h-full max-w-[296px] rounded-xl"
|
||||
className="h-[168px] w-56 shrink-0 rounded-xl object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from 'react'
|
||||
import type { Banner as BannerType } from '@/models/app'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { Carousel, useCarousel } from '@/app/components/base/carousel'
|
||||
import { useSelector } from '@/context/app-context'
|
||||
@ -16,7 +17,7 @@ const RESIZE_DEBOUNCE_DELAY = 50
|
||||
|
||||
const LoadingState: FC = () => (
|
||||
<div
|
||||
className="flex items-center justify-center rounded-2xl bg-components-panel-on-panel-item-bg shadow-md"
|
||||
className="flex items-center justify-center rounded-[24px] bg-background-default-dodge shadow-xs"
|
||||
style={{ minHeight: MIN_LOADING_HEIGHT }}
|
||||
>
|
||||
<Loading />
|
||||
@ -63,9 +64,11 @@ const BannerImpressionTracker: FC<BannerImpressionTrackerProps> = ({
|
||||
}
|
||||
|
||||
const Banner: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const locale = useLocale()
|
||||
const { data: banners, isLoading, isError } = useGetBanners(locale)
|
||||
const accountId = useSelector(s => s.userProfile.id)
|
||||
const userName = useSelector(s => s.userProfile.name)
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
const [isResizing, setIsResizing] = useState(false)
|
||||
const resizeTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
@ -107,40 +110,54 @@ const Banner: FC = () => {
|
||||
return null
|
||||
|
||||
return (
|
||||
<Carousel
|
||||
opts={{ loop: true }}
|
||||
plugins={[
|
||||
Carousel.Plugin.Autoplay({
|
||||
delay: AUTOPLAY_DELAY,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true,
|
||||
}),
|
||||
]}
|
||||
className="rounded-2xl"
|
||||
<div
|
||||
className="relative flex w-full flex-col items-start overflow-hidden rounded-[24px] bg-background-default-dodge transition-shadow hover:shadow-xs"
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<BannerImpressionTracker
|
||||
banners={enabledBanners}
|
||||
accountId={accountId}
|
||||
language={locale}
|
||||
trackedBannerIdsRef={trackedBannerIdsRef}
|
||||
/>
|
||||
<Carousel.Content>
|
||||
{enabledBanners.map((banner, index) => (
|
||||
<Carousel.Item key={banner.id}>
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={AUTOPLAY_DELAY}
|
||||
isPaused={isPaused}
|
||||
sort={index + 1}
|
||||
language={locale}
|
||||
accountId={accountId}
|
||||
/>
|
||||
</Carousel.Item>
|
||||
))}
|
||||
</Carousel.Content>
|
||||
</Carousel>
|
||||
<div className="flex w-full flex-col gap-1 px-8 pt-8">
|
||||
<p className="truncate title-5xl-semi-bold text-dify-logo-black">
|
||||
{t('banner.greeting', { name: userName, ns: 'explore' })}
|
||||
</p>
|
||||
<p className="truncate body-md-regular text-text-secondary">
|
||||
{t('banner.tagline', { ns: 'explore' })}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Carousel
|
||||
opts={{ loop: true }}
|
||||
plugins={[
|
||||
Carousel.Plugin.Fade(),
|
||||
Carousel.Plugin.Autoplay({
|
||||
delay: AUTOPLAY_DELAY,
|
||||
stopOnInteraction: false,
|
||||
stopOnMouseEnter: true,
|
||||
}),
|
||||
]}
|
||||
className="w-full rounded-2xl shadow-xs"
|
||||
>
|
||||
<BannerImpressionTracker
|
||||
banners={enabledBanners}
|
||||
accountId={accountId}
|
||||
language={locale}
|
||||
trackedBannerIdsRef={trackedBannerIdsRef}
|
||||
/>
|
||||
<Carousel.Content>
|
||||
{enabledBanners.map((banner, index) => (
|
||||
<Carousel.Item key={banner.id}>
|
||||
<BannerItem
|
||||
banner={banner}
|
||||
autoplayDelay={AUTOPLAY_DELAY}
|
||||
isPaused={isPaused}
|
||||
sort={index + 1}
|
||||
language={locale}
|
||||
accountId={accountId}
|
||||
/>
|
||||
</Carousel.Item>
|
||||
))}
|
||||
</Carousel.Content>
|
||||
</Carousel>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
"apps.resetFilter": "Clear filter",
|
||||
"apps.resultNum": "{{num}} results",
|
||||
"apps.title": "Try Dify's curated apps to find AI solutions for your business",
|
||||
"banner.greeting": "Welcome back, {{name}} 👋",
|
||||
"banner.tagline": "What if… this is where your next idea begins.",
|
||||
"banner.viewMore": "VIEW MORE",
|
||||
"category.Agent": "Agent",
|
||||
"category.Assistant": "Assistant",
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
"apps.resetFilter": "清除筛选",
|
||||
"apps.resultNum": "{{num}} 个结果",
|
||||
"apps.title": "试用 Dify 精选示例应用,为您的业务寻找 AI 解决方案",
|
||||
"banner.greeting": "欢迎回来,{{name}} 👋",
|
||||
"banner.tagline": "如果……你的下一个想法从这里开始。",
|
||||
"banner.viewMore": "查看更多",
|
||||
"category.Agent": "Agent",
|
||||
"category.Assistant": "助手",
|
||||
|
||||
@ -92,6 +92,7 @@
|
||||
"echarts-for-react": "catalog:",
|
||||
"elkjs": "catalog:",
|
||||
"embla-carousel-autoplay": "catalog:",
|
||||
"embla-carousel-fade": "catalog:",
|
||||
"embla-carousel-react": "catalog:",
|
||||
"emoji-mart": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user