Merge branch 'main' into feat/llm-node-support-tools

This commit is contained in:
zxhlyh
2026-01-07 16:29:47 +08:00
389 changed files with 35555 additions and 4807 deletions

View File

@ -0,0 +1,41 @@
'use client'
import * as React from 'react'
import { SkeletonContainer, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
type AppCardSkeletonProps = {
count?: number
}
/**
* Skeleton placeholder for App cards during loading states.
* Matches the visual layout of AppCard component.
*/
export const AppCardSkeleton = React.memo(({ count = 6 }: AppCardSkeletonProps) => {
return (
<>
{Array.from({ length: count }).map((_, index) => (
<div
key={index}
className="h-[160px] rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg p-4"
>
<SkeletonContainer className="h-full">
<SkeletonRow>
<SkeletonRectangle className="h-10 w-10 rounded-lg" />
<div className="flex flex-1 flex-col gap-1">
<SkeletonRectangle className="h-4 w-2/3" />
<SkeletonRectangle className="h-3 w-1/3" />
</div>
</SkeletonRow>
<div className="mt-4 flex flex-col gap-2">
<SkeletonRectangle className="h-3 w-full" />
<SkeletonRectangle className="h-3 w-4/5" />
</div>
</SkeletonContainer>
</div>
))}
</>
)
})
AppCardSkeleton.displayName = 'AppCardSkeleton'

View File

@ -27,7 +27,9 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
import { CheckModal } from '@/hooks/use-pay'
import { useInfiniteAppList } from '@/service/use-apps'
import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames'
import AppCard from './app-card'
import { AppCardSkeleton } from './app-card-skeleton'
import Empty from './empty'
import Footer from './footer'
import useAppsQueryState from './hooks/use-apps-query-state'
@ -45,7 +47,7 @@ const List = () => {
const { t } = useTranslation()
const { systemFeatures } = useGlobalPublicStore()
const router = useRouter()
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator } = useAppContext()
const { isCurrentWorkspaceEditor, isCurrentWorkspaceDatasetOperator, isLoadingCurrentWorkspace } = useAppContext()
const showTagManagementModal = useTagStore(s => s.showTagManagementModal)
const [activeTab, setActiveTab] = useQueryState(
'category',
@ -89,6 +91,7 @@ const List = () => {
const {
data,
isLoading,
isFetching,
isFetchingNextPage,
fetchNextPage,
hasNextPage,
@ -172,6 +175,8 @@ const List = () => {
const pages = data?.pages ?? []
const hasAnyApp = (pages[0]?.total ?? 0) > 0
// Show skeleton during initial load or when refetching with no previous data
const showSkeleton = isLoading || (isFetching && pages.length === 0)
return (
<>
@ -205,23 +210,34 @@ const List = () => {
/>
</div>
</div>
{hasAnyApp
? (
<div className="relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6">
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} onSuccess={refetch} selectedAppType={activeTab} />}
{pages.map(({ data: apps }) => apps.map(app => (
<AppCard key={app.id} app={app} onRefresh={refetch} />
)))}
</div>
)
: (
<div className="relative grid grow grid-cols-1 content-start gap-4 overflow-hidden px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6">
{isCurrentWorkspaceEditor
&& <NewAppCard ref={newAppCardRef} className="z-10" onSuccess={refetch} selectedAppType={activeTab} />}
<Empty />
</div>
)}
<div className={cn(
'relative grid grow grid-cols-1 content-start gap-4 px-12 pt-2 sm:grid-cols-1 md:grid-cols-2 xl:grid-cols-4 2xl:grid-cols-5 2k:grid-cols-6',
!hasAnyApp && 'overflow-hidden',
)}
>
{(isCurrentWorkspaceEditor || isLoadingCurrentWorkspace) && (
<NewAppCard
ref={newAppCardRef}
isLoading={isLoadingCurrentWorkspace}
onSuccess={refetch}
selectedAppType={activeTab}
className={cn(!hasAnyApp && 'z-10')}
/>
)}
{(() => {
if (showSkeleton)
return <AppCardSkeleton count={6} />
if (hasAnyApp) {
return pages.flatMap(({ data: apps }) => apps).map(app => (
<AppCard key={app.id} app={app} onRefresh={refetch} />
))
}
// No apps - show empty state
return <Empty />
})()}
</div>
{isCurrentWorkspaceEditor && (
<div

View File

@ -25,6 +25,7 @@ const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-fro
export type CreateAppCardProps = {
className?: string
isLoading?: boolean
onSuccess?: () => void
ref: React.RefObject<HTMLDivElement | null>
selectedAppType?: string
@ -33,6 +34,7 @@ export type CreateAppCardProps = {
const CreateAppCard = ({
ref,
className,
isLoading = false,
onSuccess,
selectedAppType,
}: CreateAppCardProps) => {
@ -56,7 +58,11 @@ const CreateAppCard = ({
return (
<div
ref={ref}
className={cn('relative col-span-1 inline-flex h-[160px] flex-col justify-between rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg', className)}
className={cn(
'relative col-span-1 inline-flex h-[160px] flex-col justify-between rounded-xl border-[0.5px] border-components-card-border bg-components-card-bg transition-opacity',
isLoading && 'pointer-events-none opacity-50',
className,
)}
>
<div className="grow rounded-t-xl p-2">
<div className="px-6 pb-1 pt-2 text-xs font-medium leading-[18px] text-text-tertiary">{t('createApp', { ns: 'app' })}</div>

View File

@ -17,7 +17,7 @@ vi.mock('@/hooks/use-app-favicon', () => ({
useAppFavicon: vi.fn(),
}))
vi.mock('@/i18n-config/i18next-config', () => ({
vi.mock('@/i18n-config/client', () => ({
changeLanguage: vi.fn().mockResolvedValue(undefined),
}))

View File

@ -25,7 +25,7 @@ import { useToastContext } from '@/app/components/base/toast'
import { InputVarType } from '@/app/components/workflow/types'
import { useWebAppStore } from '@/context/web-app-context'
import { useAppFavicon } from '@/hooks/use-app-favicon'
import { changeLanguage } from '@/i18n-config/i18next-config'
import { changeLanguage } from '@/i18n-config/client'
import {
delConversation,
pinConversation,

View File

@ -13,7 +13,7 @@ import { shareQueryKeys } from '@/service/use-share'
import { CONVERSATION_ID_INFO } from '../constants'
import { useEmbeddedChatbot } from './hooks'
vi.mock('@/i18n-config/i18next-config', () => ({
vi.mock('@/i18n-config/client', () => ({
changeLanguage: vi.fn().mockResolvedValue(undefined),
}))

View File

@ -23,7 +23,7 @@ import { useToastContext } from '@/app/components/base/toast'
import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
import { InputVarType } from '@/app/components/workflow/types'
import { useWebAppStore } from '@/context/web-app-context'
import { changeLanguage } from '@/i18n-config/i18next-config'
import { changeLanguage } from '@/i18n-config/client'
import { updateFeedback } from '@/service/share'
import {
useInvalidateShareConversations,

View File

@ -4,7 +4,7 @@ import type {
GetValuesOptions,
} from '../types'
import { useCallback } from 'react'
import { getTransformedValuesWhenSecretInputPristine } from '../utils'
import { getTransformedValuesWhenSecretInputPristine } from '../utils/secret-input'
import { useCheckValidated } from './use-check-validated'
export const useGetFormValues = (form: AnyFormApi, formSchemas: FormSchema[]) => {

View File

@ -1 +0,0 @@
export * from './secret-input'

View File

@ -0,0 +1,22 @@
import type { ZodSchema } from 'zod'
type SubmitValidator<T> = ({ value }: { value: T }) => { fields: Record<string, string> } | undefined
export const zodSubmitValidator = <T>(schema: ZodSchema<T>): SubmitValidator<T> => {
return ({ value }) => {
const result = schema.safeParse(value)
if (!result.success) {
const fieldErrors: Record<string, string> = {}
for (const issue of result.error.issues) {
const path = issue.path[0]
if (path === undefined)
continue
const key = String(path)
if (!fieldErrors[key])
fieldErrors[key] = issue.message
}
return { fields: fieldErrors }
}
return undefined
}
}

View File

@ -1,17 +0,0 @@
<svg width="25" height="25" viewBox="0 0 25 25" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g clip-path="url(#clip0_6305_73327)">
<path d="M0.5 12.5C0.5 8.77247 0.5 6.9087 1.10896 5.43853C1.92092 3.47831 3.47831 1.92092 5.43853 1.10896C6.9087 0.5 8.77247 0.5 12.5 0.5C16.2275 0.5 18.0913 0.5 19.5615 1.10896C21.5217 1.92092 23.0791 3.47831 23.891 5.43853C24.5 6.9087 24.5 8.77247 24.5 12.5C24.5 16.2275 24.5 18.0913 23.891 19.5615C23.0791 21.5217 21.5217 23.0791 19.5615 23.891C18.0913 24.5 16.2275 24.5 12.5 24.5C8.77247 24.5 6.9087 24.5 5.43853 23.891C3.47831 23.0791 1.92092 21.5217 1.10896 19.5615C0.5 18.0913 0.5 16.2275 0.5 12.5Z" fill="white"/>
<rect width="24" height="24" transform="translate(0.5 0.5)" fill="url(#pattern0_6305_73327)"/>
<rect width="24" height="24" transform="translate(0.5 0.5)" fill="white" fill-opacity="0.01"/>
</g>
<path d="M12.5 0.25C14.3603 0.25 15.7684 0.250313 16.8945 0.327148C18.0228 0.404144 18.8867 0.558755 19.6572 0.87793C21.6787 1.71525 23.2847 3.32133 24.1221 5.34277C24.4412 6.11333 24.5959 6.97723 24.6729 8.10547C24.7497 9.23161 24.75 10.6397 24.75 12.5C24.75 14.3603 24.7497 15.7684 24.6729 16.8945C24.5959 18.0228 24.4412 18.8867 24.1221 19.6572C23.2847 21.6787 21.6787 23.2847 19.6572 24.1221C18.8867 24.4412 18.0228 24.5959 16.8945 24.6729C15.7684 24.7497 14.3603 24.75 12.5 24.75C10.6397 24.75 9.23161 24.7497 8.10547 24.6729C6.97723 24.5959 6.11333 24.4412 5.34277 24.1221C3.32133 23.2847 1.71525 21.6787 0.87793 19.6572C0.558755 18.8867 0.404144 18.0228 0.327148 16.8945C0.250313 15.7684 0.25 14.3603 0.25 12.5C0.25 10.6397 0.250313 9.23161 0.327148 8.10547C0.404144 6.97723 0.558755 6.11333 0.87793 5.34277C1.71525 3.32133 3.32133 1.71525 5.34277 0.87793C6.11333 0.558755 6.97723 0.404144 8.10547 0.327148C9.23161 0.250313 10.6397 0.25 12.5 0.25Z" stroke="#101828" stroke-opacity="0.08" stroke-width="0.5"/>
<defs>
<pattern id="pattern0_6305_73327" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_6305_73327" transform="scale(0.00625)"/>
</pattern>
<clipPath id="clip0_6305_73327">
<path d="M0.5 12.5C0.5 8.77247 0.5 6.9087 1.10896 5.43853C1.92092 3.47831 3.47831 1.92092 5.43853 1.10896C6.9087 0.5 8.77247 0.5 12.5 0.5C16.2275 0.5 18.0913 0.5 19.5615 1.10896C21.5217 1.92092 23.0791 3.47831 23.891 5.43853C24.5 6.9087 24.5 8.77247 24.5 12.5C24.5 16.2275 24.5 18.0913 23.891 19.5615C23.0791 21.5217 21.5217 23.0791 19.5615 23.891C18.0913 24.5 16.2275 24.5 12.5 24.5C8.77247 24.5 6.9087 24.5 5.43853 23.891C3.47831 23.0791 1.92092 21.5217 1.10896 19.5615C0.5 18.0913 0.5 16.2275 0.5 12.5Z" fill="white"/>
</clipPath>
<image id="image0_6305_73327" width="160" height="160" preserveAspectRatio="none" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,4 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" fill="white"/>
<path d="M25.7926 10.1311H21.5089L29.3208 29.869H33.6045L25.7926 10.1311ZM13.4164 10.1311L5.60449 29.869H9.97273L11.5703 25.724H19.743L21.3405 29.869H25.7087L17.8969 10.1311H13.4164ZM12.9834 22.0583L15.6566 15.1217L18.3299 22.0583H12.9834Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 403 B

View File

@ -1,4 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" fill="white"/>
<path d="M36.6676 11.2917C36.3316 11.1277 36.1871 11.4402 35.9906 11.599C35.9242 11.6511 35.8668 11.7188 35.8108 11.7787C35.3199 12.3048 34.747 12.6485 33.9996 12.6068C32.9046 12.5469 31.971 12.8907 31.1455 13.7293C30.9696 12.6954 30.3863 12.0782 29.4996 11.6824C29.0348 11.4766 28.5647 11.2709 28.2406 10.823C28.0127 10.5053 27.9515 10.1511 27.8368 9.80214C27.7652 9.59121 27.6923 9.37506 27.4502 9.33861C27.1871 9.29694 27.0843 9.51829 26.9814 9.70318C26.5674 10.4584 26.4084 11.2917 26.4228 12.1355C26.4592 14.0313 27.26 15.5417 28.8486 16.6173C29.0296 16.7397 29.0764 16.8646 29.0191 17.0443C28.9111 17.4141 28.7822 17.7735 28.6676 18.1433C28.596 18.3803 28.4879 18.4323 28.2354 18.3282C27.363 17.9637 26.609 17.4246 25.9436 16.7709C24.8135 15.6771 23.7914 14.4689 22.5166 13.5235C22.2171 13.3021 21.919 13.0964 21.609 12.9011C20.3082 11.6355 21.7796 10.5964 22.1194 10.474C22.4762 10.3464 22.2431 9.9037 21.092 9.90891C19.9423 9.91413 18.889 10.2995 17.5478 10.8126C17.3512 10.8907 17.1455 10.948 16.9332 10.9922C15.7158 10.7631 14.4515 10.711 13.1298 10.8594C10.6428 11.1381 8.65587 12.3152 7.19493 14.3255C5.44102 16.7397 5.02826 19.4845 5.53347 22.349C6.06473 25.3646 7.60249 27.8646 9.96707 29.8178C12.4176 31.8413 15.2406 32.8334 18.4606 32.6433C20.4163 32.5313 22.5947 32.2683 25.0504 30.1875C25.6702 30.4949 26.3199 30.6173 27.3994 30.711C28.2302 30.7891 29.0296 30.6694 29.6494 30.5417C30.6194 30.3361 30.5518 29.4375 30.2015 29.2709C27.3578 27.9454 27.9814 28.4845 27.4136 28.0495C28.859 26.3361 31.0374 24.5574 31.889 18.797C31.9554 18.3386 31.898 18.0522 31.889 17.6798C31.8838 17.4558 31.9346 17.3673 32.1923 17.3413C32.9046 17.2605 33.596 17.0651 34.2314 16.7137C36.0739 15.7058 36.816 14.0522 36.9918 12.0678C37.0179 11.7657 36.9866 11.4506 36.6676 11.2917ZM20.613 29.1485C17.8564 26.9793 16.5204 26.2657 15.9684 26.297C15.4527 26.3255 15.5452 26.9167 15.6584 27.3022C15.777 27.6823 15.9319 27.9454 16.1494 28.2787C16.2991 28.5001 16.402 28.8307 15.9996 29.0755C15.1116 29.6277 13.5687 28.8907 13.4958 28.8542C11.7001 27.797 10.1988 26.3985 9.14025 24.487C8.11941 22.6459 7.52566 20.6719 7.42801 18.5651C7.40197 18.0547 7.5517 17.875 8.05691 17.7839C8.72227 17.6615 9.40978 17.6355 10.0751 17.7318C12.8876 18.1433 15.2822 19.4037 17.2887 21.3959C18.4346 22.5339 19.3018 23.8907 20.195 25.2162C21.1442 26.6251 22.1663 27.9662 23.4671 29.0651C23.9254 29.4506 24.2926 29.7449 24.6428 29.961C23.5856 30.0782 21.8199 30.1042 20.613 29.1485ZM21.9332 20.6407C21.9332 20.4141 22.1142 20.2345 22.342 20.2345C22.3928 20.2345 22.4398 20.2449 22.4814 20.2605C22.5374 20.2813 22.5895 20.3126 22.6299 20.3594C22.7027 20.4298 22.7444 20.5339 22.7444 20.6407C22.7444 20.8673 22.5635 21.047 22.3368 21.047C22.109 21.047 21.9332 20.8673 21.9332 20.6407ZM26.036 22.7501C25.7731 22.8569 25.51 22.9506 25.2575 22.961C24.8655 22.9793 24.4371 22.8203 24.204 22.6251C23.8434 22.323 23.5856 22.1537 23.4762 21.6225C23.4306 21.3959 23.4567 21.047 23.497 20.8465C23.5908 20.4141 23.4866 20.1381 23.1832 19.8855C22.9346 19.6798 22.6207 19.6251 22.2744 19.6251C22.1455 19.6251 22.027 19.5678 21.9384 19.5209C21.7939 19.4479 21.6754 19.2683 21.7887 19.047C21.8251 18.9766 22.001 18.8022 22.0426 18.7709C22.5114 18.5027 23.053 18.5913 23.5543 18.7918C24.0191 18.9818 24.3694 19.3307 24.8746 19.823C25.3915 20.4194 25.484 20.5861 25.7783 21.0313C26.01 21.3829 26.2223 21.7422 26.3668 22.1537C26.454 22.4089 26.3408 22.6198 26.036 22.7501Z" fill="#4D6BFE"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -1,105 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" fill="white"/>
<mask id="mask0_3892_95663" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="6" y="6" width="28" height="29">
<path d="M20 6C20.2936 6 20.5488 6.2005 20.6205 6.48556C20.8393 7.3566 21.1277 8.20866 21.4828 9.03356C22.4116 11.191 23.6854 13.0791 25.3032 14.6968C26.9218 16.3146 28.8095 17.5888 30.9664 18.5172C31.7941 18.8735 32.6436 19.16 33.5149 19.3795C33.6533 19.4143 33.7762 19.4942 33.8641 19.6067C33.9519 19.7192 33.9998 19.8578 34 20.0005C34 20.2941 33.7995 20.5492 33.5149 20.621C32.6437 20.8399 31.7915 21.1282 30.9664 21.4833C28.8095 22.4121 26.9209 23.6859 25.3032 25.3036C23.6854 26.9223 22.4116 28.8099 21.4828 30.9669C21.1278 31.7919 20.8394 32.6439 20.6205 33.5149C20.586 33.6534 20.5062 33.7764 20.3937 33.8644C20.2813 33.9524 20.1427 34.0003 20 34.0005C19.8572 34.0003 19.7186 33.9525 19.6062 33.8645C19.4937 33.7765 19.414 33.6535 19.3795 33.5149C19.1605 32.6439 18.872 31.7918 18.5167 30.9669C17.5884 28.8099 16.3151 26.9214 14.6964 25.3036C13.0782 23.6859 11.1906 22.4121 9.03309 21.4833C8.20814 21.1283 7.35608 20.8399 6.48509 20.621C6.34667 20.5864 6.22377 20.5065 6.13589 20.3941C6.04801 20.2817 6.00018 20.1432 6 20.0005C6.00024 19.8578 6.04808 19.7192 6.13594 19.6067C6.2238 19.4942 6.34667 19.4143 6.48509 19.3795C7.35612 19.1607 8.20819 18.8723 9.03309 18.5172C11.1906 17.5888 13.0786 16.3146 14.6964 14.6968C16.3141 13.0791 17.5884 11.191 18.5167 9.03356C18.8719 8.20862 19.1604 7.35656 19.3795 6.48556C19.4508 6.2005 19.7064 6 20 6Z" fill="black"/>
<path d="M20 6C20.2936 6 20.5488 6.2005 20.6205 6.48556C20.8393 7.3566 21.1277 8.20866 21.4828 9.03356C22.4116 11.191 23.6854 13.0791 25.3032 14.6968C26.9218 16.3146 28.8095 17.5888 30.9664 18.5172C31.7941 18.8735 32.6436 19.16 33.5149 19.3795C33.6533 19.4143 33.7762 19.4942 33.8641 19.6067C33.9519 19.7192 33.9998 19.8578 34 20.0005C34 20.2941 33.7995 20.5492 33.5149 20.621C32.6437 20.8399 31.7915 21.1282 30.9664 21.4833C28.8095 22.4121 26.9209 23.6859 25.3032 25.3036C23.6854 26.9223 22.4116 28.8099 21.4828 30.9669C21.1278 31.7919 20.8394 32.6439 20.6205 33.5149C20.586 33.6534 20.5062 33.7764 20.3937 33.8644C20.2813 33.9524 20.1427 34.0003 20 34.0005C19.8572 34.0003 19.7186 33.9525 19.6062 33.8645C19.4937 33.7765 19.414 33.6535 19.3795 33.5149C19.1605 32.6439 18.872 31.7918 18.5167 30.9669C17.5884 28.8099 16.3151 26.9214 14.6964 25.3036C13.0782 23.6859 11.1906 22.4121 9.03309 21.4833C8.20814 21.1283 7.35608 20.8399 6.48509 20.621C6.34667 20.5864 6.22377 20.5065 6.13589 20.3941C6.04801 20.2817 6.00018 20.1432 6 20.0005C6.00024 19.8578 6.04808 19.7192 6.13594 19.6067C6.2238 19.4942 6.34667 19.4143 6.48509 19.3795C7.35612 19.1607 8.20819 18.8723 9.03309 18.5172C11.1906 17.5888 13.0786 16.3146 14.6964 14.6968C16.3141 13.0791 17.5884 11.191 18.5167 9.03356C18.8719 8.20862 19.1604 7.35656 19.3795 6.48556C19.4508 6.2005 19.7064 6 20 6Z" fill="url(#paint0_linear_3892_95663)"/>
</mask>
<g mask="url(#mask0_3892_95663)">
<g filter="url(#filter0_f_3892_95663)">
<path d="M3.47232 27.8921C6.70753 29.0411 10.426 26.8868 11.7778 23.0804C13.1296 19.274 11.6028 15.2569 8.36763 14.108C5.13242 12.959 1.41391 15.1133 0.06211 18.9197C-1.28969 22.7261 0.23711 26.7432 3.47232 27.8921Z" fill="#FFE432"/>
</g>
<g filter="url(#filter1_f_3892_95663)">
<path d="M17.8359 15.341C22.2806 15.341 25.8838 11.6588 25.8838 7.11644C25.8838 2.57412 22.2806 -1.10815 17.8359 -1.10815C13.3912 -1.10815 9.78809 2.57412 9.78809 7.11644C9.78809 11.6588 13.3912 15.341 17.8359 15.341Z" fill="#FC413D"/>
</g>
<g filter="url(#filter2_f_3892_95663)">
<path d="M14.7081 41.6431C19.3478 41.4163 22.8707 36.3599 22.5768 30.3493C22.283 24.3387 18.2836 19.65 13.644 19.8769C9.00433 20.1037 5.48139 25.1601 5.77525 31.1707C6.06911 37.1813 10.0685 41.87 14.7081 41.6431Z" fill="#00B95C"/>
</g>
<g filter="url(#filter3_f_3892_95663)">
<path d="M14.7081 41.6431C19.3478 41.4163 22.8707 36.3599 22.5768 30.3493C22.283 24.3387 18.2836 19.65 13.644 19.8769C9.00433 20.1037 5.48139 25.1601 5.77525 31.1707C6.06911 37.1813 10.0685 41.87 14.7081 41.6431Z" fill="#00B95C"/>
</g>
<g filter="url(#filter4_f_3892_95663)">
<path d="M19.355 38.0071C23.2447 35.6405 24.2857 30.2506 21.6803 25.9684C19.0748 21.6862 13.8095 20.1334 9.91983 22.5C6.03016 24.8666 4.98909 30.2565 7.59454 34.5387C10.2 38.8209 15.4653 40.3738 19.355 38.0071Z" fill="#00B95C"/>
</g>
<g filter="url(#filter5_f_3892_95663)">
<path d="M35.0759 24.5504C39.4477 24.5504 42.9917 21.1377 42.9917 16.9278C42.9917 12.7179 39.4477 9.30518 35.0759 9.30518C30.7042 9.30518 27.1602 12.7179 27.1602 16.9278C27.1602 21.1377 30.7042 24.5504 35.0759 24.5504Z" fill="#3186FF"/>
</g>
<g filter="url(#filter6_f_3892_95663)">
<path d="M0.362818 23.6667C4.3882 26.7279 10.2688 25.7676 13.4976 21.5219C16.7264 17.2762 16.0806 11.3528 12.0552 8.29156C8.02982 5.23037 2.14917 6.19062 -1.07959 10.4364C-4.30835 14.6821 -3.66256 20.6055 0.362818 23.6667Z" fill="#FBBC04"/>
</g>
<g filter="url(#filter7_f_3892_95663)">
<path d="M20.9877 28.1903C25.7924 31.4936 32.1612 30.5732 35.2128 26.1346C38.2644 21.696 36.8432 15.4199 32.0385 12.1166C27.2338 8.81334 20.865 9.73372 17.8134 14.1723C14.7618 18.611 16.183 24.887 20.9877 28.1903Z" fill="#3186FF"/>
</g>
<g filter="url(#filter8_f_3892_95663)">
<path d="M29.7231 4.99175C30.9455 6.65415 29.3748 9.88535 26.2149 12.2096C23.0549 14.5338 19.5026 15.0707 18.2801 13.4088C17.0576 11.7468 18.6284 8.51514 21.7883 6.19092C24.9482 3.86717 28.5006 3.32982 29.7231 4.99175Z" fill="#749BFF"/>
</g>
<g filter="url(#filter9_f_3892_95663)">
<path d="M19.6891 12.9486C24.5759 8.41581 26.2531 2.27858 23.4354 -0.759249C20.6176 -3.79708 14.3718 -2.58516 9.485 1.94765C4.59823 6.48046 2.92099 12.6177 5.73879 15.6555C8.55658 18.6933 14.8024 17.4814 19.6891 12.9486Z" fill="#FC413D"/>
</g>
<g filter="url(#filter10_f_3892_95663)">
<path d="M9.6712 29.23C12.5757 31.3088 15.9102 31.6247 17.1191 29.9356C18.328 28.2465 16.9535 25.1921 14.049 23.1133C11.1446 21.0345 7.81003 20.7186 6.60113 22.4077C5.39223 24.0968 6.76675 27.1512 9.6712 29.23Z" fill="#FFEE48"/>
</g>
</g>
<defs>
<filter id="filter0_f_3892_95663" x="-3.44095" y="10.7885" width="18.7217" height="20.4229" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="1.50514" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter1_f_3892_95663" x="-4.76352" y="-15.6598" width="45.1989" height="45.5524" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="7.2758" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter2_f_3892_95663" x="-6.61209" y="7.49899" width="41.5757" height="46.522" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="6.18495" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter3_f_3892_95663" x="-6.61209" y="7.49899" width="41.5757" height="46.522" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="6.18495" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter4_f_3892_95663" x="-6.21073" y="9.02316" width="41.6959" height="42.4608" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="6.18495" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter5_f_3892_95663" x="15.405" y="-2.44994" width="39.3423" height="38.7556" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="5.87756" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter6_f_3892_95663" x="-13.7886" y="-4.15284" width="39.9951" height="40.2639" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="5.32691" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter7_f_3892_95663" x="6.6925" y="0.620963" width="39.6414" height="39.065" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="4.75678" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter8_f_3892_95663" x="9.35225" y="-4.48661" width="29.2984" height="27.3739" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="4.25649" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter9_f_3892_95663" x="-2.81919" y="-9.62339" width="34.8122" height="34.143" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3.59514" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<filter id="filter10_f_3892_95663" x="-2.73761" y="12.4221" width="29.1949" height="27.4994" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="4.44986" result="effect1_foregroundBlur_3892_95663"/>
</filter>
<linearGradient id="paint0_linear_3892_95663" x1="13.9595" y1="24.7349" x2="28.5025" y2="12.4738" gradientUnits="userSpaceOnUse">
<stop stop-color="#4893FC"/>
<stop offset="0.27" stop-color="#4893FC"/>
<stop offset="0.777" stop-color="#969DFF"/>
<stop offset="1" stop-color="#BD99FE"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -1,11 +0,0 @@
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="40" height="40" fill="white"/>
<g clip-path="url(#clip0_3892_95659)">
<path d="M15.745 24.54L26.715 16.35C27.254 15.95 28.022 16.106 28.279 16.73C29.628 20.018 29.025 23.971 26.341 26.685C23.658 29.399 19.924 29.995 16.511 28.639L12.783 30.384C18.13 34.081 24.623 33.166 28.681 29.06C31.9 25.805 32.897 21.368 31.965 17.367L31.973 17.376C30.622 11.498 32.305 9.149 35.755 4.345L36 4L31.46 8.59V8.576L15.743 24.544M13.48 26.531C9.643 22.824 10.305 17.085 13.58 13.776C16 11.327 19.968 10.328 23.432 11.797L27.152 10.06C26.482 9.57 25.622 9.043 24.637 8.673C20.182 6.819 14.848 7.742 11.227 11.401C7.744 14.924 6.648 20.341 8.53 24.962C9.935 28.416 7.631 30.86 5.31 33.326C4.49 34.2 3.666 35.074 3 36L13.478 26.534" fill="black"/>
</g>
<defs>
<clipPath id="clip0_3892_95659">
<rect width="33" height="32" fill="white" transform="translate(3 4)"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 981 B

View File

@ -1,17 +0,0 @@
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g clip-path="url(#clip0_3892_83671)">
<path d="M1 13C1 9.27247 1 7.4087 1.60896 5.93853C2.42092 3.97831 3.97831 2.42092 5.93853 1.60896C7.4087 1 9.27247 1 13 1C16.7275 1 18.5913 1 20.0615 1.60896C22.0217 2.42092 23.5791 3.97831 24.391 5.93853C25 7.4087 25 9.27247 25 13C25 16.7275 25 18.5913 24.391 20.0615C23.5791 22.0217 22.0217 23.5791 20.0615 24.391C18.5913 25 16.7275 25 13 25C9.27247 25 7.4087 25 5.93853 24.391C3.97831 23.5791 2.42092 22.0217 1.60896 20.0615C1 18.5913 1 16.7275 1 13Z" fill="white"/>
<rect width="24" height="24" transform="translate(1 1)" fill="url(#pattern0_3892_83671)"/>
<rect width="24" height="24" transform="translate(1 1)" fill="white" fill-opacity="0.01"/>
</g>
<path d="M13 0.75C14.8603 0.75 16.2684 0.750313 17.3945 0.827148C18.5228 0.904144 19.3867 1.05876 20.1572 1.37793C22.1787 2.21525 23.7847 3.82133 24.6221 5.84277C24.9412 6.61333 25.0959 7.47723 25.1729 8.60547C25.2497 9.73161 25.25 11.1397 25.25 13C25.25 14.8603 25.2497 16.2684 25.1729 17.3945C25.0959 18.5228 24.9412 19.3867 24.6221 20.1572C23.7847 22.1787 22.1787 23.7847 20.1572 24.6221C19.3867 24.9412 18.5228 25.0959 17.3945 25.1729C16.2684 25.2497 14.8603 25.25 13 25.25C11.1397 25.25 9.73161 25.2497 8.60547 25.1729C7.47723 25.0959 6.61333 24.9412 5.84277 24.6221C3.82133 23.7847 2.21525 22.1787 1.37793 20.1572C1.05876 19.3867 0.904144 18.5228 0.827148 17.3945C0.750313 16.2684 0.75 14.8603 0.75 13C0.75 11.1397 0.750313 9.73161 0.827148 8.60547C0.904144 7.47723 1.05876 6.61333 1.37793 5.84277C2.21525 3.82133 3.82133 2.21525 5.84277 1.37793C6.61333 1.05876 7.47723 0.904144 8.60547 0.827148C9.73161 0.750313 11.1397 0.75 13 0.75Z" stroke="#101828" stroke-opacity="0.08" stroke-width="0.5"/>
<defs>
<pattern id="pattern0_3892_83671" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_3892_83671" transform="scale(0.00625)"/>
</pattern>
<clipPath id="clip0_3892_83671">
<path d="M1 13C1 9.27247 1 7.4087 1.60896 5.93853C2.42092 3.97831 3.97831 2.42092 5.93853 1.60896C7.4087 1 9.27247 1 13 1C16.7275 1 18.5913 1 20.0615 1.60896C22.0217 2.42092 23.5791 3.97831 24.391 5.93853C25 7.4087 25 9.27247 25 13C25 16.7275 25 18.5913 24.391 20.0615C23.5791 22.0217 22.0217 23.5791 20.0615 24.391C18.5913 25 16.7275 25 13 25C9.27247 25 7.4087 25 5.93853 24.391C3.97831 23.5791 2.42092 22.0217 1.60896 20.0615C1 18.5913 1 16.7275 1 13Z" fill="white"/>
</clipPath>
<image id="image0_3892_83671" width="160" height="160" preserveAspectRatio="none" xlink:href="data:image/png;base64,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"/>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -1,36 +0,0 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "40",
"height": "40",
"viewBox": "0 0 40 40",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "40",
"height": "40",
"fill": "white"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M25.7926 10.1311H21.5089L29.3208 29.869H33.6045L25.7926 10.1311ZM13.4164 10.1311L5.60449 29.869H9.97273L11.5703 25.724H19.743L21.3405 29.869H25.7087L17.8969 10.1311H13.4164ZM12.9834 22.0583L15.6566 15.1217L18.3299 22.0583H12.9834Z",
"fill": "black"
},
"children": []
}
]
},
"name": "AnthropicShortLight"
}

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './AnthropicShortLight.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'AnthropicShortLight'
export default Icon

View File

@ -1,36 +0,0 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "40",
"height": "40",
"viewBox": "0 0 40 40",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "40",
"height": "40",
"fill": "white"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M36.6676 11.2917C36.3316 11.1277 36.1871 11.4402 35.9906 11.599C35.9242 11.6511 35.8668 11.7188 35.8108 11.7787C35.3199 12.3048 34.747 12.6485 33.9996 12.6068C32.9046 12.5469 31.971 12.8907 31.1455 13.7293C30.9696 12.6954 30.3863 12.0782 29.4996 11.6824C29.0348 11.4766 28.5647 11.2709 28.2406 10.823C28.0127 10.5053 27.9515 10.1511 27.8368 9.80214C27.7652 9.59121 27.6923 9.37506 27.4502 9.33861C27.1871 9.29694 27.0843 9.51829 26.9814 9.70318C26.5674 10.4584 26.4084 11.2917 26.4228 12.1355C26.4592 14.0313 27.26 15.5417 28.8486 16.6173C29.0296 16.7397 29.0764 16.8646 29.0191 17.0443C28.9111 17.4141 28.7822 17.7735 28.6676 18.1433C28.596 18.3803 28.4879 18.4323 28.2354 18.3282C27.363 17.9637 26.609 17.4246 25.9436 16.7709C24.8135 15.6771 23.7914 14.4689 22.5166 13.5235C22.2171 13.3021 21.919 13.0964 21.609 12.9011C20.3082 11.6355 21.7796 10.5964 22.1194 10.474C22.4762 10.3464 22.2431 9.9037 21.092 9.90891C19.9423 9.91413 18.889 10.2995 17.5478 10.8126C17.3512 10.8907 17.1455 10.948 16.9332 10.9922C15.7158 10.7631 14.4515 10.711 13.1298 10.8594C10.6428 11.1381 8.65587 12.3152 7.19493 14.3255C5.44102 16.7397 5.02826 19.4845 5.53347 22.349C6.06473 25.3646 7.60249 27.8646 9.96707 29.8178C12.4176 31.8413 15.2406 32.8334 18.4606 32.6433C20.4163 32.5313 22.5947 32.2683 25.0504 30.1875C25.6702 30.4949 26.3199 30.6173 27.3994 30.711C28.2302 30.7891 29.0296 30.6694 29.6494 30.5417C30.6194 30.3361 30.5518 29.4375 30.2015 29.2709C27.3578 27.9454 27.9814 28.4845 27.4136 28.0495C28.859 26.3361 31.0374 24.5574 31.889 18.797C31.9554 18.3386 31.898 18.0522 31.889 17.6798C31.8838 17.4558 31.9346 17.3673 32.1923 17.3413C32.9046 17.2605 33.596 17.0651 34.2314 16.7137C36.0739 15.7058 36.816 14.0522 36.9918 12.0678C37.0179 11.7657 36.9866 11.4506 36.6676 11.2917ZM20.613 29.1485C17.8564 26.9793 16.5204 26.2657 15.9684 26.297C15.4527 26.3255 15.5452 26.9167 15.6584 27.3022C15.777 27.6823 15.9319 27.9454 16.1494 28.2787C16.2991 28.5001 16.402 28.8307 15.9996 29.0755C15.1116 29.6277 13.5687 28.8907 13.4958 28.8542C11.7001 27.797 10.1988 26.3985 9.14025 24.487C8.11941 22.6459 7.52566 20.6719 7.42801 18.5651C7.40197 18.0547 7.5517 17.875 8.05691 17.7839C8.72227 17.6615 9.40978 17.6355 10.0751 17.7318C12.8876 18.1433 15.2822 19.4037 17.2887 21.3959C18.4346 22.5339 19.3018 23.8907 20.195 25.2162C21.1442 26.6251 22.1663 27.9662 23.4671 29.0651C23.9254 29.4506 24.2926 29.7449 24.6428 29.961C23.5856 30.0782 21.8199 30.1042 20.613 29.1485ZM21.9332 20.6407C21.9332 20.4141 22.1142 20.2345 22.342 20.2345C22.3928 20.2345 22.4398 20.2449 22.4814 20.2605C22.5374 20.2813 22.5895 20.3126 22.6299 20.3594C22.7027 20.4298 22.7444 20.5339 22.7444 20.6407C22.7444 20.8673 22.5635 21.047 22.3368 21.047C22.109 21.047 21.9332 20.8673 21.9332 20.6407ZM26.036 22.7501C25.7731 22.8569 25.51 22.9506 25.2575 22.961C24.8655 22.9793 24.4371 22.8203 24.204 22.6251C23.8434 22.323 23.5856 22.1537 23.4762 21.6225C23.4306 21.3959 23.4567 21.047 23.497 20.8465C23.5908 20.4141 23.4866 20.1381 23.1832 19.8855C22.9346 19.6798 22.6207 19.6251 22.2744 19.6251C22.1455 19.6251 22.027 19.5678 21.9384 19.5209C21.7939 19.4479 21.6754 19.2683 21.7887 19.047C21.8251 18.9766 22.001 18.8022 22.0426 18.7709C22.5114 18.5027 23.053 18.5913 23.5543 18.7918C24.0191 18.9818 24.3694 19.3307 24.8746 19.823C25.3915 20.4194 25.484 20.5861 25.7783 21.0313C26.01 21.3829 26.2223 21.7422 26.3668 22.1537C26.454 22.4089 26.3408 22.6198 26.036 22.7501Z",
"fill": "#4D6BFE"
},
"children": []
}
]
},
"name": "Deepseek"
}

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './Deepseek.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Deepseek'
export default Icon

View File

@ -1,807 +0,0 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "40",
"height": "40",
"viewBox": "0 0 40 40",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "40",
"height": "40",
"fill": "white"
},
"children": []
},
{
"type": "element",
"name": "mask",
"attributes": {
"id": "mask0_3892_95663",
"style": "mask-type:alpha",
"maskUnits": "userSpaceOnUse",
"x": "6",
"y": "6",
"width": "28",
"height": "29"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M20 6C20.2936 6 20.5488 6.2005 20.6205 6.48556C20.8393 7.3566 21.1277 8.20866 21.4828 9.03356C22.4116 11.191 23.6854 13.0791 25.3032 14.6968C26.9218 16.3146 28.8095 17.5888 30.9664 18.5172C31.7941 18.8735 32.6436 19.16 33.5149 19.3795C33.6533 19.4143 33.7762 19.4942 33.8641 19.6067C33.9519 19.7192 33.9998 19.8578 34 20.0005C34 20.2941 33.7995 20.5492 33.5149 20.621C32.6437 20.8399 31.7915 21.1282 30.9664 21.4833C28.8095 22.4121 26.9209 23.6859 25.3032 25.3036C23.6854 26.9223 22.4116 28.8099 21.4828 30.9669C21.1278 31.7919 20.8394 32.6439 20.6205 33.5149C20.586 33.6534 20.5062 33.7764 20.3937 33.8644C20.2813 33.9524 20.1427 34.0003 20 34.0005C19.8572 34.0003 19.7186 33.9525 19.6062 33.8645C19.4937 33.7765 19.414 33.6535 19.3795 33.5149C19.1605 32.6439 18.872 31.7918 18.5167 30.9669C17.5884 28.8099 16.3151 26.9214 14.6964 25.3036C13.0782 23.6859 11.1906 22.4121 9.03309 21.4833C8.20814 21.1283 7.35608 20.8399 6.48509 20.621C6.34667 20.5864 6.22377 20.5065 6.13589 20.3941C6.04801 20.2817 6.00018 20.1432 6 20.0005C6.00024 19.8578 6.04808 19.7192 6.13594 19.6067C6.2238 19.4942 6.34667 19.4143 6.48509 19.3795C7.35612 19.1607 8.20819 18.8723 9.03309 18.5172C11.1906 17.5888 13.0786 16.3146 14.6964 14.6968C16.3141 13.0791 17.5884 11.191 18.5167 9.03356C18.8719 8.20862 19.1604 7.35656 19.3795 6.48556C19.4508 6.2005 19.7064 6 20 6Z",
"fill": "black"
},
"children": []
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M20 6C20.2936 6 20.5488 6.2005 20.6205 6.48556C20.8393 7.3566 21.1277 8.20866 21.4828 9.03356C22.4116 11.191 23.6854 13.0791 25.3032 14.6968C26.9218 16.3146 28.8095 17.5888 30.9664 18.5172C31.7941 18.8735 32.6436 19.16 33.5149 19.3795C33.6533 19.4143 33.7762 19.4942 33.8641 19.6067C33.9519 19.7192 33.9998 19.8578 34 20.0005C34 20.2941 33.7995 20.5492 33.5149 20.621C32.6437 20.8399 31.7915 21.1282 30.9664 21.4833C28.8095 22.4121 26.9209 23.6859 25.3032 25.3036C23.6854 26.9223 22.4116 28.8099 21.4828 30.9669C21.1278 31.7919 20.8394 32.6439 20.6205 33.5149C20.586 33.6534 20.5062 33.7764 20.3937 33.8644C20.2813 33.9524 20.1427 34.0003 20 34.0005C19.8572 34.0003 19.7186 33.9525 19.6062 33.8645C19.4937 33.7765 19.414 33.6535 19.3795 33.5149C19.1605 32.6439 18.872 31.7918 18.5167 30.9669C17.5884 28.8099 16.3151 26.9214 14.6964 25.3036C13.0782 23.6859 11.1906 22.4121 9.03309 21.4833C8.20814 21.1283 7.35608 20.8399 6.48509 20.621C6.34667 20.5864 6.22377 20.5065 6.13589 20.3941C6.04801 20.2817 6.00018 20.1432 6 20.0005C6.00024 19.8578 6.04808 19.7192 6.13594 19.6067C6.2238 19.4942 6.34667 19.4143 6.48509 19.3795C7.35612 19.1607 8.20819 18.8723 9.03309 18.5172C11.1906 17.5888 13.0786 16.3146 14.6964 14.6968C16.3141 13.0791 17.5884 11.191 18.5167 9.03356C18.8719 8.20862 19.1604 7.35656 19.3795 6.48556C19.4508 6.2005 19.7064 6 20 6Z",
"fill": "url(#paint0_linear_3892_95663)"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"mask": "url(#mask0_3892_95663)"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter0_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M3.47232 27.8921C6.70753 29.0411 10.426 26.8868 11.7778 23.0804C13.1296 19.274 11.6028 15.2569 8.36763 14.108C5.13242 12.959 1.41391 15.1133 0.06211 18.9197C-1.28969 22.7261 0.23711 26.7432 3.47232 27.8921Z",
"fill": "#FFE432"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter1_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M17.8359 15.341C22.2806 15.341 25.8838 11.6588 25.8838 7.11644C25.8838 2.57412 22.2806 -1.10815 17.8359 -1.10815C13.3912 -1.10815 9.78809 2.57412 9.78809 7.11644C9.78809 11.6588 13.3912 15.341 17.8359 15.341Z",
"fill": "#FC413D"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter2_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M14.7081 41.6431C19.3478 41.4163 22.8707 36.3599 22.5768 30.3493C22.283 24.3387 18.2836 19.65 13.644 19.8769C9.00433 20.1037 5.48139 25.1601 5.77525 31.1707C6.06911 37.1813 10.0685 41.87 14.7081 41.6431Z",
"fill": "#00B95C"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter3_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M14.7081 41.6431C19.3478 41.4163 22.8707 36.3599 22.5768 30.3493C22.283 24.3387 18.2836 19.65 13.644 19.8769C9.00433 20.1037 5.48139 25.1601 5.77525 31.1707C6.06911 37.1813 10.0685 41.87 14.7081 41.6431Z",
"fill": "#00B95C"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter4_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M19.355 38.0071C23.2447 35.6405 24.2857 30.2506 21.6803 25.9684C19.0748 21.6862 13.8095 20.1334 9.91983 22.5C6.03016 24.8666 4.98909 30.2565 7.59454 34.5387C10.2 38.8209 15.4653 40.3738 19.355 38.0071Z",
"fill": "#00B95C"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter5_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M35.0759 24.5504C39.4477 24.5504 42.9917 21.1377 42.9917 16.9278C42.9917 12.7179 39.4477 9.30518 35.0759 9.30518C30.7042 9.30518 27.1602 12.7179 27.1602 16.9278C27.1602 21.1377 30.7042 24.5504 35.0759 24.5504Z",
"fill": "#3186FF"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter6_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M0.362818 23.6667C4.3882 26.7279 10.2688 25.7676 13.4976 21.5219C16.7264 17.2762 16.0806 11.3528 12.0552 8.29156C8.02982 5.23037 2.14917 6.19062 -1.07959 10.4364C-4.30835 14.6821 -3.66256 20.6055 0.362818 23.6667Z",
"fill": "#FBBC04"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter7_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M20.9877 28.1903C25.7924 31.4936 32.1612 30.5732 35.2128 26.1346C38.2644 21.696 36.8432 15.4199 32.0385 12.1166C27.2338 8.81334 20.865 9.73372 17.8134 14.1723C14.7618 18.611 16.183 24.887 20.9877 28.1903Z",
"fill": "#3186FF"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter8_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M29.7231 4.99175C30.9455 6.65415 29.3748 9.88535 26.2149 12.2096C23.0549 14.5338 19.5026 15.0707 18.2801 13.4088C17.0576 11.7468 18.6284 8.51514 21.7883 6.19092C24.9482 3.86717 28.5006 3.32982 29.7231 4.99175Z",
"fill": "#749BFF"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter9_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M19.6891 12.9486C24.5759 8.41581 26.2531 2.27858 23.4354 -0.759249C20.6176 -3.79708 14.3718 -2.58516 9.485 1.94765C4.59823 6.48046 2.92099 12.6177 5.73879 15.6555C8.55658 18.6933 14.8024 17.4814 19.6891 12.9486Z",
"fill": "#FC413D"
},
"children": []
}
]
},
{
"type": "element",
"name": "g",
"attributes": {
"filter": "url(#filter10_f_3892_95663)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M9.6712 29.23C12.5757 31.3088 15.9102 31.6247 17.1191 29.9356C18.328 28.2465 16.9535 25.1921 14.049 23.1133C11.1446 21.0345 7.81003 20.7186 6.60113 22.4077C5.39223 24.0968 6.76675 27.1512 9.6712 29.23Z",
"fill": "#FFEE48"
},
"children": []
}
]
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter0_f_3892_95663",
"x": "-3.44095",
"y": "10.7885",
"width": "18.7217",
"height": "20.4229",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "1.50514",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter1_f_3892_95663",
"x": "-4.76352",
"y": "-15.6598",
"width": "45.1989",
"height": "45.5524",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "7.2758",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter2_f_3892_95663",
"x": "-6.61209",
"y": "7.49899",
"width": "41.5757",
"height": "46.522",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "6.18495",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter3_f_3892_95663",
"x": "-6.61209",
"y": "7.49899",
"width": "41.5757",
"height": "46.522",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "6.18495",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter4_f_3892_95663",
"x": "-6.21073",
"y": "9.02316",
"width": "41.6959",
"height": "42.4608",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "6.18495",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter5_f_3892_95663",
"x": "15.405",
"y": "-2.44994",
"width": "39.3423",
"height": "38.7556",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "5.87756",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter6_f_3892_95663",
"x": "-13.7886",
"y": "-4.15284",
"width": "39.9951",
"height": "40.2639",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "5.32691",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter7_f_3892_95663",
"x": "6.6925",
"y": "0.620963",
"width": "39.6414",
"height": "39.065",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "4.75678",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter8_f_3892_95663",
"x": "9.35225",
"y": "-4.48661",
"width": "29.2984",
"height": "27.3739",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "4.25649",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter9_f_3892_95663",
"x": "-2.81919",
"y": "-9.62339",
"width": "34.8122",
"height": "34.143",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "3.59514",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "filter",
"attributes": {
"id": "filter10_f_3892_95663",
"x": "-2.73761",
"y": "12.4221",
"width": "29.1949",
"height": "27.4994",
"filterUnits": "userSpaceOnUse",
"color-interpolation-filters": "sRGB"
},
"children": [
{
"type": "element",
"name": "feFlood",
"attributes": {
"flood-opacity": "0",
"result": "BackgroundImageFix"
},
"children": []
},
{
"type": "element",
"name": "feBlend",
"attributes": {
"mode": "normal",
"in": "SourceGraphic",
"in2": "BackgroundImageFix",
"result": "shape"
},
"children": []
},
{
"type": "element",
"name": "feGaussianBlur",
"attributes": {
"stdDeviation": "4.44986",
"result": "effect1_foregroundBlur_3892_95663"
},
"children": []
}
]
},
{
"type": "element",
"name": "linearGradient",
"attributes": {
"id": "paint0_linear_3892_95663",
"x1": "13.9595",
"y1": "24.7349",
"x2": "28.5025",
"y2": "12.4738",
"gradientUnits": "userSpaceOnUse"
},
"children": [
{
"type": "element",
"name": "stop",
"attributes": {
"stop-color": "#4893FC"
},
"children": []
},
{
"type": "element",
"name": "stop",
"attributes": {
"offset": "0.27",
"stop-color": "#4893FC"
},
"children": []
},
{
"type": "element",
"name": "stop",
"attributes": {
"offset": "0.777",
"stop-color": "#969DFF"
},
"children": []
},
{
"type": "element",
"name": "stop",
"attributes": {
"offset": "1",
"stop-color": "#BD99FE"
},
"children": []
}
]
}
]
}
]
},
"name": "Gemini"
}

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './Gemini.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Gemini'
export default Icon

View File

@ -1,72 +0,0 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "40",
"height": "40",
"viewBox": "0 0 40 40",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "40",
"height": "40",
"fill": "white"
},
"children": []
},
{
"type": "element",
"name": "g",
"attributes": {
"clip-path": "url(#clip0_3892_95659)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M15.745 24.54L26.715 16.35C27.254 15.95 28.022 16.106 28.279 16.73C29.628 20.018 29.025 23.971 26.341 26.685C23.658 29.399 19.924 29.995 16.511 28.639L12.783 30.384C18.13 34.081 24.623 33.166 28.681 29.06C31.9 25.805 32.897 21.368 31.965 17.367L31.973 17.376C30.622 11.498 32.305 9.149 35.755 4.345L36 4L31.46 8.59V8.576L15.743 24.544M13.48 26.531C9.643 22.824 10.305 17.085 13.58 13.776C16 11.327 19.968 10.328 23.432 11.797L27.152 10.06C26.482 9.57 25.622 9.043 24.637 8.673C20.182 6.819 14.848 7.742 11.227 11.401C7.744 14.924 6.648 20.341 8.53 24.962C9.935 28.416 7.631 30.86 5.31 33.326C4.49 34.2 3.666 35.074 3 36L13.478 26.534",
"fill": "black"
},
"children": []
}
]
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_3892_95659"
},
"children": [
{
"type": "element",
"name": "rect",
"attributes": {
"width": "33",
"height": "32",
"fill": "white",
"transform": "translate(3 4)"
},
"children": []
}
]
}
]
}
]
},
"name": "Grok"
}

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './Grok.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Grok'
export default Icon

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './OpenaiBlue.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiBlue'
export default Icon

View File

@ -1,128 +0,0 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "26",
"height": "26",
"viewBox": "0 0 26 26",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"clip-path": "url(#clip0_3892_83671)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M1 13C1 9.27247 1 7.4087 1.60896 5.93853C2.42092 3.97831 3.97831 2.42092 5.93853 1.60896C7.4087 1 9.27247 1 13 1C16.7275 1 18.5913 1 20.0615 1.60896C22.0217 2.42092 23.5791 3.97831 24.391 5.93853C25 7.4087 25 9.27247 25 13C25 16.7275 25 18.5913 24.391 20.0615C23.5791 22.0217 22.0217 23.5791 20.0615 24.391C18.5913 25 16.7275 25 13 25C9.27247 25 7.4087 25 5.93853 24.391C3.97831 23.5791 2.42092 22.0217 1.60896 20.0615C1 18.5913 1 16.7275 1 13Z",
"fill": "white"
},
"children": []
},
{
"type": "element",
"name": "rect",
"attributes": {
"width": "24",
"height": "24",
"transform": "translate(1 1)",
"fill": "url(#pattern0_3892_83671)"
},
"children": []
},
{
"type": "element",
"name": "rect",
"attributes": {
"width": "24",
"height": "24",
"transform": "translate(1 1)",
"fill": "white",
"fill-opacity": "0.01"
},
"children": []
}
]
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M13 0.75C14.8603 0.75 16.2684 0.750313 17.3945 0.827148C18.5228 0.904144 19.3867 1.05876 20.1572 1.37793C22.1787 2.21525 23.7847 3.82133 24.6221 5.84277C24.9412 6.61333 25.0959 7.47723 25.1729 8.60547C25.2497 9.73161 25.25 11.1397 25.25 13C25.25 14.8603 25.2497 16.2684 25.1729 17.3945C25.0959 18.5228 24.9412 19.3867 24.6221 20.1572C23.7847 22.1787 22.1787 23.7847 20.1572 24.6221C19.3867 24.9412 18.5228 25.0959 17.3945 25.1729C16.2684 25.2497 14.8603 25.25 13 25.25C11.1397 25.25 9.73161 25.2497 8.60547 25.1729C7.47723 25.0959 6.61333 24.9412 5.84277 24.6221C3.82133 23.7847 2.21525 22.1787 1.37793 20.1572C1.05876 19.3867 0.904144 18.5228 0.827148 17.3945C0.750313 16.2684 0.75 14.8603 0.75 13C0.75 11.1397 0.750313 9.73161 0.827148 8.60547C0.904144 7.47723 1.05876 6.61333 1.37793 5.84277C2.21525 3.82133 3.82133 2.21525 5.84277 1.37793C6.61333 1.05876 7.47723 0.904144 8.60547 0.827148C9.73161 0.750313 11.1397 0.75 13 0.75Z",
"stroke": "#101828",
"stroke-opacity": "0.08",
"stroke-width": "0.5"
},
"children": []
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "pattern",
"attributes": {
"id": "pattern0_3892_83671",
"patternContentUnits": "objectBoundingBox",
"width": "1",
"height": "1"
},
"children": [
{
"type": "element",
"name": "use",
"attributes": {
"xlink:href": "#image0_3892_83671",
"transform": "scale(0.00625)"
},
"children": []
}
]
},
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_3892_83671"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M1 13C1 9.27247 1 7.4087 1.60896 5.93853C2.42092 3.97831 3.97831 2.42092 5.93853 1.60896C7.4087 1 9.27247 1 13 1C16.7275 1 18.5913 1 20.0615 1.60896C22.0217 2.42092 23.5791 3.97831 24.391 5.93853C25 7.4087 25 9.27247 25 13C25 16.7275 25 18.5913 24.391 20.0615C23.5791 22.0217 22.0217 23.5791 20.0615 24.391C18.5913 25 16.7275 25 13 25C9.27247 25 7.4087 25 5.93853 24.391C3.97831 23.5791 2.42092 22.0217 1.60896 20.0615C1 18.5913 1 16.7275 1 13Z",
"fill": "white"
},
"children": []
}
]
},
{
"type": "element",
"name": "image",
"attributes": {
"id": "image0_3892_83671",
"width": "160",
"height": "160",
"preserveAspectRatio": "none",
"xlink:href": "data:image/png;base64,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"
},
"children": []
}
]
}
]
},
"name": "OpenaiSmall"
}

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './OpenaiSmall.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiSmall'
export default Icon

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './OpenaiTeal.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiTeal'
export default Icon

File diff suppressed because one or more lines are too long

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './OpenaiViolet.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'OpenaiViolet'
export default Icon

View File

@ -1,128 +0,0 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "25",
"height": "25",
"viewBox": "0 0 25 25",
"fill": "none",
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink"
},
"children": [
{
"type": "element",
"name": "g",
"attributes": {
"clip-path": "url(#clip0_6305_73327)"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M0.5 12.5C0.5 8.77247 0.5 6.9087 1.10896 5.43853C1.92092 3.47831 3.47831 1.92092 5.43853 1.10896C6.9087 0.5 8.77247 0.5 12.5 0.5C16.2275 0.5 18.0913 0.5 19.5615 1.10896C21.5217 1.92092 23.0791 3.47831 23.891 5.43853C24.5 6.9087 24.5 8.77247 24.5 12.5C24.5 16.2275 24.5 18.0913 23.891 19.5615C23.0791 21.5217 21.5217 23.0791 19.5615 23.891C18.0913 24.5 16.2275 24.5 12.5 24.5C8.77247 24.5 6.9087 24.5 5.43853 23.891C3.47831 23.0791 1.92092 21.5217 1.10896 19.5615C0.5 18.0913 0.5 16.2275 0.5 12.5Z",
"fill": "white"
},
"children": []
},
{
"type": "element",
"name": "rect",
"attributes": {
"width": "24",
"height": "24",
"transform": "translate(0.5 0.5)",
"fill": "url(#pattern0_6305_73327)"
},
"children": []
},
{
"type": "element",
"name": "rect",
"attributes": {
"width": "24",
"height": "24",
"transform": "translate(0.5 0.5)",
"fill": "white",
"fill-opacity": "0.01"
},
"children": []
}
]
},
{
"type": "element",
"name": "path",
"attributes": {
"d": "M12.5 0.25C14.3603 0.25 15.7684 0.250313 16.8945 0.327148C18.0228 0.404144 18.8867 0.558755 19.6572 0.87793C21.6787 1.71525 23.2847 3.32133 24.1221 5.34277C24.4412 6.11333 24.5959 6.97723 24.6729 8.10547C24.7497 9.23161 24.75 10.6397 24.75 12.5C24.75 14.3603 24.7497 15.7684 24.6729 16.8945C24.5959 18.0228 24.4412 18.8867 24.1221 19.6572C23.2847 21.6787 21.6787 23.2847 19.6572 24.1221C18.8867 24.4412 18.0228 24.5959 16.8945 24.6729C15.7684 24.7497 14.3603 24.75 12.5 24.75C10.6397 24.75 9.23161 24.7497 8.10547 24.6729C6.97723 24.5959 6.11333 24.4412 5.34277 24.1221C3.32133 23.2847 1.71525 21.6787 0.87793 19.6572C0.558755 18.8867 0.404144 18.0228 0.327148 16.8945C0.250313 15.7684 0.25 14.3603 0.25 12.5C0.25 10.6397 0.250313 9.23161 0.327148 8.10547C0.404144 6.97723 0.558755 6.11333 0.87793 5.34277C1.71525 3.32133 3.32133 1.71525 5.34277 0.87793C6.11333 0.558755 6.97723 0.404144 8.10547 0.327148C9.23161 0.250313 10.6397 0.25 12.5 0.25Z",
"stroke": "#101828",
"stroke-opacity": "0.08",
"stroke-width": "0.5"
},
"children": []
},
{
"type": "element",
"name": "defs",
"attributes": {},
"children": [
{
"type": "element",
"name": "pattern",
"attributes": {
"id": "pattern0_6305_73327",
"patternContentUnits": "objectBoundingBox",
"width": "1",
"height": "1"
},
"children": [
{
"type": "element",
"name": "use",
"attributes": {
"xlink:href": "#image0_6305_73327",
"transform": "scale(0.00625)"
},
"children": []
}
]
},
{
"type": "element",
"name": "clipPath",
"attributes": {
"id": "clip0_6305_73327"
},
"children": [
{
"type": "element",
"name": "path",
"attributes": {
"d": "M0.5 12.5C0.5 8.77247 0.5 6.9087 1.10896 5.43853C1.92092 3.47831 3.47831 1.92092 5.43853 1.10896C6.9087 0.5 8.77247 0.5 12.5 0.5C16.2275 0.5 18.0913 0.5 19.5615 1.10896C21.5217 1.92092 23.0791 3.47831 23.891 5.43853C24.5 6.9087 24.5 8.77247 24.5 12.5C24.5 16.2275 24.5 18.0913 23.891 19.5615C23.0791 21.5217 21.5217 23.0791 19.5615 23.891C18.0913 24.5 16.2275 24.5 12.5 24.5C8.77247 24.5 6.9087 24.5 5.43853 23.891C3.47831 23.0791 1.92092 21.5217 1.10896 19.5615C0.5 18.0913 0.5 16.2275 0.5 12.5Z",
"fill": "white"
},
"children": []
}
]
},
{
"type": "element",
"name": "image",
"attributes": {
"id": "image0_6305_73327",
"width": "160",
"height": "160",
"preserveAspectRatio": "none",
"xlink:href": "data:image/png;base64,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"
},
"children": []
}
]
}
]
},
"name": "Tongyi"
}

View File

@ -1,20 +0,0 @@
// GENERATE BY script
// DON NOT EDIT IT MANUALLY
import type { IconData } from '@/app/components/base/icons/IconBase'
import * as React from 'react'
import IconBase from '@/app/components/base/icons/IconBase'
import data from './Tongyi.json'
const Icon = (
{
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />
Icon.displayName = 'Tongyi'
export default Icon

View File

@ -1,7 +1,6 @@
export { default as Anthropic } from './Anthropic'
export { default as AnthropicDark } from './AnthropicDark'
export { default as AnthropicLight } from './AnthropicLight'
export { default as AnthropicShortLight } from './AnthropicShortLight'
export { default as AnthropicText } from './AnthropicText'
export { default as Azureai } from './Azureai'
export { default as AzureaiText } from './AzureaiText'
@ -13,11 +12,8 @@ export { default as Chatglm } from './Chatglm'
export { default as ChatglmText } from './ChatglmText'
export { default as Cohere } from './Cohere'
export { default as CohereText } from './CohereText'
export { default as Deepseek } from './Deepseek'
export { default as Gemini } from './Gemini'
export { default as Gpt3 } from './Gpt3'
export { default as Gpt4 } from './Gpt4'
export { default as Grok } from './Grok'
export { default as Huggingface } from './Huggingface'
export { default as HuggingfaceText } from './HuggingfaceText'
export { default as HuggingfaceTextHub } from './HuggingfaceTextHub'
@ -30,19 +26,14 @@ export { default as Localai } from './Localai'
export { default as LocalaiText } from './LocalaiText'
export { default as Microsoft } from './Microsoft'
export { default as OpenaiBlack } from './OpenaiBlack'
export { default as OpenaiBlue } from './OpenaiBlue'
export { default as OpenaiGreen } from './OpenaiGreen'
export { default as OpenaiSmall } from './OpenaiSmall'
export { default as OpenaiTeal } from './OpenaiTeal'
export { default as OpenaiText } from './OpenaiText'
export { default as OpenaiTransparent } from './OpenaiTransparent'
export { default as OpenaiViolet } from './OpenaiViolet'
export { default as OpenaiYellow } from './OpenaiYellow'
export { default as Openllm } from './Openllm'
export { default as OpenllmText } from './OpenllmText'
export { default as Replicate } from './Replicate'
export { default as ReplicateText } from './ReplicateText'
export { default as Tongyi } from './Tongyi'
export { default as XorbitsInference } from './XorbitsInference'
export { default as XorbitsInferenceText } from './XorbitsInferenceText'
export { default as Zhipuai } from './Zhipuai'

View File

@ -11,7 +11,7 @@ const Icon = (
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

View File

@ -11,7 +11,7 @@ const Icon = (
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

View File

@ -11,7 +11,7 @@ const Icon = (
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

View File

@ -11,7 +11,7 @@ const Icon = (
ref,
...props
}: React.SVGProps<SVGSVGElement> & {
ref?: React.RefObject<React.RefObject<HTMLOrSVGElement>>
ref?: React.RefObject<React.MutableRefObject<HTMLOrSVGElement>>
},
) => <IconBase {...props} ref={ref} data={data as IconData} />

View File

@ -1,16 +1,14 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "80px",
"height": "18px",
"viewBox": "0 0 80 18",
"version": "1.1",
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink"
"version": "1.1"
},
"isRootNode": true,
"children": [
{
"type": "element",

View File

@ -1,16 +1,14 @@
{
"icon": {
"type": "element",
"isRootNode": true,
"name": "svg",
"attributes": {
"width": "120px",
"height": "27px",
"width": "80px",
"height": "18px",
"viewBox": "0 0 80 18",
"version": "1.1",
"xmlns": "http://www.w3.org/2000/svg",
"xmlns:xlink": "http://www.w3.org/1999/xlink"
"version": "1.1"
},
"isRootNode": true,
"children": [
{
"type": "element",

View File

@ -75,9 +75,6 @@ const buildAppContext = (overrides: Partial<AppContextValue> = {}): AppContextVa
created_at: 0,
role: 'normal',
providers: [],
trial_credits: 200,
trial_credits_used: 0,
next_credit_reset_date: 0,
}
const langGeniusVersionInfo: LangGeniusVersionResponse = {
current_env: '',
@ -99,7 +96,6 @@ const buildAppContext = (overrides: Partial<AppContextValue> = {}): AppContextVa
mutateCurrentWorkspace: vi.fn(),
langGeniusVersionInfo,
isLoadingCurrentWorkspace: false,
isValidatingCurrentWorkspace: false,
}
const useSelector: AppContextValue['useSelector'] = selector => selector({ ...base, useSelector })
return {

View File

@ -0,0 +1,97 @@
'use client'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import { DataSourceType } from '@/models/datasets'
import { cn } from '@/utils/classnames'
import s from '../index.module.css'
type DataSourceTypeSelectorProps = {
currentType: DataSourceType
disabled: boolean
onChange: (type: DataSourceType) => void
onClearPreviews: (type: DataSourceType) => void
}
type DataSourceLabelKey
= | 'stepOne.dataSourceType.file'
| 'stepOne.dataSourceType.notion'
| 'stepOne.dataSourceType.web'
type DataSourceOption = {
type: DataSourceType
iconClass?: string
labelKey: DataSourceLabelKey
}
const DATA_SOURCE_OPTIONS: DataSourceOption[] = [
{
type: DataSourceType.FILE,
labelKey: 'stepOne.dataSourceType.file',
},
{
type: DataSourceType.NOTION,
iconClass: s.notion,
labelKey: 'stepOne.dataSourceType.notion',
},
{
type: DataSourceType.WEB,
iconClass: s.web,
labelKey: 'stepOne.dataSourceType.web',
},
]
/**
* Data source type selector component for choosing between file, notion, and web sources.
*/
function DataSourceTypeSelector({
currentType,
disabled,
onChange,
onClearPreviews,
}: DataSourceTypeSelectorProps) {
const { t } = useTranslation()
const isWebEnabled = ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL
const handleTypeChange = useCallback((type: DataSourceType) => {
if (disabled)
return
onChange(type)
onClearPreviews(type)
}, [disabled, onChange, onClearPreviews])
const visibleOptions = useMemo(() => DATA_SOURCE_OPTIONS.filter((option) => {
if (option.type === DataSourceType.WEB)
return isWebEnabled
return true
}), [isWebEnabled])
return (
<div className="mb-8 grid grid-cols-3 gap-4">
{visibleOptions.map(option => (
<div
key={option.type}
className={cn(
s.dataSourceItem,
'system-sm-medium',
currentType === option.type && s.active,
disabled && currentType !== option.type && s.disabled,
)}
onClick={() => handleTypeChange(option.type)}
>
<span className={cn(s.datasetIcon, option.iconClass)} />
<span
title={t(option.labelKey, { ns: 'datasetCreation' }) || undefined}
className="truncate"
>
{t(option.labelKey, { ns: 'datasetCreation' })}
</span>
</div>
))}
</div>
)
}
export default DataSourceTypeSelector

View File

@ -0,0 +1,3 @@
export { default as DataSourceTypeSelector } from './data-source-type-selector'
export { default as NextStepButton } from './next-step-button'
export { default as PreviewPanel } from './preview-panel'

View File

@ -0,0 +1,30 @@
'use client'
import { RiArrowRightLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
type NextStepButtonProps = {
disabled: boolean
onClick: () => void
}
/**
* Reusable next step button component for dataset creation flow.
*/
function NextStepButton({ disabled, onClick }: NextStepButtonProps) {
const { t } = useTranslation()
return (
<div className="flex max-w-[640px] justify-end gap-2">
<Button disabled={disabled} variant="primary" onClick={onClick}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('stepOne.button', { ns: 'datasetCreation' })}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
)
}
export default NextStepButton

View File

@ -0,0 +1,62 @@
'use client'
import type { NotionPage } from '@/models/common'
import type { CrawlResultItem } from '@/models/datasets'
import { useTranslation } from 'react-i18next'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import FilePreview from '../../file-preview'
import NotionPagePreview from '../../notion-page-preview'
import WebsitePreview from '../../website/preview'
type PreviewPanelProps = {
currentFile: File | undefined
currentNotionPage: NotionPage | undefined
currentWebsite: CrawlResultItem | undefined
notionCredentialId: string
isShowPlanUpgradeModal: boolean
hideFilePreview: () => void
hideNotionPagePreview: () => void
hideWebsitePreview: () => void
hidePlanUpgradeModal: () => void
}
/**
* Right panel component for displaying file, notion page, or website previews.
*/
function PreviewPanel({
currentFile,
currentNotionPage,
currentWebsite,
notionCredentialId,
isShowPlanUpgradeModal,
hideFilePreview,
hideNotionPagePreview,
hideWebsitePreview,
hidePlanUpgradeModal,
}: PreviewPanelProps) {
const { t } = useTranslation()
return (
<div className="h-full w-1/2 overflow-y-auto">
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
{currentNotionPage && (
<NotionPagePreview
currentPage={currentNotionPage}
hidePreview={hideNotionPagePreview}
notionCredentialId={notionCredentialId}
/>
)}
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
{isShowPlanUpgradeModal && (
<PlanUpgradeModal
show
onClose={hidePlanUpgradeModal}
title={t('upgrade.uploadMultiplePages.title', { ns: 'billing' })!}
description={t('upgrade.uploadMultiplePages.description', { ns: 'billing' })!}
/>
)}
</div>
)
}
export default PreviewPanel

View File

@ -0,0 +1,2 @@
export { default as usePreviewState } from './use-preview-state'
export type { PreviewActions, PreviewState, UsePreviewStateReturn } from './use-preview-state'

View File

@ -0,0 +1,70 @@
'use client'
import type { NotionPage } from '@/models/common'
import type { CrawlResultItem } from '@/models/datasets'
import { useCallback, useState } from 'react'
export type PreviewState = {
currentFile: File | undefined
currentNotionPage: NotionPage | undefined
currentWebsite: CrawlResultItem | undefined
}
export type PreviewActions = {
showFilePreview: (file: File) => void
hideFilePreview: () => void
showNotionPagePreview: (page: NotionPage) => void
hideNotionPagePreview: () => void
showWebsitePreview: (website: CrawlResultItem) => void
hideWebsitePreview: () => void
}
export type UsePreviewStateReturn = PreviewState & PreviewActions
/**
* Custom hook for managing preview state across different data source types.
* Handles file, notion page, and website preview visibility.
*/
function usePreviewState(): UsePreviewStateReturn {
const [currentFile, setCurrentFile] = useState<File | undefined>()
const [currentNotionPage, setCurrentNotionPage] = useState<NotionPage | undefined>()
const [currentWebsite, setCurrentWebsite] = useState<CrawlResultItem | undefined>()
const showFilePreview = useCallback((file: File) => {
setCurrentFile(file)
}, [])
const hideFilePreview = useCallback(() => {
setCurrentFile(undefined)
}, [])
const showNotionPagePreview = useCallback((page: NotionPage) => {
setCurrentNotionPage(page)
}, [])
const hideNotionPagePreview = useCallback(() => {
setCurrentNotionPage(undefined)
}, [])
const showWebsitePreview = useCallback((website: CrawlResultItem) => {
setCurrentWebsite(website)
}, [])
const hideWebsitePreview = useCallback(() => {
setCurrentWebsite(undefined)
}, [])
return {
currentFile,
currentNotionPage,
currentWebsite,
showFilePreview,
hideFilePreview,
showNotionPagePreview,
hideNotionPagePreview,
showWebsitePreview,
hideWebsitePreview,
}
}
export default usePreviewState

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,25 @@
'use client'
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
import type { DataSourceProvider, NotionPage } from '@/models/common'
import type { CrawlOptions, CrawlResultItem, FileItem } from '@/models/datasets'
import { RiArrowRightLine, RiFolder6Line } from '@remixicon/react'
import { RiFolder6Line } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import NotionConnector from '@/app/components/base/notion-connector'
import { NotionPageSelector } from '@/app/components/base/notion-page-selector'
import PlanUpgradeModal from '@/app/components/billing/plan-upgrade-modal'
import { Plan } from '@/app/components/billing/type'
import VectorSpaceFull from '@/app/components/billing/vector-space-full'
import { ENABLE_WEBSITE_FIRECRAWL, ENABLE_WEBSITE_JINAREADER, ENABLE_WEBSITE_WATERCRAWL } from '@/config'
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
import { useProviderContext } from '@/context/provider-context'
import { DataSourceType } from '@/models/datasets'
import { cn } from '@/utils/classnames'
import EmptyDatasetCreationModal from '../empty-dataset-creation-modal'
import FilePreview from '../file-preview'
import FileUploader from '../file-uploader'
import NotionPagePreview from '../notion-page-preview'
import Website from '../website'
import WebsitePreview from '../website/preview'
import { DataSourceTypeSelector, NextStepButton, PreviewPanel } from './components'
import { usePreviewState } from './hooks'
import s from './index.module.css'
import UpgradeCard from './upgrade-card'
@ -50,6 +46,24 @@ type IStepOneProps = {
authedDataSourceList: DataSourceAuth[]
}
// Helper function to check if notion is authenticated
function checkNotionAuth(authedDataSourceList: DataSourceAuth[]): boolean {
const notionSource = authedDataSourceList.find(item => item.provider === 'notion_datasource')
return Boolean(notionSource && notionSource.credentials_list.length > 0)
}
// Helper function to get notion credential list
function getNotionCredentialList(authedDataSourceList: DataSourceAuth[]) {
return authedDataSourceList.find(item => item.provider === 'notion_datasource')?.credentials_list || []
}
// Lookup table for checking multiple items by data source type
const MULTIPLE_ITEMS_CHECK: Record<DataSourceType, (props: { files: FileItem[], notionPages: NotionPage[], websitePages: CrawlResultItem[] }) => boolean> = {
[DataSourceType.FILE]: ({ files }) => files.length > 1,
[DataSourceType.NOTION]: ({ notionPages }) => notionPages.length > 1,
[DataSourceType.WEB]: ({ websitePages }) => websitePages.length > 1,
}
const StepOne = ({
datasetId,
dataSourceType: inCreatePageDataSourceType,
@ -72,76 +86,47 @@ const StepOne = ({
onCrawlOptionsChange,
authedDataSourceList,
}: IStepOneProps) => {
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
const [showModal, setShowModal] = useState(false)
const [currentFile, setCurrentFile] = useState<File | undefined>()
const [currentNotionPage, setCurrentNotionPage] = useState<NotionPage | undefined>()
const [currentWebsite, setCurrentWebsite] = useState<CrawlResultItem | undefined>()
const { t } = useTranslation()
const dataset = useDatasetDetailContextWithSelector(state => state.dataset)
const { plan, enableBilling } = useProviderContext()
const modalShowHandle = () => setShowModal(true)
const modalCloseHandle = () => setShowModal(false)
// Preview state management
const {
currentFile,
currentNotionPage,
currentWebsite,
showFilePreview,
hideFilePreview,
showNotionPagePreview,
hideNotionPagePreview,
showWebsitePreview,
hideWebsitePreview,
} = usePreviewState()
const updateCurrentFile = useCallback((file: File) => {
setCurrentFile(file)
}, [])
// Empty dataset modal state
const [showModal, { setTrue: openModal, setFalse: closeModal }] = useBoolean(false)
const hideFilePreview = useCallback(() => {
setCurrentFile(undefined)
}, [])
const updateCurrentPage = useCallback((page: NotionPage) => {
setCurrentNotionPage(page)
}, [])
const hideNotionPagePreview = useCallback(() => {
setCurrentNotionPage(undefined)
}, [])
const updateWebsite = useCallback((website: CrawlResultItem) => {
setCurrentWebsite(website)
}, [])
const hideWebsitePreview = useCallback(() => {
setCurrentWebsite(undefined)
}, [])
// Plan upgrade modal state
const [isShowPlanUpgradeModal, { setTrue: showPlanUpgradeModal, setFalse: hidePlanUpgradeModal }] = useBoolean(false)
// Computed values
const shouldShowDataSourceTypeList = !datasetId || (datasetId && !dataset?.data_source_type)
const isInCreatePage = shouldShowDataSourceTypeList
const dataSourceType = isInCreatePage ? inCreatePageDataSourceType : dataset?.data_source_type
const { plan, enableBilling } = useProviderContext()
const allFileLoaded = (files.length > 0 && files.every(file => file.file.id))
const hasNotin = notionPages.length > 0
// Default to FILE type when no type is provided from either source
const dataSourceType = isInCreatePage
? (inCreatePageDataSourceType ?? DataSourceType.FILE)
: (dataset?.data_source_type ?? DataSourceType.FILE)
const allFileLoaded = files.length > 0 && files.every(file => file.file.id)
const hasNotion = notionPages.length > 0
const isVectorSpaceFull = plan.usage.vectorSpace >= plan.total.vectorSpace
const isShowVectorSpaceFull = (allFileLoaded || hasNotin) && isVectorSpaceFull && enableBilling
const isShowVectorSpaceFull = (allFileLoaded || hasNotion) && isVectorSpaceFull && enableBilling
const supportBatchUpload = !enableBilling || plan.type !== Plan.sandbox
const notSupportBatchUpload = !supportBatchUpload
const [isShowPlanUpgradeModal, {
setTrue: showPlanUpgradeModal,
setFalse: hidePlanUpgradeModal,
}] = useBoolean(false)
const onStepChange = useCallback(() => {
if (notSupportBatchUpload) {
let isMultiple = false
if (dataSourceType === DataSourceType.FILE && files.length > 1)
isMultiple = true
const isNotionAuthed = useMemo(() => checkNotionAuth(authedDataSourceList), [authedDataSourceList])
const notionCredentialList = useMemo(() => getNotionCredentialList(authedDataSourceList), [authedDataSourceList])
if (dataSourceType === DataSourceType.NOTION && notionPages.length > 1)
isMultiple = true
if (dataSourceType === DataSourceType.WEB && websitePages.length > 1)
isMultiple = true
if (isMultiple) {
showPlanUpgradeModal()
return
}
}
doOnStepChange()
}, [dataSourceType, doOnStepChange, files.length, notSupportBatchUpload, notionPages.length, showPlanUpgradeModal, websitePages.length])
const nextDisabled = useMemo(() => {
const fileNextDisabled = useMemo(() => {
if (!files.length)
return true
if (files.some(file => !file.file.id))
@ -149,109 +134,50 @@ const StepOne = ({
return isShowVectorSpaceFull
}, [files, isShowVectorSpaceFull])
const isNotionAuthed = useMemo(() => {
if (!authedDataSourceList)
return false
const notionSource = authedDataSourceList.find(item => item.provider === 'notion_datasource')
if (!notionSource)
return false
return notionSource.credentials_list.length > 0
}, [authedDataSourceList])
// Clear previews when switching data source type
const handleClearPreviews = useCallback((newType: DataSourceType) => {
if (newType !== DataSourceType.FILE)
hideFilePreview()
if (newType !== DataSourceType.NOTION)
hideNotionPagePreview()
if (newType !== DataSourceType.WEB)
hideWebsitePreview()
}, [hideFilePreview, hideNotionPagePreview, hideWebsitePreview])
const notionCredentialList = useMemo(() => {
return authedDataSourceList.find(item => item.provider === 'notion_datasource')?.credentials_list || []
}, [authedDataSourceList])
// Handle step change with batch upload check
const onStepChange = useCallback(() => {
if (!supportBatchUpload && dataSourceType) {
const checkFn = MULTIPLE_ITEMS_CHECK[dataSourceType]
if (checkFn?.({ files, notionPages, websitePages })) {
showPlanUpgradeModal()
return
}
}
doOnStepChange()
}, [dataSourceType, doOnStepChange, files, supportBatchUpload, notionPages, showPlanUpgradeModal, websitePages])
return (
<div className="h-full w-full overflow-x-auto">
<div className="flex h-full w-full min-w-[1440px]">
{/* Left Panel - Form */}
<div className="relative h-full w-1/2 overflow-y-auto">
<div className="flex justify-end">
<div className={cn(s.form)}>
{
shouldShowDataSourceTypeList && (
{shouldShowDataSourceTypeList && (
<>
<div className={cn(s.stepHeader, 'system-md-semibold text-text-secondary')}>
{t('steps.one', { ns: 'datasetCreation' })}
</div>
)
}
{
shouldShowDataSourceTypeList && (
<div className="mb-8 grid grid-cols-3 gap-4">
<div
className={cn(
s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.FILE && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.FILE && s.disabled,
)}
onClick={() => {
if (dataSourceTypeDisable)
return
changeType(DataSourceType.FILE)
hideNotionPagePreview()
hideWebsitePreview()
}}
>
<span className={cn(s.datasetIcon)} />
<span
title={t('stepOne.dataSourceType.file', { ns: 'datasetCreation' })!}
className="truncate"
>
{t('stepOne.dataSourceType.file', { ns: 'datasetCreation' })}
</span>
</div>
<div
className={cn(
s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.NOTION && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.NOTION && s.disabled,
)}
onClick={() => {
if (dataSourceTypeDisable)
return
changeType(DataSourceType.NOTION)
hideFilePreview()
hideWebsitePreview()
}}
>
<span className={cn(s.datasetIcon, s.notion)} />
<span
title={t('stepOne.dataSourceType.notion', { ns: 'datasetCreation' })!}
className="truncate"
>
{t('stepOne.dataSourceType.notion', { ns: 'datasetCreation' })}
</span>
</div>
{(ENABLE_WEBSITE_FIRECRAWL || ENABLE_WEBSITE_JINAREADER || ENABLE_WEBSITE_WATERCRAWL) && (
<div
className={cn(
s.dataSourceItem,
'system-sm-medium',
dataSourceType === DataSourceType.WEB && s.active,
dataSourceTypeDisable && dataSourceType !== DataSourceType.WEB && s.disabled,
)}
onClick={() => {
if (dataSourceTypeDisable)
return
changeType(DataSourceType.WEB)
hideFilePreview()
hideNotionPagePreview()
}}
>
<span className={cn(s.datasetIcon, s.web)} />
<span
title={t('stepOne.dataSourceType.web', { ns: 'datasetCreation' })!}
className="truncate"
>
{t('stepOne.dataSourceType.web', { ns: 'datasetCreation' })}
</span>
</div>
)}
</div>
)
}
<DataSourceTypeSelector
currentType={dataSourceType}
disabled={dataSourceTypeDisable}
onChange={changeType}
onClearPreviews={handleClearPreviews}
/>
</>
)}
{/* File Data Source */}
{dataSourceType === DataSourceType.FILE && (
<>
<FileUploader
@ -260,7 +186,7 @@ const StepOne = ({
prepareFileList={updateFileList}
onFileListUpdate={updateFileList}
onFileUpdate={updateFile}
onPreview={updateCurrentFile}
onPreview={showFilePreview}
supportBatchUpload={supportBatchUpload}
/>
{isShowVectorSpaceFull && (
@ -268,24 +194,17 @@ const StepOne = ({
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
<Button disabled={nextDisabled} variant="primary" onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('stepOne.button', { ns: 'datasetCreation' })}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
{
enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
<div className="mt-5">
<div className="mb-4 h-px bg-divider-subtle"></div>
<UpgradeCard />
</div>
)
}
<NextStepButton disabled={fileNextDisabled} onClick={onStepChange} />
{enableBilling && plan.type === Plan.sandbox && files.length > 0 && (
<div className="mt-5">
<div className="mb-4 h-px bg-divider-subtle" />
<UpgradeCard />
</div>
)}
</>
)}
{/* Notion Data Source */}
{dataSourceType === DataSourceType.NOTION && (
<>
{!isNotionAuthed && <NotionConnector onSetting={onSetting} />}
@ -295,7 +214,7 @@ const StepOne = ({
<NotionPageSelector
value={notionPages.map(page => page.page_id)}
onSelect={updateNotionPages}
onPreview={updateCurrentPage}
onPreview={showNotionPagePreview}
credentialList={notionCredentialList}
onSelectCredential={updateNotionCredentialId}
datasetId={datasetId}
@ -306,23 +225,21 @@ const StepOne = ({
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
<Button disabled={isShowVectorSpaceFull || !notionPages.length} variant="primary" onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('stepOne.button', { ns: 'datasetCreation' })}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
<NextStepButton
disabled={isShowVectorSpaceFull || !notionPages.length}
onClick={onStepChange}
/>
</>
)}
</>
)}
{/* Web Data Source */}
{dataSourceType === DataSourceType.WEB && (
<>
<div className={cn('mb-8 w-[640px]', !shouldShowDataSourceTypeList && 'mt-12')}>
<Website
onPreview={updateWebsite}
onPreview={showWebsitePreview}
checkedCrawlResult={websitePages}
onCheckedCrawlResultChange={updateWebsitePages}
onCrawlProviderChange={onWebsiteCrawlProviderChange}
@ -337,48 +254,43 @@ const StepOne = ({
<VectorSpaceFull />
</div>
)}
<div className="flex max-w-[640px] justify-end gap-2">
<Button disabled={isShowVectorSpaceFull || !websitePages.length} variant="primary" onClick={onStepChange}>
<span className="flex gap-0.5 px-[10px]">
<span className="px-0.5">{t('stepOne.button', { ns: 'datasetCreation' })}</span>
<RiArrowRightLine className="size-4" />
</span>
</Button>
</div>
<NextStepButton
disabled={isShowVectorSpaceFull || !websitePages.length}
onClick={onStepChange}
/>
</>
)}
{/* Empty Dataset Creation Link */}
{!datasetId && (
<>
<div className="my-8 h-px max-w-[640px] bg-divider-regular" />
<span className="inline-flex cursor-pointer items-center text-[13px] leading-4 text-text-accent" onClick={modalShowHandle}>
<span
className="inline-flex cursor-pointer items-center text-[13px] leading-4 text-text-accent"
onClick={openModal}
>
<RiFolder6Line className="mr-1 size-4" />
{t('stepOne.emptyDatasetCreation', { ns: 'datasetCreation' })}
</span>
</>
)}
</div>
<EmptyDatasetCreationModal show={showModal} onHide={modalCloseHandle} />
<EmptyDatasetCreationModal show={showModal} onHide={closeModal} />
</div>
</div>
<div className="h-full w-1/2 overflow-y-auto">
{currentFile && <FilePreview file={currentFile} hidePreview={hideFilePreview} />}
{currentNotionPage && (
<NotionPagePreview
currentPage={currentNotionPage}
hidePreview={hideNotionPagePreview}
notionCredentialId={notionCredentialId}
/>
)}
{currentWebsite && <WebsitePreview payload={currentWebsite} hidePreview={hideWebsitePreview} />}
{isShowPlanUpgradeModal && (
<PlanUpgradeModal
show
onClose={hidePlanUpgradeModal}
title={t('upgrade.uploadMultiplePages.title', { ns: 'billing' })!}
description={t('upgrade.uploadMultiplePages.description', { ns: 'billing' })!}
/>
)}
</div>
{/* Right Panel - Preview */}
<PreviewPanel
currentFile={currentFile}
currentNotionPage={currentNotionPage}
currentWebsite={currentWebsite}
notionCredentialId={notionCredentialId}
isShowPlanUpgradeModal={isShowPlanUpgradeModal}
hideFilePreview={hideFilePreview}
hideNotionPagePreview={hideNotionPagePreview}
hideWebsitePreview={hideWebsitePreview}
hidePlanUpgradeModal={hidePlanUpgradeModal}
/>
</div>
</div>
)

View File

@ -0,0 +1,36 @@
import type { DataSet } from '@/models/datasets'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import CornerLabel from '@/app/components/base/corner-label'
type CornerLabelsProps = {
dataset: DataSet
}
const CornerLabels = ({ dataset }: CornerLabelsProps) => {
const { t } = useTranslation()
if (!dataset.embedding_available) {
return (
<CornerLabel
label={t('cornerLabel.unavailable', { ns: 'dataset' })}
className="absolute right-0 top-0 z-10"
labelClassName="rounded-tr-xl"
/>
)
}
if (dataset.runtime_mode === 'rag_pipeline') {
return (
<CornerLabel
label={t('cornerLabel.pipeline', { ns: 'dataset' })}
className="absolute right-0 top-0 z-10"
labelClassName="rounded-tr-xl"
/>
)
}
return null
}
export default React.memo(CornerLabels)

View File

@ -0,0 +1,62 @@
import type { DataSet } from '@/models/datasets'
import { RiFileTextFill, RiRobot2Fill } from '@remixicon/react'
import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { cn } from '@/utils/classnames'
const EXTERNAL_PROVIDER = 'external'
type DatasetCardFooterProps = {
dataset: DataSet
}
const DatasetCardFooter = ({ dataset }: DatasetCardFooterProps) => {
const { t } = useTranslation()
const { formatTimeFromNow } = useFormatTimeFromNow()
const isExternalProvider = dataset.provider === EXTERNAL_PROVIDER
const documentCount = useMemo(() => {
const availableDocCount = dataset.total_available_documents ?? 0
if (availableDocCount < dataset.document_count)
return `${availableDocCount} / ${dataset.document_count}`
return `${dataset.document_count}`
}, [dataset.document_count, dataset.total_available_documents])
const documentCountTooltip = useMemo(() => {
const availableDocCount = dataset.total_available_documents ?? 0
if (availableDocCount < dataset.document_count)
return t('partialEnabled', { ns: 'dataset', count: dataset.document_count, num: availableDocCount })
return t('docAllEnabled', { ns: 'dataset', count: availableDocCount })
}, [t, dataset.document_count, dataset.total_available_documents])
return (
<div
className={cn(
'flex items-center gap-x-3 px-4 pb-3 pt-2 text-text-tertiary',
!dataset.embedding_available && 'opacity-30',
)}
>
<Tooltip popupContent={documentCountTooltip}>
<div className="flex items-center gap-x-1">
<RiFileTextFill className="size-3 text-text-quaternary" />
<span className="system-xs-medium">{documentCount}</span>
</div>
</Tooltip>
{!isExternalProvider && (
<Tooltip popupContent={`${dataset.app_count} ${t('appCount', { ns: 'dataset' })}`}>
<div className="flex items-center gap-x-1">
<RiRobot2Fill className="size-3 text-text-quaternary" />
<span className="system-xs-medium">{dataset.app_count}</span>
</div>
</Tooltip>
)}
<span className="system-xs-regular text-divider-deep">/</span>
<span className="system-xs-regular">{`${t('updated', { ns: 'dataset' })} ${formatTimeFromNow(dataset.updated_at * 1000)}`}</span>
</div>
)
}
export default React.memo(DatasetCardFooter)

View File

@ -0,0 +1,148 @@
import type { DataSet } from '@/models/datasets'
import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useKnowledge } from '@/hooks/use-knowledge'
import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets'
import { cn } from '@/utils/classnames'
const EXTERNAL_PROVIDER = 'external'
type DatasetCardHeaderProps = {
dataset: DataSet
}
// DocModeInfo component - placed before usage
type DocModeInfoProps = {
dataset: DataSet
isExternalProvider: boolean
isShowDocModeInfo: boolean
}
const DocModeInfo = ({
dataset,
isExternalProvider,
isShowDocModeInfo,
}: DocModeInfoProps) => {
const { t } = useTranslation()
const { formatIndexingTechniqueAndMethod } = useKnowledge()
if (isExternalProvider) {
return (
<div className="system-2xs-medium-uppercase flex items-center gap-x-3 text-text-tertiary">
<span>{t('externalKnowledgeBase', { ns: 'dataset' })}</span>
</div>
)
}
if (!isShowDocModeInfo)
return null
const indexingText = dataset.indexing_technique
? formatIndexingTechniqueAndMethod(
dataset.indexing_technique as 'economy' | 'high_quality',
dataset.retrieval_model_dict?.search_method as Parameters<typeof formatIndexingTechniqueAndMethod>[1],
)
: ''
return (
<div className="system-2xs-medium-uppercase flex items-center gap-x-3 text-text-tertiary">
{dataset.doc_form && (
<span
className="min-w-0 max-w-full truncate"
title={t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
>
{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
</span>
)}
{dataset.indexing_technique && indexingText && (
<span
className="min-w-0 max-w-full truncate"
title={indexingText}
>
{indexingText}
</span>
)}
{dataset.is_multimodal && (
<span
className="min-w-0 max-w-full truncate"
title={t('multimodal', { ns: 'dataset' })}
>
{t('multimodal', { ns: 'dataset' })}
</span>
)}
</div>
)
}
// Main DatasetCardHeader component
const DatasetCardHeader = ({ dataset }: DatasetCardHeaderProps) => {
const { t } = useTranslation()
const { formatTimeFromNow } = useFormatTimeFromNow()
const isExternalProvider = dataset.provider === EXTERNAL_PROVIDER
const isShowChunkingModeIcon = dataset.doc_form && (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published)
const isShowDocModeInfo = Boolean(
dataset.doc_form
&& dataset.indexing_technique
&& dataset.retrieval_model_dict?.search_method
&& (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published),
)
const chunkingModeIcon = dataset.doc_form ? DOC_FORM_ICON_WITH_BG[dataset.doc_form] : React.Fragment
const Icon = isExternalProvider ? DOC_FORM_ICON_WITH_BG.external : chunkingModeIcon
const iconInfo = useMemo(() => dataset.icon_info || {
icon: '📙',
icon_type: 'emoji' as const,
icon_background: '#FFF4ED',
icon_url: '',
}, [dataset.icon_info])
const editTimeText = useMemo(
() => `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${formatTimeFromNow(dataset.updated_at * 1000)}`,
[t, dataset.updated_at, formatTimeFromNow],
)
return (
<div className={cn('flex items-center gap-x-3 px-4 pb-2 pt-4', !dataset.embedding_available && 'opacity-30')}>
<div className="relative shrink-0">
<AppIcon
size="large"
iconType={iconInfo.icon_type}
icon={iconInfo.icon}
background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background}
imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined}
/>
{(isShowChunkingModeIcon || isExternalProvider) && (
<div className="absolute -bottom-1 -right-1 z-[5]">
<Icon className="size-4" />
</div>
)}
</div>
<div className="flex grow flex-col gap-y-1 overflow-hidden py-px">
<div
className="system-md-semibold truncate text-text-secondary"
title={dataset.name}
>
{dataset.name}
</div>
<div className="flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary">
<div className="truncate" title={dataset.author_name}>{dataset.author_name}</div>
<div>·</div>
<div className="truncate" title={editTimeText}>{editTimeText}</div>
</div>
<DocModeInfo
dataset={dataset}
isExternalProvider={isExternalProvider}
isShowDocModeInfo={isShowDocModeInfo}
/>
</div>
</div>
)
}
export default React.memo(DatasetCardHeader)

View File

@ -0,0 +1,55 @@
import type { DataSet } from '@/models/datasets'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Confirm from '@/app/components/base/confirm'
import RenameDatasetModal from '../../../rename-modal'
type ModalState = {
showRenameModal: boolean
showConfirmDelete: boolean
confirmMessage: string
}
type DatasetCardModalsProps = {
dataset: DataSet
modalState: ModalState
onCloseRename: () => void
onCloseConfirm: () => void
onConfirmDelete: () => void
onSuccess?: () => void
}
const DatasetCardModals = ({
dataset,
modalState,
onCloseRename,
onCloseConfirm,
onConfirmDelete,
onSuccess,
}: DatasetCardModalsProps) => {
const { t } = useTranslation()
return (
<>
{modalState.showRenameModal && (
<RenameDatasetModal
show={modalState.showRenameModal}
dataset={dataset}
onClose={onCloseRename}
onSuccess={onSuccess}
/>
)}
{modalState.showConfirmDelete && (
<Confirm
title={t('deleteDatasetConfirmTitle', { ns: 'dataset' })}
content={modalState.confirmMessage}
isShow={modalState.showConfirmDelete}
onConfirm={onConfirmDelete}
onCancel={onCloseConfirm}
/>
)}
</>
)
}
export default React.memo(DatasetCardModals)

View File

@ -0,0 +1,18 @@
import type { DataSet } from '@/models/datasets'
import * as React from 'react'
import { cn } from '@/utils/classnames'
type DescriptionProps = {
dataset: DataSet
}
const Description = ({ dataset }: DescriptionProps) => (
<div
className={cn('system-xs-regular line-clamp-2 h-10 px-4 py-1 text-text-tertiary', !dataset.embedding_available && 'opacity-30')}
title={dataset.description}
>
{dataset.description}
</div>
)
export default React.memo(Description)

View File

@ -0,0 +1,52 @@
import type { DataSet } from '@/models/datasets'
import { RiMoreFill } from '@remixicon/react'
import * as React from 'react'
import CustomPopover from '@/app/components/base/popover'
import { cn } from '@/utils/classnames'
import Operations from '../operations'
type OperationsPopoverProps = {
dataset: DataSet
isCurrentWorkspaceDatasetOperator: boolean
openRenameModal: () => void
handleExportPipeline: (include?: boolean) => void
detectIsUsedByApp: () => void
}
const OperationsPopover = ({
dataset,
isCurrentWorkspaceDatasetOperator,
openRenameModal,
handleExportPipeline,
detectIsUsedByApp,
}: OperationsPopoverProps) => (
<div className="absolute right-2 top-2 z-[15] hidden group-hover:block">
<CustomPopover
htmlContent={(
<Operations
showDelete={!isCurrentWorkspaceDatasetOperator}
showExportPipeline={dataset.runtime_mode === 'rag_pipeline'}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
/>
)}
className="z-20 min-w-[186px]"
popupClassName="rounded-xl bg-none shadow-none ring-0 min-w-[186px]"
position="br"
trigger="click"
btnElement={(
<div className="flex size-8 items-center justify-center rounded-[10px] hover:bg-state-base-hover">
<RiMoreFill className="h-5 w-5 text-text-tertiary" />
</div>
)}
btnClassName={open =>
cn(
'size-9 cursor-pointer justify-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 shadow-lg shadow-shadow-shadow-5 ring-[2px] ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border',
open ? 'border-components-actionbar-border bg-state-base-hover' : '',
)}
/>
</div>
)
export default React.memo(OperationsPopover)

View File

@ -0,0 +1,55 @@
import type { Tag } from '@/app/components/base/tag-management/constant'
import type { DataSet } from '@/models/datasets'
import * as React from 'react'
import TagSelector from '@/app/components/base/tag-management/selector'
import { cn } from '@/utils/classnames'
type TagAreaProps = {
dataset: DataSet
tags: Tag[]
setTags: (tags: Tag[]) => void
onSuccess?: () => void
isHoveringTagSelector: boolean
onClick: (e: React.MouseEvent) => void
}
const TagArea = React.forwardRef<HTMLDivElement, TagAreaProps>(({
dataset,
tags,
setTags,
onSuccess,
isHoveringTagSelector,
onClick,
}, ref) => (
<div
className={cn('relative w-full px-3', !dataset.embedding_available && 'opacity-30')}
onClick={onClick}
>
<div
ref={ref}
className={cn(
'invisible w-full group-hover:visible',
tags.length > 0 && 'visible',
)}
>
<TagSelector
position="bl"
type="knowledge"
targetID={dataset.id}
value={tags.map(tag => tag.id)}
selectedTags={tags}
onCacheUpdate={setTags}
onChange={onSuccess}
/>
</div>
<div
className={cn(
'absolute right-0 top-0 z-[5] h-full w-20 bg-tag-selector-mask-bg group-hover:bg-tag-selector-mask-hover-bg',
isHoveringTagSelector && 'hidden',
)}
/>
</div>
))
TagArea.displayName = 'TagArea'
export default TagArea

View File

@ -0,0 +1,138 @@
import type { Tag } from '@/app/components/base/tag-management/constant'
import type { DataSet } from '@/models/datasets'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { useCheckDatasetUsage, useDeleteDataset } from '@/service/use-dataset-card'
import { useExportPipelineDSL } from '@/service/use-pipeline'
type ModalState = {
showRenameModal: boolean
showConfirmDelete: boolean
confirmMessage: string
}
type UseDatasetCardStateOptions = {
dataset: DataSet
onSuccess?: () => void
}
export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateOptions) => {
const { t } = useTranslation()
const [tags, setTags] = useState<Tag[]>(dataset.tags)
useEffect(() => {
setTags(dataset.tags)
}, [dataset.tags])
// Modal state
const [modalState, setModalState] = useState<ModalState>({
showRenameModal: false,
showConfirmDelete: false,
confirmMessage: '',
})
// Export state
const [exporting, setExporting] = useState(false)
// Modal handlers
const openRenameModal = useCallback(() => {
setModalState(prev => ({ ...prev, showRenameModal: true }))
}, [])
const closeRenameModal = useCallback(() => {
setModalState(prev => ({ ...prev, showRenameModal: false }))
}, [])
const closeConfirmDelete = useCallback(() => {
setModalState(prev => ({ ...prev, showConfirmDelete: false }))
}, [])
// API mutations
const { mutateAsync: checkUsage } = useCheckDatasetUsage()
const { mutateAsync: deleteDatasetMutation } = useDeleteDataset()
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
// Export pipeline handler
const handleExportPipeline = useCallback(async (include: boolean = false) => {
const { pipeline_id, name } = dataset
if (!pipeline_id || exporting)
return
try {
setExporting(true)
const { data } = await exportPipelineConfig({
pipelineId: pipeline_id,
include,
})
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
const url = URL.createObjectURL(file)
a.href = url
a.download = `${name}.pipeline`
a.click()
URL.revokeObjectURL(url)
}
catch {
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
}
finally {
setExporting(false)
}
}, [dataset, exportPipelineConfig, exporting, t])
// Delete flow handlers
const detectIsUsedByApp = useCallback(async () => {
try {
const { is_using: isUsedByApp } = await checkUsage(dataset.id)
const message = isUsedByApp
? t('datasetUsedByApp', { ns: 'dataset' })!
: t('deleteDatasetConfirmContent', { ns: 'dataset' })!
setModalState(prev => ({
...prev,
confirmMessage: message,
showConfirmDelete: true,
}))
}
catch (e: unknown) {
if (e instanceof Response) {
const res = await e.json()
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
}
else {
Toast.notify({ type: 'error', message: (e as Error)?.message || 'Unknown error' })
}
}
}, [dataset.id, checkUsage, t])
const onConfirmDelete = useCallback(async () => {
try {
await deleteDatasetMutation(dataset.id)
Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) })
onSuccess?.()
}
finally {
closeConfirmDelete()
}
}, [dataset.id, deleteDatasetMutation, onSuccess, t, closeConfirmDelete])
return {
// Tag state
tags,
setTags,
// Modal state
modalState,
openRenameModal,
closeRenameModal,
closeConfirmDelete,
// Export state
exporting,
// Handlers
handleExportPipeline,
detectIsUsedByApp,
onConfirmDelete,
}
}

View File

@ -1,28 +1,17 @@
'use client'
import type { Tag } from '@/app/components/base/tag-management/constant'
import type { DataSet } from '@/models/datasets'
import { RiFileTextFill, RiMoreFill, RiRobot2Fill } from '@remixicon/react'
import { useHover } from 'ahooks'
import { useRouter } from 'next/navigation'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Confirm from '@/app/components/base/confirm'
import CornerLabel from '@/app/components/base/corner-label'
import CustomPopover from '@/app/components/base/popover'
import TagSelector from '@/app/components/base/tag-management/selector'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip'
import { useMemo, useRef } from 'react'
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
import { useKnowledge } from '@/hooks/use-knowledge'
import { DOC_FORM_ICON_WITH_BG, DOC_FORM_TEXT } from '@/models/datasets'
import { checkIsUsedInApp, deleteDataset } from '@/service/datasets'
import { useExportPipelineDSL } from '@/service/use-pipeline'
import { cn } from '@/utils/classnames'
import RenameDatasetModal from '../../rename-modal'
import Operations from './operations'
import CornerLabels from './components/corner-labels'
import DatasetCardFooter from './components/dataset-card-footer'
import DatasetCardHeader from './components/dataset-card-header'
import DatasetCardModals from './components/dataset-card-modals'
import Description from './components/description'
import OperationsPopover from './components/operations-popover'
import TagArea from './components/tag-area'
import { useDatasetCardState } from './hooks/use-dataset-card-state'
const EXTERNAL_PROVIDER = 'external'
@ -35,320 +24,80 @@ const DatasetCard = ({
dataset,
onSuccess,
}: DatasetCardProps) => {
const { t } = useTranslation()
const { push } = useRouter()
const isCurrentWorkspaceDatasetOperator = useAppContextWithSelector(state => state.isCurrentWorkspaceDatasetOperator)
const [tags, setTags] = useState<Tag[]>(dataset.tags)
const tagSelectorRef = useRef<HTMLDivElement>(null)
const isHoveringTagSelector = useHover(tagSelectorRef)
const [showRenameModal, setShowRenameModal] = useState(false)
const [showConfirmDelete, setShowConfirmDelete] = useState(false)
const [confirmMessage, setConfirmMessage] = useState<string>('')
const [exporting, setExporting] = useState(false)
const {
tags,
setTags,
modalState,
openRenameModal,
closeRenameModal,
closeConfirmDelete,
handleExportPipeline,
detectIsUsedByApp,
onConfirmDelete,
} = useDatasetCardState({ dataset, onSuccess })
const isExternalProvider = useMemo(() => {
return dataset.provider === EXTERNAL_PROVIDER
}, [dataset.provider])
const isExternalProvider = dataset.provider === EXTERNAL_PROVIDER
const isPipelineUnpublished = useMemo(() => {
return dataset.runtime_mode === 'rag_pipeline' && !dataset.is_published
}, [dataset.runtime_mode, dataset.is_published])
const isShowChunkingModeIcon = useMemo(() => {
return dataset.doc_form && (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published)
}, [dataset.doc_form, dataset.runtime_mode, dataset.is_published])
const isShowDocModeInfo = useMemo(() => {
return dataset.doc_form && dataset.indexing_technique && dataset.retrieval_model_dict?.search_method && (dataset.runtime_mode !== 'rag_pipeline' || dataset.is_published)
}, [dataset.doc_form, dataset.indexing_technique, dataset.retrieval_model_dict?.search_method, dataset.runtime_mode, dataset.is_published])
const chunkingModeIcon = dataset.doc_form ? DOC_FORM_ICON_WITH_BG[dataset.doc_form] : React.Fragment
const Icon = isExternalProvider ? DOC_FORM_ICON_WITH_BG.external : chunkingModeIcon
const iconInfo = dataset.icon_info || {
icon: '📙',
icon_type: 'emoji',
icon_background: '#FFF4ED',
icon_url: '',
const handleCardClick = (e: React.MouseEvent) => {
e.preventDefault()
if (isExternalProvider)
push(`/datasets/${dataset.id}/hitTesting`)
else if (isPipelineUnpublished)
push(`/datasets/${dataset.id}/pipeline`)
else
push(`/datasets/${dataset.id}/documents`)
}
const { formatIndexingTechniqueAndMethod } = useKnowledge()
const documentCount = useMemo(() => {
const availableDocCount = dataset.total_available_documents ?? 0
if (availableDocCount === dataset.document_count)
return `${dataset.document_count}`
if (availableDocCount < dataset.document_count)
return `${availableDocCount} / ${dataset.document_count}`
}, [dataset.document_count, dataset.total_available_documents])
const documentCountTooltip = useMemo(() => {
const availableDocCount = dataset.total_available_documents ?? 0
if (availableDocCount === dataset.document_count)
return t('docAllEnabled', { ns: 'dataset', count: availableDocCount })
if (availableDocCount < dataset.document_count)
return t('partialEnabled', { ns: 'dataset', count: dataset.document_count, num: availableDocCount })
}, [t, dataset.document_count, dataset.total_available_documents])
const { formatTimeFromNow } = useFormatTimeFromNow()
const editTimeText = useMemo(() => {
return `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${formatTimeFromNow(dataset.updated_at * 1000)}`
}, [t, dataset.updated_at, formatTimeFromNow])
const openRenameModal = useCallback(() => {
setShowRenameModal(true)
}, [])
const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL()
const handleExportPipeline = useCallback(async (include = false) => {
const { pipeline_id, name } = dataset
if (!pipeline_id)
return
if (exporting)
return
try {
setExporting(true)
const { data } = await exportPipelineConfig({
pipelineId: pipeline_id,
include,
})
const a = document.createElement('a')
const file = new Blob([data], { type: 'application/yaml' })
const url = URL.createObjectURL(file)
a.href = url
a.download = `${name}.pipeline`
a.click()
URL.revokeObjectURL(url)
}
catch {
Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) })
}
finally {
setExporting(false)
}
}, [dataset, exportPipelineConfig, exporting, t])
const detectIsUsedByApp = useCallback(async () => {
try {
const { is_using: isUsedByApp } = await checkIsUsedInApp(dataset.id)
setConfirmMessage(isUsedByApp ? t('datasetUsedByApp', { ns: 'dataset' })! : t('deleteDatasetConfirmContent', { ns: 'dataset' })!)
setShowConfirmDelete(true)
}
catch (e: any) {
const res = await e.json()
Toast.notify({ type: 'error', message: res?.message || 'Unknown error' })
}
}, [dataset.id, t])
const onConfirmDelete = useCallback(async () => {
try {
await deleteDataset(dataset.id)
Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) })
if (onSuccess)
onSuccess()
}
finally {
setShowConfirmDelete(false)
}
}, [dataset.id, onSuccess, t])
useEffect(() => {
setTags(dataset.tags)
}, [dataset])
const handleTagAreaClick = (e: React.MouseEvent) => {
e.stopPropagation()
e.preventDefault()
}
return (
<>
<div
className="group relative col-span-1 flex h-[190px] cursor-pointer flex-col rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-all duration-200 ease-in-out hover:bg-components-card-bg-alt hover:shadow-md hover:shadow-shadow-shadow-5"
data-disable-nprogress={true}
onClick={(e) => {
e.preventDefault()
if (isExternalProvider)
push(`/datasets/${dataset.id}/hitTesting`)
else if (isPipelineUnpublished)
push(`/datasets/${dataset.id}/pipeline`)
else
push(`/datasets/${dataset.id}/documents`)
}}
onClick={handleCardClick}
>
{!dataset.embedding_available && (
<CornerLabel
label={t('cornerLabel.unavailable', { ns: 'dataset' })}
className="absolute right-0 top-0 z-10"
labelClassName="rounded-tr-xl"
/>
)}
{dataset.embedding_available && dataset.runtime_mode === 'rag_pipeline' && (
<CornerLabel
label={t('cornerLabel.pipeline', { ns: 'dataset' })}
className="absolute right-0 top-0 z-10"
labelClassName="rounded-tr-xl"
/>
)}
<div className={cn('flex items-center gap-x-3 px-4 pb-2 pt-4', !dataset.embedding_available && 'opacity-30')}>
<div className="relative shrink-0">
<AppIcon
size="large"
iconType={iconInfo.icon_type}
icon={iconInfo.icon}
background={iconInfo.icon_type === 'image' ? undefined : iconInfo.icon_background}
imageUrl={iconInfo.icon_type === 'image' ? iconInfo.icon_url : undefined}
/>
{(isShowChunkingModeIcon || isExternalProvider) && (
<div className="absolute -bottom-1 -right-1 z-[5]">
<Icon className="size-4" />
</div>
)}
</div>
<div className="flex grow flex-col gap-y-1 overflow-hidden py-px">
<div
className="system-md-semibold truncate text-text-secondary"
title={dataset.name}
>
{dataset.name}
</div>
<div className="flex items-center gap-1 text-[10px] font-medium leading-[18px] text-text-tertiary">
<div className="truncate" title={dataset.author_name}>{dataset.author_name}</div>
<div>·</div>
<div className="truncate" title={editTimeText}>{editTimeText}</div>
</div>
<div className="system-2xs-medium-uppercase flex items-center gap-x-3 text-text-tertiary">
{isExternalProvider && <span>{t('externalKnowledgeBase', { ns: 'dataset' })}</span>}
{!isExternalProvider && isShowDocModeInfo && (
<>
{dataset.doc_form && (
<span
className="min-w-0 max-w-full truncate"
title={t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
>
{t(`chunkingMode.${DOC_FORM_TEXT[dataset.doc_form]}`, { ns: 'dataset' })}
</span>
)}
{dataset.indexing_technique && (
<span
className="min-w-0 max-w-full truncate"
title={formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method) as any}
>
{formatIndexingTechniqueAndMethod(dataset.indexing_technique, dataset.retrieval_model_dict?.search_method) as any}
</span>
)}
{dataset.is_multimodal && (
<span
className="min-w-0 max-w-full truncate"
title={t('multimodal', { ns: 'dataset' })}
>
{t('multimodal', { ns: 'dataset' })}
</span>
)}
</>
)}
</div>
</div>
</div>
<div
className={cn('system-xs-regular line-clamp-2 h-10 px-4 py-1 text-text-tertiary', !dataset.embedding_available && 'opacity-30')}
title={dataset.description}
>
{dataset.description}
</div>
<div
className={cn('relative w-full px-3', !dataset.embedding_available && 'opacity-30')}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
}}
>
<div
ref={tagSelectorRef}
className={cn(
'invisible w-full group-hover:visible',
tags.length > 0 && 'visible',
)}
>
<TagSelector
position="bl"
type="knowledge"
targetID={dataset.id}
value={tags.map(tag => tag.id)}
selectedTags={tags}
onCacheUpdate={setTags}
onChange={onSuccess}
/>
</div>
{/* Tag Mask */}
<div
className={cn(
'absolute right-0 top-0 z-[5] h-full w-20 bg-tag-selector-mask-bg group-hover:bg-tag-selector-mask-hover-bg',
isHoveringTagSelector && 'hidden',
)}
/>
</div>
<div
className={cn(
'flex items-center gap-x-3 px-4 pb-3 pt-2 text-text-tertiary',
!dataset.embedding_available && 'opacity-30',
)}
>
<Tooltip popupContent={documentCountTooltip}>
<div className="flex items-center gap-x-1">
<RiFileTextFill className="size-3 text-text-quaternary" />
<span className="system-xs-medium">{documentCount}</span>
</div>
</Tooltip>
{!isExternalProvider && (
<Tooltip popupContent={`${dataset.app_count} ${t('appCount', { ns: 'dataset' })}`}>
<div className="flex items-center gap-x-1">
<RiRobot2Fill className="size-3 text-text-quaternary" />
<span className="system-xs-medium">{dataset.app_count}</span>
</div>
</Tooltip>
)}
<span className="system-xs-regular text-divider-deep">/</span>
<span className="system-xs-regular">{`${t('updated', { ns: 'dataset' })} ${formatTimeFromNow(dataset.updated_at * 1000)}`}</span>
</div>
<div className="absolute right-2 top-2 z-[15] hidden group-hover:block">
<CustomPopover
htmlContent={(
<Operations
showDelete={!isCurrentWorkspaceDatasetOperator}
showExportPipeline={dataset.runtime_mode === 'rag_pipeline'}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
/>
)}
className="z-20 min-w-[186px]"
popupClassName="rounded-xl bg-none shadow-none ring-0 min-w-[186px]"
position="br"
trigger="click"
btnElement={(
<div className="flex size-8 items-center justify-center rounded-[10px] hover:bg-state-base-hover">
<RiMoreFill className="h-5 w-5 text-text-tertiary" />
</div>
)}
btnClassName={open =>
cn(
'size-9 cursor-pointer justify-center rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0 shadow-lg shadow-shadow-shadow-5 ring-[2px] ring-inset ring-components-actionbar-bg hover:border-components-actionbar-border',
open ? 'border-components-actionbar-border bg-state-base-hover' : '',
)}
/>
</div>
</div>
{showRenameModal && (
<RenameDatasetModal
show={showRenameModal}
<CornerLabels dataset={dataset} />
<DatasetCardHeader dataset={dataset} />
<Description dataset={dataset} />
<TagArea
ref={tagSelectorRef}
dataset={dataset}
onClose={() => setShowRenameModal(false)}
tags={tags}
setTags={setTags}
onSuccess={onSuccess}
isHoveringTagSelector={isHoveringTagSelector}
onClick={handleTagAreaClick}
/>
)}
{showConfirmDelete && (
<Confirm
title={t('deleteDatasetConfirmTitle', { ns: 'dataset' })}
content={confirmMessage}
isShow={showConfirmDelete}
onConfirm={onConfirmDelete}
onCancel={() => setShowConfirmDelete(false)}
<DatasetCardFooter dataset={dataset} />
<OperationsPopover
dataset={dataset}
isCurrentWorkspaceDatasetOperator={isCurrentWorkspaceDatasetOperator}
openRenameModal={openRenameModal}
handleExportPipeline={handleExportPipeline}
detectIsUsedByApp={detectIsUsedByApp}
/>
)}
</div>
<DatasetCardModals
dataset={dataset}
modalState={modalState}
onCloseRename={closeRenameModal}
onCloseConfirm={closeConfirmDelete}
onConfirmDelete={onConfirmDelete}
onSuccess={onSuccess}
/>
</>
)
}

View File

@ -1,7 +1,7 @@
import type { SlashCommandHandler } from './types'
import { RiUser3Line } from '@remixicon/react'
import * as React from 'react'
import i18n from '@/i18n-config/i18next-config'
import { getI18n } from 'react-i18next'
import { registerCommands, unregisterCommands } from './command-bus'
// Account command dependency types - no external dependencies needed
@ -21,6 +21,7 @@ export const accountCommand: SlashCommandHandler<AccountDeps> = {
},
async search(args: string, locale: string = 'en') {
const i18n = getI18n()
return [{
id: 'account',
title: i18n.t('account.account', { ns: 'common', lng: locale }),

View File

@ -1,7 +1,7 @@
import type { SlashCommandHandler } from './types'
import { RiDiscordLine } from '@remixicon/react'
import * as React from 'react'
import i18n from '@/i18n-config/i18next-config'
import { getI18n } from 'react-i18next'
import { registerCommands, unregisterCommands } from './command-bus'
// Community command dependency types
@ -22,6 +22,7 @@ export const communityCommand: SlashCommandHandler<CommunityDeps> = {
},
async search(args: string, locale: string = 'en') {
const i18n = getI18n()
return [{
id: 'community',
title: i18n.t('userProfile.community', { ns: 'common', lng: locale }),

View File

@ -1,8 +1,8 @@
import type { SlashCommandHandler } from './types'
import { RiBookOpenLine } from '@remixicon/react'
import * as React from 'react'
import { getI18n } from 'react-i18next'
import { defaultDocBaseUrl } from '@/context/i18n'
import i18n from '@/i18n-config/i18next-config'
import { getDocLanguage } from '@/i18n-config/language'
import { registerCommands, unregisterCommands } from './command-bus'
@ -19,6 +19,7 @@ export const docsCommand: SlashCommandHandler<DocDeps> = {
// Direct execution function
execute: () => {
const i18n = getI18n()
const currentLocale = i18n.language
const docLanguage = getDocLanguage(currentLocale)
const url = `${defaultDocBaseUrl}/${docLanguage}`
@ -26,6 +27,7 @@ export const docsCommand: SlashCommandHandler<DocDeps> = {
},
async search(args: string, locale: string = 'en') {
const i18n = getI18n()
return [{
id: 'doc',
title: i18n.t('userProfile.helpCenter', { ns: 'common', lng: locale }),
@ -41,6 +43,7 @@ export const docsCommand: SlashCommandHandler<DocDeps> = {
},
register(_deps: DocDeps) {
const i18n = getI18n()
registerCommands({
'navigation.doc': async (_args) => {
// Get the current language from i18n

View File

@ -1,7 +1,7 @@
import type { SlashCommandHandler } from './types'
import { RiFeedbackLine } from '@remixicon/react'
import * as React from 'react'
import i18n from '@/i18n-config/i18next-config'
import { getI18n } from 'react-i18next'
import { registerCommands, unregisterCommands } from './command-bus'
// Forum command dependency types
@ -22,6 +22,7 @@ export const forumCommand: SlashCommandHandler<ForumDeps> = {
},
async search(args: string, locale: string = 'en') {
const i18n = getI18n()
return [{
id: 'forum',
title: i18n.t('userProfile.forum', { ns: 'common', lng: locale }),

View File

@ -1,6 +1,6 @@
import type { CommandSearchResult } from '../types'
import type { SlashCommandHandler } from './types'
import i18n from '@/i18n-config/i18next-config'
import { getI18n } from 'react-i18next'
import { languages } from '@/i18n-config/language'
import { registerCommands, unregisterCommands } from './command-bus'
@ -14,6 +14,7 @@ const buildLanguageCommands = (query: string): CommandSearchResult[] => {
const list = languages.filter(item => item.supported && (
!q || item.name.toLowerCase().includes(q) || String(item.value).toLowerCase().includes(q)
))
const i18n = getI18n()
return list.map(item => ({
id: `lang-${item.value}`,
title: item.name,

View File

@ -2,8 +2,8 @@
import type { ActionItem } from '../types'
import { useTheme } from 'next-themes'
import { useEffect } from 'react'
import { getI18n } from 'react-i18next'
import { setLocaleOnClient } from '@/i18n-config'
import i18n from '@/i18n-config/i18next-config'
import { accountCommand } from './account'
import { executeCommand } from './command-bus'
import { communityCommand } from './community'
@ -14,6 +14,8 @@ import { slashCommandRegistry } from './registry'
import { themeCommand } from './theme'
import { zenCommand } from './zen'
const i18n = getI18n()
export const slashAction: ActionItem = {
key: '/',
shortcut: '/',

View File

@ -2,7 +2,7 @@ import type { CommandSearchResult } from '../types'
import type { SlashCommandHandler } from './types'
import { RiComputerLine, RiMoonLine, RiSunLine } from '@remixicon/react'
import * as React from 'react'
import i18n from '@/i18n-config/i18next-config'
import { getI18n } from 'react-i18next'
import { registerCommands, unregisterCommands } from './command-bus'
// Theme dependency types
@ -32,6 +32,7 @@ const THEME_ITEMS = [
] as const
const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => {
const i18n = getI18n()
const q = query.toLowerCase()
const list = THEME_ITEMS.filter(item =>
!q

View File

@ -1,8 +1,8 @@
import type { SlashCommandHandler } from './types'
import { RiFullscreenLine } from '@remixicon/react'
import * as React from 'react'
import { getI18n } from 'react-i18next'
import { isInWorkflowPage } from '@/app/components/workflow/constants'
import i18n from '@/i18n-config/i18next-config'
import { registerCommands, unregisterCommands } from './command-bus'
// Zen command dependency types - no external dependencies needed
@ -32,6 +32,7 @@ export const zenCommand: SlashCommandHandler<ZenDeps> = {
execute: toggleZenMode,
async search(_args: string, locale: string = 'en') {
const i18n = getI18n()
return [{
id: 'zen',
title: i18n.t('gotoAnything.actions.zenTitle', { ns: 'app', lng: locale }) || 'Zen Mode',

View File

@ -15,7 +15,6 @@ import Divider from '@/app/components/base/divider'
import Loading from '@/app/components/base/loading'
import List from '@/app/components/plugins/marketplace/list'
import ProviderCard from '@/app/components/plugins/provider-card'
import { getLocaleOnClient } from '@/i18n-config'
import { cn } from '@/utils/classnames'
import { getMarketplaceUrl } from '@/utils/var'
import {
@ -33,7 +32,6 @@ const InstallFromMarketplace = ({
const { t } = useTranslation()
const { theme } = useTheme()
const [collapse, setCollapse] = useState(false)
const locale = getLocaleOnClient()
const {
plugins: allPlugins,
isLoading: isAllPluginsLoading,
@ -70,7 +68,6 @@ const InstallFromMarketplace = ({
marketplaceCollectionPluginsMap={{}}
plugins={allPlugins}
showInstallButton
locale={locale}
cardContainerClassName="grid grid-cols-2 gap-2"
cardRender={cardRender}
emptyClassName="h-auto"

View File

@ -2,13 +2,13 @@
import type { Item } from '@/app/components/base/select'
import type { Locale } from '@/i18n-config'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import { SimpleSelect } from '@/app/components/base/select'
import { ToastContext } from '@/app/components/base/toast'
import { useAppContext } from '@/context/app-context'
import { useLocale } from '@/context/i18n'
import { setLocaleOnClient } from '@/i18n-config'
import { languages } from '@/i18n-config/language'
@ -25,6 +25,7 @@ export default function LanguagePage() {
const { notify } = useContext(ToastContext)
const [editing, setEditing] = useState(false)
const { t } = useTranslation()
const router = useRouter()
const handleSelectLanguage = async (item: Item) => {
const url = '/account/interface-language'
@ -35,7 +36,8 @@ export default function LanguagePage() {
await updateUserProfile({ url, body: { [bodyKey]: item.value } })
notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) })
setLocaleOnClient(item.value.toString() as Locale)
setLocaleOnClient(item.value.toString() as Locale, false)
router.refresh()
}
catch (e) {
notify({ type: 'error', message: (e as Error).message })

View File

@ -6,10 +6,8 @@ import {
RiBrainLine,
} from '@remixicon/react'
import { useDebounce } from 'ahooks'
import { useEffect, useMemo } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { IS_CLOUD_EDITION } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
import { useProviderContext } from '@/context/provider-context'
import { cn } from '@/utils/classnames'
@ -22,7 +20,6 @@ import {
} from './hooks'
import InstallFromMarketplace from './install-from-marketplace'
import ProviderAddedCard from './provider-added-card'
import QuotaPanel from './provider-added-card/quota-panel'
import SystemModelSelector from './system-model-selector'
type Props = {
@ -34,16 +31,19 @@ const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/an
const ModelProviderPage = ({ searchText }: Props) => {
const debouncedSearchText = useDebounce(searchText, { wait: 500 })
const { t } = useTranslation()
const { mutateCurrentWorkspace, isValidatingCurrentWorkspace } = useAppContext()
const { data: textGenerationDefaultModel } = useDefaultModel(ModelTypeEnum.textGeneration)
const { data: embeddingsDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
const { data: rerankDefaultModel } = useDefaultModel(ModelTypeEnum.rerank)
const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
const { data: textGenerationDefaultModel, isLoading: isTextGenerationDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textGeneration)
const { data: embeddingsDefaultModel, isLoading: isEmbeddingsDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textEmbedding)
const { data: rerankDefaultModel, isLoading: isRerankDefaultModelLoading } = useDefaultModel(ModelTypeEnum.rerank)
const { data: speech2textDefaultModel, isLoading: isSpeech2textDefaultModelLoading } = useDefaultModel(ModelTypeEnum.speech2text)
const { data: ttsDefaultModel, isLoading: isTTSDefaultModelLoading } = useDefaultModel(ModelTypeEnum.tts)
const { modelProviders: providers } = useProviderContext()
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
const defaultModelNotConfigured = !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
const isDefaultModelLoading = isTextGenerationDefaultModelLoading
|| isEmbeddingsDefaultModelLoading
|| isRerankDefaultModelLoading
|| isSpeech2textDefaultModelLoading
|| isTTSDefaultModelLoading
const defaultModelNotConfigured = !isDefaultModelLoading && !textGenerationDefaultModel && !embeddingsDefaultModel && !speech2textDefaultModel && !rerankDefaultModel && !ttsDefaultModel
const [configuredProviders, notConfiguredProviders] = useMemo(() => {
const configuredProviders: ModelProvider[] = []
const notConfiguredProviders: ModelProvider[] = []
@ -88,10 +88,6 @@ const ModelProviderPage = ({ searchText }: Props) => {
return [filteredConfiguredProviders, filteredNotConfiguredProviders]
}, [configuredProviders, debouncedSearchText, notConfiguredProviders])
useEffect(() => {
mutateCurrentWorkspace()
}, [mutateCurrentWorkspace])
return (
<div className="relative -mt-2 pt-1">
<div className={cn('mb-2 flex items-center')}>
@ -115,10 +111,10 @@ const ModelProviderPage = ({ searchText }: Props) => {
rerankDefaultModel={rerankDefaultModel}
speech2textDefaultModel={speech2textDefaultModel}
ttsDefaultModel={ttsDefaultModel}
isLoading={isDefaultModelLoading}
/>
</div>
</div>
{IS_CLOUD_EDITION && <QuotaPanel providers={providers} isLoading={isValidatingCurrentWorkspace} />}
{!filteredConfiguredProviders?.length && (
<div className="mb-2 rounded-[10px] bg-workflow-process-bg p-4">
<div className="flex h-10 w-10 items-center justify-center rounded-[10px] border-[0.5px] border-components-card-border bg-components-card-bg shadow-lg backdrop-blur">

View File

@ -14,7 +14,6 @@ import Divider from '@/app/components/base/divider'
import Loading from '@/app/components/base/loading'
import List from '@/app/components/plugins/marketplace/list'
import ProviderCard from '@/app/components/plugins/provider-card'
import { getLocaleOnClient } from '@/i18n-config'
import { cn } from '@/utils/classnames'
import { getMarketplaceUrl } from '@/utils/var'
import {
@ -32,7 +31,6 @@ const InstallFromMarketplace = ({
const { t } = useTranslation()
const { theme } = useTheme()
const [collapse, setCollapse] = useState(false)
const locale = getLocaleOnClient()
const {
plugins: allPlugins,
isLoading: isAllPluginsLoading,
@ -69,7 +67,6 @@ const InstallFromMarketplace = ({
marketplaceCollectionPluginsMap={{}}
plugins={allPlugins}
showInstallButton
locale={locale}
cardContainerClassName="grid grid-cols-2 gap-2"
cardRender={cardRender}
emptyClassName="h-auto"

View File

@ -7,7 +7,6 @@ import { useToastContext } from '@/app/components/base/toast'
import { ConfigProvider } from '@/app/components/header/account-setting/model-provider-page/model-auth'
import { useCredentialStatus } from '@/app/components/header/account-setting/model-provider-page/model-auth/hooks'
import Indicator from '@/app/components/header/indicator'
import { IS_CLOUD_EDITION } from '@/config'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import { changeModelProviderPriority } from '@/service/common'
import { cn } from '@/utils/classnames'
@ -115,7 +114,7 @@ const CredentialPanel = ({
provider={provider}
/>
{
systemConfig.enabled && isCustomConfigured && IS_CLOUD_EDITION && (
systemConfig.enabled && isCustomConfigured && (
<PrioritySelector
value={priorityUseType}
onSelect={handleChangePriority}
@ -132,7 +131,7 @@ const CredentialPanel = ({
)
}
{
systemConfig.enabled && isCustomConfigured && !provider.provider_credential_schema && IS_CLOUD_EDITION && (
systemConfig.enabled && isCustomConfigured && !provider.provider_credential_schema && (
<div className="ml-1">
<PrioritySelector
value={priorityUseType}

View File

@ -3,7 +3,6 @@ import type {
ModelItem,
ModelProvider,
} from '../declarations'
import type { ModelProviderQuotaGetPaid } from '../utils'
import {
RiArrowRightSLine,
RiInformation2Fill,
@ -29,6 +28,7 @@ import {
} from '../utils'
import CredentialPanel from './credential-panel'
import ModelList from './model-list'
import QuotaPanel from './quota-panel'
export const UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST = 'UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST'
type ProviderAddedCardProps = {
@ -49,7 +49,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
const systemConfig = provider.system_configuration
const hasModelList = fetched && !!modelList.length
const { isCurrentWorkspaceManager } = useAppContext()
const showModelProvider = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider as ModelProviderQuotaGetPaid) && !IS_CE_EDITION
const showQuota = systemConfig.enabled && [...MODEL_PROVIDER_QUOTA_GET_PAID].includes(provider.provider) && !IS_CE_EDITION
const showCredential = configurationMethods.includes(ConfigurationMethodEnum.predefinedModel) && isCurrentWorkspaceManager
const getModelList = async (providerName: string) => {
@ -104,6 +104,13 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
}
</div>
</div>
{
showQuota && (
<QuotaPanel
provider={provider}
/>
)
}
{
showCredential && (
<CredentialPanel
@ -115,7 +122,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
{
collapsed && (
<div className="system-xs-medium group flex items-center justify-between border-t border-t-divider-subtle py-1.5 pl-2 pr-[11px] text-text-tertiary">
{(showModelProvider || !notConfigured) && (
{(showQuota || !notConfigured) && (
<>
<div className="flex h-6 items-center pl-1 pr-1.5 leading-6 group-hover:hidden">
{
@ -143,7 +150,7 @@ const ProviderAddedCard: FC<ProviderAddedCardProps> = ({
</div>
</>
)}
{!showModelProvider && notConfigured && (
{!showQuota && notConfigured && (
<div className="flex h-6 items-center pl-1 pr-1.5">
<RiInformation2Fill className="mr-1 h-4 w-4 text-text-accent" />
<span className="system-xs-medium text-text-secondary">{t('modelProvider.configureTip', { ns: 'common' })}</span>

View File

@ -1,163 +1,66 @@
import type { FC } from 'react'
import type { ModelProvider } from '../declarations'
import type { Plugin } from '@/app/components/plugins/types'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { AnthropicShortLight, Deepseek, Gemini, Grok, OpenaiSmall, Tongyi } from '@/app/components/base/icons/src/public/llm'
import Loading from '@/app/components/base/loading'
import Tooltip from '@/app/components/base/tooltip'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import { useAppContext } from '@/context/app-context'
import useTimestamp from '@/hooks/use-timestamp'
import { cn } from '@/utils/classnames'
import { formatNumber } from '@/utils/format'
import { PreferredProviderTypeEnum } from '../declarations'
import { useMarketplaceAllPlugins } from '../hooks'
import { modelNameMap, ModelProviderQuotaGetPaid } from '../utils'
const allProviders = [
{ key: ModelProviderQuotaGetPaid.OPENAI, Icon: OpenaiSmall },
{ key: ModelProviderQuotaGetPaid.ANTHROPIC, Icon: AnthropicShortLight },
{ key: ModelProviderQuotaGetPaid.GEMINI, Icon: Gemini },
{ key: ModelProviderQuotaGetPaid.X, Icon: Grok },
{ key: ModelProviderQuotaGetPaid.DEEPSEEK, Icon: Deepseek },
{ key: ModelProviderQuotaGetPaid.TONGYI, Icon: Tongyi },
] as const
// Map provider key to plugin ID
// provider key format: langgenius/provider/model, plugin ID format: langgenius/provider
const providerKeyToPluginId: Record<string, string> = {
[ModelProviderQuotaGetPaid.OPENAI]: 'langgenius/openai',
[ModelProviderQuotaGetPaid.ANTHROPIC]: 'langgenius/anthropic',
[ModelProviderQuotaGetPaid.GEMINI]: 'langgenius/gemini',
[ModelProviderQuotaGetPaid.X]: 'langgenius/x',
[ModelProviderQuotaGetPaid.DEEPSEEK]: 'langgenius/deepseek',
[ModelProviderQuotaGetPaid.TONGYI]: 'langgenius/tongyi',
}
import {
CustomConfigurationStatusEnum,
PreferredProviderTypeEnum,
QuotaUnitEnum,
} from '../declarations'
import {
MODEL_PROVIDER_QUOTA_GET_PAID,
} from '../utils'
import PriorityUseTip from './priority-use-tip'
type QuotaPanelProps = {
providers: ModelProvider[]
isLoading?: boolean
provider: ModelProvider
}
const QuotaPanel: FC<QuotaPanelProps> = ({
providers,
isLoading = false,
provider,
}) => {
const { t } = useTranslation()
const { currentWorkspace } = useAppContext()
const credits = Math.max((currentWorkspace.trial_credits - currentWorkspace.trial_credits_used) || 0, 0)
const providerMap = useMemo(() => new Map(
providers.map(p => [p.provider, p.preferred_provider_type]),
), [providers])
const { formatTime } = useTimestamp()
const {
plugins: allPlugins,
} = useMarketplaceAllPlugins(providers, '')
const [selectedPlugin, setSelectedPlugin] = useState<Plugin | null>(null)
const [isShowInstallModal, {
setTrue: showInstallFromMarketplace,
setFalse: hideInstallFromMarketplace,
}] = useBoolean(false)
const selectedPluginIdRef = useRef<string | null>(null)
const handleIconClick = useCallback((key: string) => {
const providerType = providerMap.get(key)
if (!providerType && allPlugins) {
const pluginId = providerKeyToPluginId[key]
const plugin = allPlugins.find(p => p.plugin_id === pluginId)
if (plugin) {
setSelectedPlugin(plugin)
selectedPluginIdRef.current = pluginId
showInstallFromMarketplace()
}
}
}, [allPlugins, providerMap, showInstallFromMarketplace])
useEffect(() => {
if (isShowInstallModal && selectedPluginIdRef.current) {
const isInstalled = providers.some(p => p.provider.startsWith(selectedPluginIdRef.current!))
if (isInstalled) {
hideInstallFromMarketplace()
selectedPluginIdRef.current = null
}
}
}, [providers, isShowInstallModal, hideInstallFromMarketplace])
if (isLoading) {
return (
<div className="my-2 flex min-h-[72px] items-center justify-center rounded-xl border-[0.5px] border-components-panel-border bg-third-party-model-bg-default shadow-xs">
<Loading />
</div>
)
}
const customConfig = provider.custom_configuration
const priorityUseType = provider.preferred_provider_type
const systemConfig = provider.system_configuration
const currentQuota = systemConfig.enabled && systemConfig.quota_configurations.find(item => item.quota_type === systemConfig.current_quota_type)
const openaiOrAnthropic = MODEL_PROVIDER_QUOTA_GET_PAID.includes(provider.provider)
return (
<div className={cn('my-2 min-w-[72px] shrink-0 rounded-xl border-[0.5px] pb-2.5 pl-4 pr-2.5 pt-3 shadow-xs', credits <= 0 ? 'border-state-destructive-border hover:bg-state-destructive-hover' : 'border-components-panel-border bg-third-party-model-bg-default')}>
<div className="group relative min-w-[112px] shrink-0 rounded-lg border-[0.5px] border-components-panel-border bg-white/[0.18] px-3 py-2 shadow-xs">
<div className="system-xs-medium-uppercase mb-2 flex h-4 items-center text-text-tertiary">
{t('modelProvider.quota', { ns: 'common' })}
<Tooltip popupContent={t('modelProvider.card.tip', { ns: 'common' })} />
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-1 text-xs text-text-tertiary">
<span className="system-md-semibold-uppercase mr-0.5 text-text-secondary">{formatNumber(credits)}</span>
<span>{t('modelProvider.credits', { ns: 'common' })}</span>
{currentWorkspace.next_credit_reset_date
? (
<>
<span>·</span>
<span>
{t('modelProvider.resetDate', {
ns: 'common',
date: formatTime(currentWorkspace.next_credit_reset_date, t('dateFormat', { ns: 'appLog' })),
interpolation: { escapeValue: false },
})}
</span>
</>
)
: null}
</div>
<div className="flex items-center gap-1">
{allProviders.map(({ key, Icon }) => {
const providerType = providerMap.get(key)
const usingQuota = providerType === PreferredProviderTypeEnum.system
const getTooltipKey = () => {
if (usingQuota)
return 'modelProvider.card.modelSupported'
if (providerType === PreferredProviderTypeEnum.custom)
return 'modelProvider.card.modelAPI'
return 'modelProvider.card.modelNotSupported'
}
return (
<Tooltip
key={key}
popupContent={t(getTooltipKey(), { modelName: modelNameMap[key], ns: 'common' })}
>
<div
className={cn('relative h-6 w-6', !providerType && 'cursor-pointer hover:opacity-80')}
onClick={() => handleIconClick(key)}
>
<Icon className="h-6 w-6 rounded-lg" />
{!usingQuota && (
<div className="absolute inset-0 rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge opacity-30" />
)}
</div>
</Tooltip>
)
})}
</div>
</div>
{isShowInstallModal && selectedPlugin && (
<InstallFromMarketplace
manifest={selectedPlugin}
uniqueIdentifier={selectedPlugin.latest_package_identifier}
onClose={hideInstallFromMarketplace}
onSuccess={hideInstallFromMarketplace}
<Tooltip popupContent={
openaiOrAnthropic
? t('modelProvider.card.tip', { ns: 'common' })
: t('modelProvider.quotaTip', { ns: 'common' })
}
/>
)}
</div>
{
currentQuota && (
<div className="flex h-4 items-center text-xs text-text-tertiary">
<span className="system-md-semibold-uppercase mr-0.5 text-text-secondary">{formatNumber(Math.max((currentQuota?.quota_limit || 0) - (currentQuota?.quota_used || 0), 0))}</span>
{
currentQuota?.quota_unit === QuotaUnitEnum.tokens && 'Tokens'
}
{
currentQuota?.quota_unit === QuotaUnitEnum.times && t('modelProvider.callTimes', { ns: 'common' })
}
{
currentQuota?.quota_unit === QuotaUnitEnum.credits && t('modelProvider.credits', { ns: 'common' })
}
</div>
)
}
{
priorityUseType === PreferredProviderTypeEnum.system && customConfig.status === CustomConfigurationStatusEnum.active && (
<PriorityUseTip />
)
}
</div>
)
}
export default React.memo(QuotaPanel)
export default QuotaPanel

View File

@ -3,7 +3,7 @@ import type {
DefaultModel,
DefaultModelResponse,
} from '../declarations'
import { RiEqualizer2Line } from '@remixicon/react'
import { RiEqualizer2Line, RiLoader2Line } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
@ -32,6 +32,7 @@ type SystemModelSelectorProps = {
speech2textDefaultModel: DefaultModelResponse | undefined
ttsDefaultModel: DefaultModelResponse | undefined
notConfigured: boolean
isLoading?: boolean
}
const SystemModel: FC<SystemModelSelectorProps> = ({
textGenerationDefaultModel,
@ -40,6 +41,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
speech2textDefaultModel,
ttsDefaultModel,
notConfigured,
isLoading,
}) => {
const { t } = useTranslation()
const { notify } = useToastContext()
@ -129,13 +131,16 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
crossAxis: 8,
}}
>
<PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
<PortalToFollowElemTrigger asChild onClick={() => setOpen(v => !v)}>
<Button
className="relative"
variant={notConfigured ? 'primary' : 'secondary'}
size="small"
disabled={isLoading}
>
<RiEqualizer2Line className="mr-1 h-3.5 w-3.5" />
{isLoading
? <RiLoader2Line className="mr-1 h-3.5 w-3.5 animate-spin" />
: <RiEqualizer2Line className="mr-1 h-3.5 w-3.5" />}
{t('modelProvider.systemModelSettings', { ns: 'common' })}
</Button>
</PortalToFollowElemTrigger>

View File

@ -17,25 +17,7 @@ import {
ModelTypeEnum,
} from './declarations'
export enum ModelProviderQuotaGetPaid {
ANTHROPIC = 'langgenius/anthropic/anthropic',
OPENAI = 'langgenius/openai/openai',
// AZURE_OPENAI = 'langgenius/azure_openai/azure_openai',
GEMINI = 'langgenius/gemini/google',
X = 'langgenius/x/x',
DEEPSEEK = 'langgenius/deepseek/deepseek',
TONGYI = 'langgenius/tongyi/tongyi',
}
export const MODEL_PROVIDER_QUOTA_GET_PAID = [ModelProviderQuotaGetPaid.ANTHROPIC, ModelProviderQuotaGetPaid.OPENAI, ModelProviderQuotaGetPaid.GEMINI, ModelProviderQuotaGetPaid.X, ModelProviderQuotaGetPaid.DEEPSEEK, ModelProviderQuotaGetPaid.TONGYI]
export const modelNameMap = {
[ModelProviderQuotaGetPaid.OPENAI]: 'OpenAI',
[ModelProviderQuotaGetPaid.ANTHROPIC]: 'Anthropic',
[ModelProviderQuotaGetPaid.GEMINI]: 'Gemini',
[ModelProviderQuotaGetPaid.X]: 'xAI',
[ModelProviderQuotaGetPaid.DEEPSEEK]: 'DeepSeek',
[ModelProviderQuotaGetPaid.TONGYI]: 'TONGYI',
}
export const MODEL_PROVIDER_QUOTA_GET_PAID = ['langgenius/anthropic/anthropic', 'langgenius/openai/openai', 'langgenius/azure_openai/azure_openai']
export const isNullOrUndefined = (value: any) => {
return value === undefined || value === null

View File

@ -1,22 +0,0 @@
import * as React from 'react'
import { getLocaleOnServer } from '@/i18n-config/server'
import { ToastProvider } from './base/toast'
import I18N from './i18n'
export type II18NServerProps = {
children: React.ReactNode
}
const I18NServer = async ({
children,
}: II18NServerProps) => {
const locale = await getLocaleOnServer()
return (
<I18N {...{ locale }}>
<ToastProvider>{children}</ToastProvider>
</I18N>
)
}
export default I18NServer

View File

@ -1,45 +0,0 @@
'use client'
import type { FC } from 'react'
import type { Locale } from '@/i18n-config'
import { usePrefetchQuery } from '@tanstack/react-query'
import { useHydrateAtoms } from 'jotai/utils'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { localeAtom } from '@/context/i18n'
import { setLocaleOnClient } from '@/i18n-config'
import { getSystemFeatures } from '@/service/common'
import Loading from './base/loading'
export type II18nProps = {
locale: Locale
children: React.ReactNode
}
const I18n: FC<II18nProps> = ({
locale,
children,
}) => {
useHydrateAtoms([[localeAtom, locale]])
const [loading, setLoading] = useState(true)
usePrefetchQuery({
queryKey: ['systemFeatures'],
queryFn: getSystemFeatures,
})
useEffect(() => {
setLocaleOnClient(locale, false).then(() => {
setLoading(false)
})
}, [locale])
if (loading)
return <div className="flex h-screen w-screen items-center justify-center"><Loading type="app" /></div>
return (
<>
{children}
</>
)
}
export default React.memo(I18n)

View File

@ -1,4 +1,5 @@
import type { FC } from 'react'
import { useTranslation } from '#i18n'
import { RiAlertFill } from '@remixicon/react'
import { camelCase } from 'es-toolkit/string'
import Link from 'next/link'
@ -6,14 +7,12 @@ import * as React from 'react'
import { useMemo } from 'react'
import { Trans } from 'react-i18next'
import { cn } from '@/utils/classnames'
import { useMixedTranslation } from '../marketplace/hooks'
type DeprecationNoticeProps = {
status: 'deleted' | 'active'
deprecatedReason: string
alternativePluginId: string
alternativePluginURL: string
locale?: string
className?: string
innerWrapperClassName?: string
iconWrapperClassName?: string
@ -34,13 +33,12 @@ const DeprecationNotice: FC<DeprecationNoticeProps> = ({
deprecatedReason,
alternativePluginId,
alternativePluginURL,
locale,
className,
innerWrapperClassName,
iconWrapperClassName,
textClassName,
}) => {
const { t } = useMixedTranslation(locale)
const { t } = useTranslation()
const deprecatedReasonKey = useMemo(() => {
if (!deprecatedReason)

View File

@ -502,31 +502,6 @@ describe('Card', () => {
})
})
// ================================
// Locale Tests
// ================================
describe('Locale', () => {
it('should use locale from props when provided', () => {
const plugin = createMockPlugin({
label: { 'en-US': 'English Title', 'zh-Hans': '中文标题' },
})
render(<Card payload={plugin} locale="zh-Hans" />)
expect(screen.getByText('中文标题')).toBeInTheDocument()
})
it('should fallback to default locale when prop locale not found', () => {
const plugin = createMockPlugin({
label: { 'en-US': 'English Title' },
})
render(<Card payload={plugin} locale="fr-FR" />)
expect(screen.getByText('English Title')).toBeInTheDocument()
})
})
// ================================
// Memoization Tests
// ================================

View File

@ -1,15 +1,13 @@
'use client'
import type { Plugin } from '../types'
import type { Locale } from '@/i18n-config'
import { useTranslation } from '#i18n'
import { RiAlertFill } from '@remixicon/react'
import * as React from 'react'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import { useGetLanguage } from '@/context/i18n'
import useTheme from '@/hooks/use-theme'
import {
renderI18nObject,
} from '@/i18n-config'
import { getLanguage } from '@/i18n-config/language'
import { Theme } from '@/types/app'
import { cn } from '@/utils/classnames'
import Partner from '../base/badges/partner'
@ -33,7 +31,6 @@ export type Props = {
footer?: React.ReactNode
isLoading?: boolean
loadingFileName?: string
locale?: Locale
limitedInstall?: boolean
}
@ -48,13 +45,11 @@ const Card = ({
footer,
isLoading = false,
loadingFileName,
locale: localeFromProps,
limitedInstall = false,
}: Props) => {
const defaultLocale = useGetLanguage()
const locale = localeFromProps ? getLanguage(localeFromProps) : defaultLocale
const { t } = useMixedTranslation(localeFromProps)
const { categoriesMap } = useCategories(t, true)
const locale = useGetLanguage()
const { t } = useTranslation()
const { categoriesMap } = useCategories(true)
const { category, type, name, org, label, brief, icon, icon_dark, verified, badges = [] } = payload
const { theme } = useTheme()
const iconSrc = theme === Theme.dark && icon_dark ? icon_dark : icon

View File

@ -1,4 +1,3 @@
import type { TFunction } from 'i18next'
import type { CategoryKey, TagKey } from './constants'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -13,9 +12,8 @@ export type Tag = {
label: string
}
export const useTags = (translateFromOut?: TFunction) => {
const { t: translation } = useTranslation()
const t = translateFromOut || translation
export const useTags = () => {
const { t } = useTranslation()
const tags = useMemo(() => {
return tagKeys.map((tag) => {
@ -53,9 +51,8 @@ type Category = {
label: string
}
export const useCategories = (translateFromOut?: TFunction, isSingle?: boolean) => {
const { t: translation } = useTranslation()
const t = translateFromOut || translation
export const useCategories = (isSingle?: boolean) => {
const { t } = useTranslation()
const categories = useMemo(() => {
return categoryKeys.map((category) => {

View File

@ -1,7 +1,5 @@
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
// Import component after mocks are set up
import Description from './index'
// ================================
@ -30,20 +28,18 @@ const commonTranslations: Record<string, string> = {
'operation.in': 'in',
}
// Mock getLocaleOnServer and translate
vi.mock('@/i18n-config/server', () => ({
getLocaleOnServer: vi.fn(() => Promise.resolve(mockDefaultLocale)),
getTranslation: vi.fn((locale: string, ns: string) => {
return Promise.resolve({
t: (key: string) => {
if (ns === 'plugin')
return pluginTranslations[key] || key
if (ns === 'common')
return commonTranslations[key] || key
return key
},
})
}),
// Mock i18n hooks
vi.mock('#i18n', () => ({
useLocale: vi.fn(() => mockDefaultLocale),
useTranslation: vi.fn((ns: string) => ({
t: (key: string) => {
if (ns === 'plugin')
return pluginTranslations[key] || key
if (ns === 'common')
return commonTranslations[key] || key
return key
},
})),
}))
// ================================
@ -59,29 +55,29 @@ describe('Description', () => {
// Rendering Tests
// ================================
describe('Rendering', () => {
it('should render without crashing', async () => {
const { container } = render(await Description({}))
it('should render without crashing', () => {
const { container } = render(<Description />)
expect(container.firstChild).toBeInTheDocument()
})
it('should render h1 heading with empower text', async () => {
render(await Description({}))
it('should render h1 heading with empower text', () => {
render(<Description />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toBeInTheDocument()
expect(heading).toHaveTextContent('Empower your AI development')
})
it('should render h2 subheading', async () => {
render(await Description({}))
it('should render h2 subheading', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toBeInTheDocument()
})
it('should apply correct CSS classes to h1', async () => {
render(await Description({}))
it('should apply correct CSS classes to h1', () => {
render(<Description />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toHaveClass('title-4xl-semi-bold')
@ -90,8 +86,8 @@ describe('Description', () => {
expect(heading).toHaveClass('text-text-primary')
})
it('should apply correct CSS classes to h2', async () => {
render(await Description({}))
it('should apply correct CSS classes to h2', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('body-md-regular')
@ -104,14 +100,18 @@ describe('Description', () => {
// Non-Chinese Locale Rendering Tests
// ================================
describe('Non-Chinese Locale Rendering', () => {
it('should render discover text for en-US locale', async () => {
render(await Description({ locale: 'en-US' }))
beforeEach(() => {
mockDefaultLocale = 'en-US'
})
it('should render discover text for en-US locale', () => {
render(<Description />)
expect(screen.getByText(/Discover/)).toBeInTheDocument()
})
it('should render all category names', async () => {
render(await Description({ locale: 'en-US' }))
it('should render all category names', () => {
render(<Description />)
expect(screen.getByText('Models')).toBeInTheDocument()
expect(screen.getByText('Tools')).toBeInTheDocument()
@ -122,36 +122,36 @@ describe('Description', () => {
expect(screen.getByText('Bundles')).toBeInTheDocument()
})
it('should render "and" conjunction text', async () => {
render(await Description({ locale: 'en-US' }))
it('should render "and" conjunction text', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('and')
})
it('should render "in" preposition at the end for non-Chinese locales', async () => {
render(await Description({ locale: 'en-US' }))
it('should render "in" preposition at the end for non-Chinese locales', () => {
render(<Description />)
expect(screen.getByText('in')).toBeInTheDocument()
})
it('should render Dify Marketplace text at the end for non-Chinese locales', async () => {
render(await Description({ locale: 'en-US' }))
it('should render Dify Marketplace text at the end for non-Chinese locales', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
it('should render category spans with styled underline effect', async () => {
const { container } = render(await Description({ locale: 'en-US' }))
it('should render category spans with styled underline effect', () => {
const { container } = render(<Description />)
const styledSpans = container.querySelectorAll('.body-md-medium.relative.z-\\[1\\]')
// 7 category spans (models, tools, datasources, triggers, agents, extensions, bundles)
expect(styledSpans.length).toBe(7)
})
it('should apply text-text-secondary class to category spans', async () => {
const { container } = render(await Description({ locale: 'en-US' }))
it('should apply text-text-secondary class to category spans', () => {
const { container } = render(<Description />)
const styledSpans = container.querySelectorAll('.text-text-secondary')
expect(styledSpans.length).toBeGreaterThanOrEqual(7)
@ -162,29 +162,33 @@ describe('Description', () => {
// Chinese (zh-Hans) Locale Rendering Tests
// ================================
describe('Chinese (zh-Hans) Locale Rendering', () => {
it('should render "in" text at the beginning for zh-Hans locale', async () => {
render(await Description({ locale: 'zh-Hans' }))
beforeEach(() => {
mockDefaultLocale = 'zh-Hans'
})
it('should render "in" text at the beginning for zh-Hans locale', () => {
render(<Description />)
// In zh-Hans mode, "in" appears at the beginning
const inElements = screen.getAllByText('in')
expect(inElements.length).toBeGreaterThanOrEqual(1)
})
it('should render Dify Marketplace text for zh-Hans locale', async () => {
render(await Description({ locale: 'zh-Hans' }))
it('should render Dify Marketplace text for zh-Hans locale', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
it('should render discover text for zh-Hans locale', async () => {
render(await Description({ locale: 'zh-Hans' }))
it('should render discover text for zh-Hans locale', () => {
render(<Description />)
expect(screen.getByText(/Discover/)).toBeInTheDocument()
})
it('should render all categories for zh-Hans locale', async () => {
render(await Description({ locale: 'zh-Hans' }))
it('should render all categories for zh-Hans locale', () => {
render(<Description />)
expect(screen.getByText('Models')).toBeInTheDocument()
expect(screen.getByText('Tools')).toBeInTheDocument()
@ -195,8 +199,8 @@ describe('Description', () => {
expect(screen.getByText('Bundles')).toBeInTheDocument()
})
it('should render both zh-Hans specific elements and shared elements', async () => {
render(await Description({ locale: 'zh-Hans' }))
it('should render both zh-Hans specific elements and shared elements', () => {
render(<Description />)
// zh-Hans has specific element order: "in" -> Dify Marketplace -> Discover
// then the same category list with "and" -> Bundles
@ -206,61 +210,57 @@ describe('Description', () => {
})
// ================================
// Locale Prop Variations Tests
// Locale Variations Tests
// ================================
describe('Locale Prop Variations', () => {
it('should use default locale when locale prop is undefined', async () => {
describe('Locale Variations', () => {
it('should use en-US locale by default', () => {
mockDefaultLocale = 'en-US'
render(await Description({}))
render(<Description />)
// Should use the default locale from getLocaleOnServer
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should use provided locale prop instead of default', async () => {
it('should handle ja-JP locale as non-Chinese', () => {
mockDefaultLocale = 'ja-JP'
render(await Description({ locale: 'en-US' }))
// The locale prop should be used, triggering non-Chinese rendering
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toBeInTheDocument()
})
it('should handle ja-JP locale as non-Chinese', async () => {
render(await Description({ locale: 'ja-JP' }))
render(<Description />)
// Should render in non-Chinese format (discover first, then "in Dify Marketplace" at end)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
it('should handle ko-KR locale as non-Chinese', async () => {
render(await Description({ locale: 'ko-KR' }))
it('should handle ko-KR locale as non-Chinese', () => {
mockDefaultLocale = 'ko-KR'
render(<Description />)
// Should render in non-Chinese format
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle de-DE locale as non-Chinese', async () => {
render(await Description({ locale: 'de-DE' }))
it('should handle de-DE locale as non-Chinese', () => {
mockDefaultLocale = 'de-DE'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle fr-FR locale as non-Chinese', async () => {
render(await Description({ locale: 'fr-FR' }))
it('should handle fr-FR locale as non-Chinese', () => {
mockDefaultLocale = 'fr-FR'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle pt-BR locale as non-Chinese', async () => {
render(await Description({ locale: 'pt-BR' }))
it('should handle pt-BR locale as non-Chinese', () => {
mockDefaultLocale = 'pt-BR'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
it('should handle es-ES locale as non-Chinese', async () => {
render(await Description({ locale: 'es-ES' }))
it('should handle es-ES locale as non-Chinese', () => {
mockDefaultLocale = 'es-ES'
render(<Description />)
expect(screen.getByText('Empower your AI development')).toBeInTheDocument()
})
@ -270,24 +270,27 @@ describe('Description', () => {
// Conditional Rendering Tests
// ================================
describe('Conditional Rendering', () => {
it('should render zh-Hans specific content when locale is zh-Hans', async () => {
const { container } = render(await Description({ locale: 'zh-Hans' }))
it('should render zh-Hans specific content when locale is zh-Hans', () => {
mockDefaultLocale = 'zh-Hans'
const { container } = render(<Description />)
// zh-Hans has additional span with mr-1 before "in" text at the start
const mrSpan = container.querySelector('span.mr-1')
expect(mrSpan).toBeInTheDocument()
})
it('should render non-Chinese specific content when locale is not zh-Hans', async () => {
render(await Description({ locale: 'en-US' }))
it('should render non-Chinese specific content when locale is not zh-Hans', () => {
mockDefaultLocale = 'en-US'
render(<Description />)
// Non-Chinese has "in" and "Dify Marketplace" at the end
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading.textContent).toContain('Dify Marketplace')
})
it('should not render zh-Hans intro content for non-Chinese locales', async () => {
render(await Description({ locale: 'en-US' }))
it('should not render zh-Hans intro content for non-Chinese locales', () => {
mockDefaultLocale = 'en-US'
render(<Description />)
// For en-US, the order should be Discover ... in Dify Marketplace
// The "in" text should only appear once at the end
@ -303,8 +306,9 @@ describe('Description', () => {
expect(inIndex).toBeLessThan(marketplaceIndex)
})
it('should render zh-Hans with proper word order', async () => {
render(await Description({ locale: 'zh-Hans' }))
it('should render zh-Hans with proper word order', () => {
mockDefaultLocale = 'zh-Hans'
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@ -323,58 +327,58 @@ describe('Description', () => {
// Category Styling Tests
// ================================
describe('Category Styling', () => {
it('should apply underline effect with after pseudo-element styling', async () => {
const { container } = render(await Description({}))
it('should apply underline effect with after pseudo-element styling', () => {
const { container } = render(<Description />)
const categorySpan = container.querySelector('.after\\:absolute')
expect(categorySpan).toBeInTheDocument()
})
it('should apply correct after pseudo-element classes', async () => {
const { container } = render(await Description({}))
it('should apply correct after pseudo-element classes', () => {
const { container } = render(<Description />)
// Check for the specific after pseudo-element classes
const categorySpans = container.querySelectorAll('.after\\:bottom-\\[1\\.5px\\]')
expect(categorySpans.length).toBe(7)
})
it('should apply full width to after element', async () => {
const { container } = render(await Description({}))
it('should apply full width to after element', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.after\\:w-full')
expect(categorySpans.length).toBe(7)
})
it('should apply correct height to after element', async () => {
const { container } = render(await Description({}))
it('should apply correct height to after element', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.after\\:h-2')
expect(categorySpans.length).toBe(7)
})
it('should apply bg-text-text-selected to after element', async () => {
const { container } = render(await Description({}))
it('should apply bg-text-text-selected to after element', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.after\\:bg-text-text-selected')
expect(categorySpans.length).toBe(7)
})
it('should have z-index 1 on category spans', async () => {
const { container } = render(await Description({}))
it('should have z-index 1 on category spans', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.z-\\[1\\]')
expect(categorySpans.length).toBe(7)
})
it('should apply left margin to category spans', async () => {
const { container } = render(await Description({}))
it('should apply left margin to category spans', () => {
const { container } = render(<Description />)
const categorySpans = container.querySelectorAll('.ml-1')
expect(categorySpans.length).toBeGreaterThanOrEqual(7)
})
it('should apply both left and right margin to specific spans', async () => {
const { container } = render(await Description({}))
it('should apply both left and right margin to specific spans', () => {
const { container } = render(<Description />)
// Extensions and Bundles spans have both ml-1 and mr-1
const extensionsBundlesSpans = container.querySelectorAll('.ml-1.mr-1')
@ -386,28 +390,17 @@ describe('Description', () => {
// Edge Cases Tests
// ================================
describe('Edge Cases', () => {
it('should handle empty props object', async () => {
const { container } = render(await Description({}))
expect(container.firstChild).toBeInTheDocument()
})
it('should render fragment as root element', async () => {
const { container } = render(await Description({}))
it('should render fragment as root element', () => {
const { container } = render(<Description />)
// Fragment renders h1 and h2 as direct children
expect(container.querySelector('h1')).toBeInTheDocument()
expect(container.querySelector('h2')).toBeInTheDocument()
})
it('should handle locale prop with undefined value', async () => {
render(await Description({ locale: undefined }))
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument()
})
it('should handle zh-Hant as non-Chinese simplified', async () => {
render(await Description({ locale: 'zh-Hant' }))
it('should handle zh-Hant as non-Chinese simplified', () => {
mockDefaultLocale = 'zh-Hant'
render(<Description />)
// zh-Hant is different from zh-Hans, should use non-Chinese format
const subheading = screen.getByRole('heading', { level: 2 })
@ -426,8 +419,8 @@ describe('Description', () => {
// Content Structure Tests
// ================================
describe('Content Structure', () => {
it('should have comma separators between categories', async () => {
render(await Description({}))
it('should have comma separators between categories', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@ -436,8 +429,8 @@ describe('Description', () => {
expect(content).toMatch(/Models[^\n\r,\u2028\u2029]*,.*Tools[^\n\r,\u2028\u2029]*,.*Data Sources[^\n\r,\u2028\u2029]*,.*Triggers[^\n\r,\u2028\u2029]*,.*Agent Strategies[^\n\r,\u2028\u2029]*,.*Extensions/)
})
it('should have "and" before last category (Bundles)', async () => {
render(await Description({}))
it('should have "and" before last category (Bundles)', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@ -449,8 +442,9 @@ describe('Description', () => {
expect(andIndex).toBeLessThan(bundlesIndex)
})
it('should render all text elements in correct order for en-US', async () => {
render(await Description({ locale: 'en-US' }))
it('should render all text elements in correct order for en-US', () => {
mockDefaultLocale = 'en-US'
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@ -477,8 +471,9 @@ describe('Description', () => {
}
})
it('should render all text elements in correct order for zh-Hans', async () => {
render(await Description({ locale: 'zh-Hans' }))
it('should render all text elements in correct order for zh-Hans', () => {
mockDefaultLocale = 'zh-Hans'
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
const content = subheading.textContent || ''
@ -499,82 +494,48 @@ describe('Description', () => {
// Layout Tests
// ================================
describe('Layout', () => {
it('should have shrink-0 on h1 heading', async () => {
render(await Description({}))
it('should have shrink-0 on h1 heading', () => {
render(<Description />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toHaveClass('shrink-0')
})
it('should have shrink-0 on h2 subheading', async () => {
render(await Description({}))
it('should have shrink-0 on h2 subheading', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('shrink-0')
})
it('should have flex layout on h2', async () => {
render(await Description({}))
it('should have flex layout on h2', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('flex')
})
it('should have items-center on h2', async () => {
render(await Description({}))
it('should have items-center on h2', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('items-center')
})
it('should have justify-center on h2', async () => {
render(await Description({}))
it('should have justify-center on h2', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toHaveClass('justify-center')
})
})
// ================================
// Translation Function Tests
// ================================
describe('Translation Functions', () => {
it('should call getTranslation for plugin namespace', async () => {
const { getTranslation } = await import('@/i18n-config/server')
render(await Description({ locale: 'en-US' }))
expect(getTranslation).toHaveBeenCalledWith('en-US', 'plugin')
})
it('should call getTranslation for common namespace', async () => {
const { getTranslation } = await import('@/i18n-config/server')
render(await Description({ locale: 'en-US' }))
expect(getTranslation).toHaveBeenCalledWith('en-US', 'common')
})
it('should call getLocaleOnServer when locale prop is undefined', async () => {
const { getLocaleOnServer } = await import('@/i18n-config/server')
render(await Description({}))
expect(getLocaleOnServer).toHaveBeenCalled()
})
it('should use locale prop when provided', async () => {
const { getTranslation } = await import('@/i18n-config/server')
render(await Description({ locale: 'ja-JP' }))
expect(getTranslation).toHaveBeenCalledWith('ja-JP', 'plugin')
expect(getTranslation).toHaveBeenCalledWith('ja-JP', 'common')
})
})
// ================================
// Accessibility Tests
// ================================
describe('Accessibility', () => {
it('should have proper heading hierarchy', async () => {
render(await Description({}))
it('should have proper heading hierarchy', () => {
render(<Description />)
const h1 = screen.getByRole('heading', { level: 1 })
const h2 = screen.getByRole('heading', { level: 2 })
@ -583,22 +544,22 @@ describe('Description', () => {
expect(h2).toBeInTheDocument()
})
it('should have readable text content', async () => {
render(await Description({}))
it('should have readable text content', () => {
render(<Description />)
const h1 = screen.getByRole('heading', { level: 1 })
expect(h1.textContent).not.toBe('')
})
it('should have visible h1 heading', async () => {
render(await Description({}))
it('should have visible h1 heading', () => {
render(<Description />)
const heading = screen.getByRole('heading', { level: 1 })
expect(heading).toBeVisible()
})
it('should have visible h2 heading', async () => {
render(await Description({}))
it('should have visible h2 heading', () => {
render(<Description />)
const subheading = screen.getByRole('heading', { level: 2 })
expect(subheading).toBeVisible()
@ -615,8 +576,8 @@ describe('Description Integration', () => {
mockDefaultLocale = 'en-US'
})
it('should render complete component structure', async () => {
const { container } = render(await Description({ locale: 'en-US' }))
it('should render complete component structure', () => {
const { container } = render(<Description />)
// Main headings
expect(container.querySelector('h1')).toBeInTheDocument()
@ -627,8 +588,9 @@ describe('Description Integration', () => {
expect(categorySpans.length).toBe(7)
})
it('should render complete zh-Hans structure', async () => {
const { container } = render(await Description({ locale: 'zh-Hans' }))
it('should render complete zh-Hans structure', () => {
mockDefaultLocale = 'zh-Hans'
const { container } = render(<Description />)
// Main headings
expect(container.querySelector('h1')).toBeInTheDocument()
@ -639,14 +601,16 @@ describe('Description Integration', () => {
expect(categorySpans.length).toBe(7)
})
it('should correctly switch between zh-Hans and en-US layouts', async () => {
it('should correctly differentiate between zh-Hans and en-US layouts', () => {
// Render en-US
const { container: enContainer, unmount: unmountEn } = render(await Description({ locale: 'en-US' }))
mockDefaultLocale = 'en-US'
const { container: enContainer, unmount: unmountEn } = render(<Description />)
const enContent = enContainer.querySelector('h2')?.textContent || ''
unmountEn()
// Render zh-Hans
const { container: zhContainer } = render(await Description({ locale: 'zh-Hans' }))
mockDefaultLocale = 'zh-Hans'
const { container: zhContainer } = render(<Description />)
const zhContent = zhContainer.querySelector('h2')?.textContent || ''
// Both should have all categories
@ -666,14 +630,16 @@ describe('Description Integration', () => {
expect(zhMarketplaceIndex).toBeLessThan(zhDiscoverIndex)
})
it('should maintain consistent styling across locales', async () => {
it('should maintain consistent styling across locales', () => {
// Render en-US
const { container: enContainer, unmount: unmountEn } = render(await Description({ locale: 'en-US' }))
mockDefaultLocale = 'en-US'
const { container: enContainer, unmount: unmountEn } = render(<Description />)
const enCategoryCount = enContainer.querySelectorAll('.body-md-medium').length
unmountEn()
// Render zh-Hans
const { container: zhContainer } = render(await Description({ locale: 'zh-Hans' }))
mockDefaultLocale = 'zh-Hans'
const { container: zhContainer } = render(<Description />)
const zhCategoryCount = zhContainer.querySelectorAll('.body-md-medium').length
// Both should have same number of styled category spans

View File

@ -1,17 +1,11 @@
/* eslint-disable dify-i18n/require-ns-option */
import type { Locale } from '@/i18n-config'
import { getLocaleOnServer, getTranslation } from '@/i18n-config/server'
import { useLocale, useTranslation } from '#i18n'
type DescriptionProps = {
locale?: Locale
}
const Description = async ({
locale: localeFromProps,
}: DescriptionProps) => {
const localeDefault = await getLocaleOnServer()
const { t } = await getTranslation(localeFromProps || localeDefault, 'plugin')
const { t: tCommon } = await getTranslation(localeFromProps || localeDefault, 'common')
const isZhHans = localeFromProps === 'zh-Hans'
const Description = () => {
const { t } = useTranslation('plugin')
const { t: tCommon } = useTranslation('common')
const locale = useLocale()
const isZhHans = locale === 'zh-Hans'
return (
<>

View File

@ -7,9 +7,9 @@ import Line from './line'
// Mock external dependencies only
// ================================
// Mock useMixedTranslation hook
vi.mock('../hooks', () => ({
useMixedTranslation: (_locale?: string) => ({
// Mock i18n translation hook
vi.mock('#i18n', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
@ -471,36 +471,6 @@ describe('Empty', () => {
})
})
// ================================
// Locale Prop Tests
// ================================
describe('Locale Prop', () => {
it('should pass locale to useMixedTranslation', () => {
render(<Empty locale="zh-CN" />)
// Translation should still work
expect(screen.getByText('No plugin found')).toBeInTheDocument()
})
it('should handle undefined locale', () => {
render(<Empty locale={undefined} />)
expect(screen.getByText('No plugin found')).toBeInTheDocument()
})
it('should handle en-US locale', () => {
render(<Empty locale="en-US" />)
expect(screen.getByText('No plugin found')).toBeInTheDocument()
})
it('should handle ja-JP locale', () => {
render(<Empty locale="ja-JP" />)
expect(screen.getByText('No plugin found')).toBeInTheDocument()
})
})
// ================================
// Placeholder Cards Layout Tests
// ================================
@ -634,7 +604,6 @@ describe('Empty', () => {
text="Custom message"
lightCard
className="custom-wrapper"
locale="en-US"
/>,
)
@ -695,12 +664,6 @@ describe('Empty', () => {
expect(container.querySelector('.only-class')).toBeInTheDocument()
})
it('should render with only locale prop', () => {
render(<Empty locale="zh-CN" />)
expect(screen.getByText('No plugin found')).toBeInTheDocument()
})
it('should handle text with unicode characters', () => {
render(<Empty text="没有找到插件 🔍" />)
@ -813,7 +776,7 @@ describe('Empty and Line Integration', () => {
})
it('should render complete Empty component structure', () => {
const { container } = render(<Empty text="Test" lightCard className="test" locale="en-US" />)
const { container } = render(<Empty text="Test" lightCard className="test" />)
// Container
expect(container.querySelector('.test')).toBeInTheDocument()

View File

@ -1,6 +1,6 @@
'use client'
import { useTranslation } from '#i18n'
import { Group } from '@/app/components/base/icons/src/vender/other'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import { cn } from '@/utils/classnames'
import Line from './line'
@ -8,16 +8,14 @@ type Props = {
text?: string
lightCard?: boolean
className?: string
locale?: string
}
const Empty = ({
text,
lightCard,
className,
locale,
}: Props) => {
const { t } = useMixedTranslation(locale)
const { t } = useTranslation()
return (
<div

View File

@ -18,8 +18,6 @@ import {
useEffect,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import i18n from '@/i18n-config/i18next-config'
import { postMarketplace } from '@/service/base'
import { SCROLL_BOTTOM_THRESHOLD } from './constants'
import {
@ -218,21 +216,6 @@ export const useMarketplacePlugins = () => {
}
}
/**
* ! Support zh-Hans, pt-BR, ja-JP and en-US for Marketplace page
* ! For other languages, use en-US as fallback
*/
export const useMixedTranslation = (localeFromOuter?: string) => {
let t = useTranslation().t
if (localeFromOuter)
t = i18n.getFixedT(localeFromOuter)
return {
t,
}
}
export const useMarketplaceContainerScroll = (
callback: () => void,
scrollContainerId = 'marketplace-container',

View File

@ -11,7 +11,6 @@ import { PluginCategoryEnum } from '@/app/components/plugins/types'
// Note: Import after mocks are set up
import { DEFAULT_SORT, SCROLL_BOTTOM_THRESHOLD } from './constants'
import { MarketplaceContext, MarketplaceContextProvider, useMarketplaceContext } from './context'
import { useMixedTranslation } from './hooks'
import PluginTypeSwitch, { PLUGIN_TYPE_SEARCH_MAP } from './plugin-type-switch'
import StickySearchAndSwitchWrapper from './sticky-search-and-switch-wrapper'
import {
@ -602,48 +601,6 @@ describe('utils', () => {
})
})
// ================================
// Hooks Tests
// ================================
describe('hooks', () => {
describe('useMixedTranslation', () => {
it('should return translation function', () => {
const { result } = renderHook(() => useMixedTranslation())
expect(result.current.t).toBeDefined()
expect(typeof result.current.t).toBe('function')
})
it('should return translation key when no translation found', () => {
const { result } = renderHook(() => useMixedTranslation())
// The global mock returns key with namespace prefix
expect(result.current.t('category.all', { ns: 'plugin' })).toBe('plugin.category.all')
})
it('should use locale from outer when provided', () => {
const { result } = renderHook(() => useMixedTranslation('zh-Hans'))
expect(result.current.t).toBeDefined()
})
it('should handle different locale values', () => {
const locales = ['en-US', 'zh-Hans', 'ja-JP', 'pt-BR']
locales.forEach((locale) => {
const { result } = renderHook(() => useMixedTranslation(locale))
expect(result.current.t).toBeDefined()
expect(typeof result.current.t).toBe('function')
})
})
it('should use getFixedT when localeFromOuter is provided', () => {
const { result } = renderHook(() => useMixedTranslation('fr-FR'))
// The global mock returns key with namespace prefix
expect(result.current.t('search', { ns: 'plugin' })).toBe('plugin.search')
})
})
})
// ================================
// useMarketplaceCollectionsAndPlugins Tests
// ================================
@ -2088,17 +2045,6 @@ describe('StickySearchAndSwitchWrapper', () => {
})
describe('Props', () => {
it('should accept locale prop', () => {
render(
<MarketplaceContextProvider>
<StickySearchAndSwitchWrapper locale="zh-Hans" />
</MarketplaceContextProvider>,
)
// Component should render without errors
expect(screen.getByTestId('portal-elem')).toBeInTheDocument()
})
it('should accept showSearchParams prop', () => {
render(
<MarketplaceContextProvider>

View File

@ -1,6 +1,5 @@
import type { MarketplaceCollection, SearchParams } from './types'
import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { TanstackQueryInitializer } from '@/context/query-client'
import { MarketplaceContextProvider } from './context'
import Description from './description'
@ -9,7 +8,6 @@ import StickySearchAndSwitchWrapper from './sticky-search-and-switch-wrapper'
import { getMarketplaceCollectionsAndPlugins } from './utils'
type MarketplaceProps = {
locale: Locale
showInstallButton?: boolean
shouldExclude?: boolean
searchParams?: SearchParams
@ -18,7 +16,6 @@ type MarketplaceProps = {
showSearchParams?: boolean
}
const Marketplace = async ({
locale,
showInstallButton = true,
shouldExclude,
searchParams,
@ -42,14 +39,12 @@ const Marketplace = async ({
scrollContainerId={scrollContainerId}
showSearchParams={showSearchParams}
>
<Description locale={locale} />
<Description />
<StickySearchAndSwitchWrapper
locale={locale}
pluginTypeSwitchClassName={pluginTypeSwitchClassName}
showSearchParams={showSearchParams}
/>
<ListWrapper
locale={locale}
marketplaceCollections={marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
showInstallButton={showInstallButton}

View File

@ -1,6 +1,6 @@
'use client'
import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { useLocale, useTranslation } from '#i18n'
import { RiArrowRightUpLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import { useTheme } from 'next-themes'
@ -11,34 +11,30 @@ import Card from '@/app/components/plugins/card'
import CardMoreInfo from '@/app/components/plugins/card/card-more-info'
import { useTags } from '@/app/components/plugins/hooks'
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import { useLocale } from '@/context/i18n'
import { getPluginDetailLinkInMarketplace, getPluginLinkInMarketplace } from '../utils'
type CardWrapperProps = {
plugin: Plugin
showInstallButton?: boolean
locale?: Locale
}
const CardWrapperComponent = ({
plugin,
showInstallButton,
locale,
}: CardWrapperProps) => {
const { t } = useMixedTranslation(locale)
const { t } = useTranslation()
const { theme } = useTheme()
const [isShowInstallFromMarketplace, {
setTrue: showInstallFromMarketplace,
setFalse: hideInstallFromMarketplace,
}] = useBoolean(false)
const localeFromLocale = useLocale()
const { getTagLabel } = useTags(t)
const locale = useLocale()
const { getTagLabel } = useTags()
// Memoize marketplace link params to prevent unnecessary re-renders
const marketplaceLinkParams = useMemo(() => ({
language: localeFromLocale,
language: locale,
theme,
}), [localeFromLocale, theme])
}), [locale, theme])
// Memoize tag labels to prevent recreating array on every render
const tagLabels = useMemo(() =>
@ -52,7 +48,6 @@ const CardWrapperComponent = ({
<Card
key={plugin.name}
payload={plugin}
locale={locale}
footer={(
<CardMoreInfo
downloadCount={plugin.install_count}
@ -99,7 +94,6 @@ const CardWrapperComponent = ({
<Card
key={plugin.name}
payload={plugin}
locale={locale}
footer={(
<CardMoreInfo
downloadCount={plugin.install_count}

View File

@ -1,6 +1,5 @@
import type { MarketplaceCollection, SearchParamsFromCollection } from '../types'
import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
@ -12,9 +11,9 @@ import ListWrapper from './list-wrapper'
// Mock External Dependencies Only
// ================================
// Mock useMixedTranslation hook
vi.mock('../hooks', () => ({
useMixedTranslation: (_locale?: string) => ({
// Mock i18n translation hook
vi.mock('#i18n', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string, num?: number }) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
@ -28,6 +27,7 @@ vi.mock('../hooks', () => ({
return translations[fullKey] || key
},
}),
useLocale: () => 'en-US',
}))
// Mock useMarketplaceContext with controllable values
@ -148,15 +148,15 @@ vi.mock('@/app/components/plugins/install-plugin/install-from-marketplace', () =
// Mock SortDropdown component
vi.mock('../sort-dropdown', () => ({
default: ({ locale }: { locale: Locale }) => (
<div data-testid="sort-dropdown" data-locale={locale}>Sort</div>
default: () => (
<div data-testid="sort-dropdown">Sort</div>
),
}))
// Mock Empty component
vi.mock('../empty', () => ({
default: ({ className, locale }: { className?: string, locale?: string }) => (
<div data-testid="empty-component" className={className} data-locale={locale}>
default: ({ className }: { className?: string }) => (
<div data-testid="empty-component" className={className}>
No plugins found
</div>
),
@ -233,7 +233,6 @@ describe('List', () => {
marketplaceCollectionPluginsMap: {} as Record<string, Plugin[]>,
plugins: undefined,
showInstallButton: false,
locale: 'en-US' as Locale,
cardContainerClassName: '',
cardRender: undefined,
onMoreClick: undefined,
@ -351,18 +350,6 @@ describe('List', () => {
expect(screen.getByTestId('empty-component')).toHaveClass('custom-empty-class')
})
it('should pass locale to Empty component', () => {
render(
<List
{...defaultProps}
plugins={[]}
locale={'zh-CN' as Locale}
/>,
)
expect(screen.getByTestId('empty-component')).toHaveAttribute('data-locale', 'zh-CN')
})
it('should pass showInstallButton to CardWrapper', () => {
const plugins = createMockPluginList(1)
@ -508,7 +495,6 @@ describe('ListWithCollection', () => {
marketplaceCollections: [] as MarketplaceCollection[],
marketplaceCollectionPluginsMap: {} as Record<string, Plugin[]>,
showInstallButton: false,
locale: 'en-US' as Locale,
cardContainerClassName: '',
cardRender: undefined,
onMoreClick: undefined,
@ -820,7 +806,6 @@ describe('ListWrapper', () => {
marketplaceCollections: [] as MarketplaceCollection[],
marketplaceCollectionPluginsMap: {} as Record<string, Plugin[]>,
showInstallButton: false,
locale: 'en-US' as Locale,
}
beforeEach(() => {
@ -901,14 +886,6 @@ describe('ListWrapper', () => {
expect(screen.queryByTestId('sort-dropdown')).not.toBeInTheDocument()
})
it('should pass locale to SortDropdown', () => {
mockContextValues.plugins = createMockPluginList(1)
render(<ListWrapper {...defaultProps} locale={'zh-CN' as Locale} />)
expect(screen.getByTestId('sort-dropdown')).toHaveAttribute('data-locale', 'zh-CN')
})
})
// ================================
@ -1169,7 +1146,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
locale="en-US"
/>,
)
@ -1188,7 +1164,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
locale="en-US"
/>,
)
@ -1209,7 +1184,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={plugins}
locale="en-US"
/>,
)
@ -1231,7 +1205,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
showInstallButton={true}
locale="en-US"
/>,
)
@ -1252,7 +1225,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
showInstallButton={true}
locale="en-US"
/>,
)
@ -1274,7 +1246,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
showInstallButton={true}
locale="en-US"
/>,
)
@ -1293,7 +1264,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
showInstallButton={true}
locale="en-US"
/>,
)
@ -1310,7 +1280,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
showInstallButton={true}
locale="en-US"
/>,
)
@ -1327,7 +1296,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
showInstallButton={true}
locale="en-US"
/>,
)
@ -1354,7 +1322,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
showInstallButton={false}
locale="en-US"
/>,
)
@ -1375,7 +1342,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
showInstallButton={false}
locale="en-US"
/>,
)
@ -1390,7 +1356,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
locale="en-US"
/>,
)
@ -1414,7 +1379,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
locale="en-US"
/>,
)
@ -1432,7 +1396,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
locale="en-US"
/>,
)
@ -1450,7 +1413,6 @@ describe('CardWrapper (via List integration)', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={[plugin]}
locale="en-US"
/>,
)
@ -1482,7 +1444,6 @@ describe('Combined Workflows', () => {
<ListWrapper
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
locale="en-US"
/>,
)
@ -1501,7 +1462,6 @@ describe('Combined Workflows', () => {
<ListWrapper
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
locale="en-US"
/>,
)
@ -1521,7 +1481,6 @@ describe('Combined Workflows', () => {
<ListWrapper
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
locale="en-US"
/>,
)
@ -1535,7 +1494,6 @@ describe('Combined Workflows', () => {
<ListWrapper
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
locale="en-US"
/>,
)
@ -1551,7 +1509,6 @@ describe('Combined Workflows', () => {
<ListWrapper
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
locale="en-US"
/>,
)
@ -1569,7 +1526,6 @@ describe('Combined Workflows', () => {
<ListWrapper
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
locale="en-US"
/>,
)
@ -1601,7 +1557,6 @@ describe('Accessibility', () => {
<ListWithCollection
marketplaceCollections={collections}
marketplaceCollectionPluginsMap={pluginsMap}
locale="en-US"
/>,
)
@ -1625,7 +1580,6 @@ describe('Accessibility', () => {
marketplaceCollections={collections}
marketplaceCollectionPluginsMap={pluginsMap}
onMoreClick={onMoreClick}
locale="en-US"
/>,
)
@ -1642,7 +1596,6 @@ describe('Accessibility', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={plugins}
locale="en-US"
/>,
)
@ -1668,7 +1621,6 @@ describe('Performance', () => {
marketplaceCollections={[]}
marketplaceCollectionPluginsMap={{}}
plugins={plugins}
locale="en-US"
/>,
)
const endTime = performance.now()
@ -1689,7 +1641,6 @@ describe('Performance', () => {
<ListWithCollection
marketplaceCollections={collections}
marketplaceCollectionPluginsMap={pluginsMap}
locale="en-US"
/>,
)
const endTime = performance.now()

View File

@ -1,7 +1,6 @@
'use client'
import type { Plugin } from '../../types'
import type { MarketplaceCollection } from '../types'
import type { Locale } from '@/i18n-config'
import { cn } from '@/utils/classnames'
import Empty from '../empty'
import CardWrapper from './card-wrapper'
@ -12,7 +11,6 @@ type ListProps = {
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
plugins?: Plugin[]
showInstallButton?: boolean
locale: Locale
cardContainerClassName?: string
cardRender?: (plugin: Plugin) => React.JSX.Element | null
onMoreClick?: () => void
@ -23,7 +21,6 @@ const List = ({
marketplaceCollectionPluginsMap,
plugins,
showInstallButton,
locale,
cardContainerClassName,
cardRender,
onMoreClick,
@ -37,7 +34,6 @@ const List = ({
marketplaceCollections={marketplaceCollections}
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMap}
showInstallButton={showInstallButton}
locale={locale}
cardContainerClassName={cardContainerClassName}
cardRender={cardRender}
onMoreClick={onMoreClick}
@ -61,7 +57,6 @@ const List = ({
key={`${plugin.org}/${plugin.name}`}
plugin={plugin}
showInstallButton={showInstallButton}
locale={locale}
/>
)
})
@ -71,7 +66,7 @@ const List = ({
}
{
plugins && !plugins.length && (
<Empty className={emptyClassName} locale={locale} />
<Empty className={emptyClassName} />
)
}
</>

View File

@ -3,9 +3,8 @@
import type { MarketplaceCollection } from '../types'
import type { SearchParamsFromCollection } from '@/app/components/plugins/marketplace/types'
import type { Plugin } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { useLocale, useTranslation } from '#i18n'
import { RiArrowRightSLine } from '@remixicon/react'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import { getLanguage } from '@/i18n-config/language'
import { cn } from '@/utils/classnames'
import CardWrapper from './card-wrapper'
@ -14,7 +13,6 @@ type ListWithCollectionProps = {
marketplaceCollections: MarketplaceCollection[]
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
showInstallButton?: boolean
locale: Locale
cardContainerClassName?: string
cardRender?: (plugin: Plugin) => React.JSX.Element | null
onMoreClick?: (searchParams?: SearchParamsFromCollection) => void
@ -23,12 +21,12 @@ const ListWithCollection = ({
marketplaceCollections,
marketplaceCollectionPluginsMap,
showInstallButton,
locale,
cardContainerClassName,
cardRender,
onMoreClick,
}: ListWithCollectionProps) => {
const { t } = useMixedTranslation(locale)
const { t } = useTranslation()
const locale = useLocale()
return (
<>
@ -72,7 +70,6 @@ const ListWithCollection = ({
key={plugin.plugin_id}
plugin={plugin}
showInstallButton={showInstallButton}
locale={locale}
/>
)
})

View File

@ -1,10 +1,9 @@
'use client'
import type { Plugin } from '../../types'
import type { MarketplaceCollection } from '../types'
import type { Locale } from '@/i18n-config'
import { useTranslation } from '#i18n'
import { useEffect } from 'react'
import Loading from '@/app/components/base/loading'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import { useMarketplaceContext } from '../context'
import SortDropdown from '../sort-dropdown'
import List from './index'
@ -13,15 +12,13 @@ type ListWrapperProps = {
marketplaceCollections: MarketplaceCollection[]
marketplaceCollectionPluginsMap: Record<string, Plugin[]>
showInstallButton?: boolean
locale: Locale
}
const ListWrapper = ({
marketplaceCollections,
marketplaceCollectionPluginsMap,
showInstallButton,
locale,
}: ListWrapperProps) => {
const { t } = useMixedTranslation(locale)
const { t } = useTranslation()
const plugins = useMarketplaceContext(v => v.plugins)
const pluginsTotal = useMarketplaceContext(v => v.pluginsTotal)
const marketplaceCollectionsFromClient = useMarketplaceContext(v => v.marketplaceCollectionsFromClient)
@ -55,7 +52,7 @@ const ListWrapper = ({
<div className="mb-4 flex items-center pt-3">
<div className="title-xl-semi-bold text-text-primary">{t('marketplace.pluginsResult', { ns: 'plugin', num: pluginsTotal })}</div>
<div className="mx-3 h-3.5 w-[1px] bg-divider-regular"></div>
<SortDropdown locale={locale} />
<SortDropdown />
</div>
)
}
@ -73,7 +70,6 @@ const ListWrapper = ({
marketplaceCollectionPluginsMap={marketplaceCollectionPluginsMapFromClient || marketplaceCollectionPluginsMap}
plugins={plugins}
showInstallButton={showInstallButton}
locale={locale}
onMoreClick={handleMoreClick}
/>
)

View File

@ -1,4 +1,5 @@
'use client'
import { useTranslation } from '#i18n'
import {
RiArchive2Line,
RiBrain2Line,
@ -12,7 +13,6 @@ import { Trigger as TriggerIcon } from '@/app/components/base/icons/src/vender/p
import { cn } from '@/utils/classnames'
import { PluginCategoryEnum } from '../types'
import { useMarketplaceContext } from './context'
import { useMixedTranslation } from './hooks'
export const PLUGIN_TYPE_SEARCH_MAP = {
all: 'all',
@ -25,16 +25,14 @@ export const PLUGIN_TYPE_SEARCH_MAP = {
bundle: 'bundle',
}
type PluginTypeSwitchProps = {
locale?: string
className?: string
showSearchParams?: boolean
}
const PluginTypeSwitch = ({
locale,
className,
showSearchParams,
}: PluginTypeSwitchProps) => {
const { t } = useMixedTranslation(locale)
const { t } = useTranslation()
const activePluginType = useMarketplaceContext(s => s.activePluginType)
const handleActivePluginTypeChange = useMarketplaceContext(s => s.handleActivePluginTypeChange)

View File

@ -10,9 +10,9 @@ import ToolSelectorTrigger from './trigger/tool-selector'
// Mock external dependencies only
// ================================
// Mock useMixedTranslation hook
vi.mock('../hooks', () => ({
useMixedTranslation: (_locale?: string) => ({
// Mock i18n translation hook
vi.mock('#i18n', () => ({
useTranslation: () => ({
t: (key: string, options?: { ns?: string }) => {
// Build full key with namespace prefix if provided
const fullKey = options?.ns ? `${options.ns}.${key}` : key
@ -364,13 +364,6 @@ describe('SearchBox', () => {
expect(container.querySelector('.custom-input-class')).toBeInTheDocument()
})
it('should pass locale to TagsFilter', () => {
render(<SearchBox {...defaultProps} locale="zh-CN" />)
// TagsFilter should be rendered with locale
expect(screen.getByTestId('portal-elem')).toBeInTheDocument()
})
it('should handle empty placeholder', () => {
render(<SearchBox {...defaultProps} placeholder="" />)
@ -449,12 +442,6 @@ describe('SearchBoxWrapper', () => {
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should render with locale prop', () => {
render(<SearchBoxWrapper locale="en-US" />)
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should render in marketplace mode', () => {
const { container } = render(<SearchBoxWrapper />)
@ -500,13 +487,6 @@ describe('SearchBoxWrapper', () => {
expect(screen.getByPlaceholderText('Search plugins')).toBeInTheDocument()
})
it('should pass locale to useMixedTranslation', () => {
render(<SearchBoxWrapper locale="zh-CN" />)
// Translation should still work
expect(screen.getByPlaceholderText('Search plugins')).toBeInTheDocument()
})
})
})
@ -665,12 +645,6 @@ describe('MarketplaceTrigger', () => {
})
describe('Props Variations', () => {
it('should handle locale prop', () => {
render(<MarketplaceTrigger {...defaultProps} locale="zh-CN" />)
expect(screen.getByText('All Tags')).toBeInTheDocument()
})
it('should handle empty tagsMap', () => {
const { container } = render(
<MarketplaceTrigger {...defaultProps} tagsMap={{}} tags={[]} />,
@ -1251,7 +1225,6 @@ describe('Combined Workflows', () => {
supportAddCustomTool
onShowAddCustomCollectionModal={vi.fn()}
placeholder="Search plugins"
locale="en-US"
wrapperClassName="custom-wrapper"
inputClassName="custom-input"
autoFocus={false}

View File

@ -13,7 +13,6 @@ type SearchBoxProps = {
tags: string[]
onTagsChange: (tags: string[]) => void
placeholder?: string
locale?: string
supportAddCustomTool?: boolean
usedInMarketplace?: boolean
onShowAddCustomCollectionModal?: () => void
@ -28,7 +27,6 @@ const SearchBox = ({
tags,
onTagsChange,
placeholder = '',
locale,
usedInMarketplace = false,
supportAddCustomTool,
onShowAddCustomCollectionModal,
@ -49,7 +47,6 @@ const SearchBox = ({
tags={tags}
onTagsChange={onTagsChange}
usedInMarketplace
locale={locale}
/>
<Divider type="vertical" className="mx-1 h-3.5" />
<div className="flex grow items-center gap-x-2 p-1">
@ -109,7 +106,6 @@ const SearchBox = ({
<TagsFilter
tags={tags}
onTagsChange={onTagsChange}
locale={locale}
/>
</>
)

View File

@ -1,16 +1,11 @@
'use client'
import { useTranslation } from '#i18n'
import { useMarketplaceContext } from '../context'
import { useMixedTranslation } from '../hooks'
import SearchBox from './index'
type SearchBoxWrapperProps = {
locale?: string
}
const SearchBoxWrapper = ({
locale,
}: SearchBoxWrapperProps) => {
const { t } = useMixedTranslation(locale)
const SearchBoxWrapper = () => {
const { t } = useTranslation()
const searchPluginText = useMarketplaceContext(v => v.searchPluginText)
const handleSearchPluginTextChange = useMarketplaceContext(v => v.handleSearchPluginTextChange)
const filterPluginTags = useMarketplaceContext(v => v.filterPluginTags)
@ -24,7 +19,6 @@ const SearchBoxWrapper = ({
onSearchChange={handleSearchPluginTextChange}
tags={filterPluginTags}
onTagsChange={handleFilterPluginTagsChange}
locale={locale}
placeholder={t('searchPlugins', { ns: 'plugin' })}
usedInMarketplace
/>

View File

@ -1,5 +1,6 @@
'use client'
import { useTranslation } from '#i18n'
import { useState } from 'react'
import Checkbox from '@/app/components/base/checkbox'
import Input from '@/app/components/base/input'
@ -9,7 +10,6 @@ import {
PortalToFollowElemTrigger,
} from '@/app/components/base/portal-to-follow-elem'
import { useTags } from '@/app/components/plugins/hooks'
import { useMixedTranslation } from '@/app/components/plugins/marketplace/hooks'
import MarketplaceTrigger from './trigger/marketplace'
import ToolSelectorTrigger from './trigger/tool-selector'
@ -17,18 +17,16 @@ type TagsFilterProps = {
tags: string[]
onTagsChange: (tags: string[]) => void
usedInMarketplace?: boolean
locale?: string
}
const TagsFilter = ({
tags,
onTagsChange,
usedInMarketplace = false,
locale,
}: TagsFilterProps) => {
const { t } = useMixedTranslation(locale)
const { t } = useTranslation()
const [open, setOpen] = useState(false)
const [searchText, setSearchText] = useState('')
const { tags: options, tagsMap } = useTags(t)
const { tags: options, tagsMap } = useTags()
const filteredOptions = options.filter(option => option.label.toLowerCase().includes(searchText.toLowerCase()))
const handleCheck = (id: string) => {
if (tags.includes(id))
@ -59,7 +57,6 @@ const TagsFilter = ({
open={open}
tags={tags}
tagsMap={tagsMap}
locale={locale}
onTagsChange={onTagsChange}
/>
)

View File

@ -1,15 +1,14 @@
import type { Tag } from '../../../hooks'
import { useTranslation } from '#i18n'
import { RiArrowDownSLine, RiCloseCircleFill, RiFilter3Line } from '@remixicon/react'
import * as React from 'react'
import { cn } from '@/utils/classnames'
import { useMixedTranslation } from '../../hooks'
type MarketplaceTriggerProps = {
selectedTagsLength: number
open: boolean
tags: string[]
tagsMap: Record<string, Tag>
locale?: string
onTagsChange: (tags: string[]) => void
}
@ -18,10 +17,9 @@ const MarketplaceTrigger = ({
open,
tags,
tagsMap,
locale,
onTagsChange,
}: MarketplaceTriggerProps) => {
const { t } = useMixedTranslation(locale)
const { t } = useTranslation()
return (
<div

Some files were not shown because too many files have changed in this diff Show More