mirror of https://github.com/langgenius/dify.git
chore: remove next img (#33517)
This commit is contained in:
parent
041d7ffe3d
commit
4822d550b6
|
|
@ -4,7 +4,6 @@ import type { AppIconSelection } from '../../base/app-icon-picker'
|
|||
import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react'
|
||||
|
||||
import { useDebounceFn, useKeyPress } from 'ahooks'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -117,10 +116,10 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
<div className="px-10">
|
||||
<div className="h-6 w-full 2xl:h-[139px]" />
|
||||
<div className="pb-6 pt-1">
|
||||
<span className="title-2xl-semi-bold text-text-primary">{t('newApp.startFromBlank', { ns: 'app' })}</span>
|
||||
<span className="text-text-primary title-2xl-semi-bold">{t('newApp.startFromBlank', { ns: 'app' })}</span>
|
||||
</div>
|
||||
<div className="mb-2 leading-6">
|
||||
<span className="system-sm-semibold text-text-secondary">{t('newApp.chooseAppType', { ns: 'app' })}</span>
|
||||
<span className="text-text-secondary system-sm-semibold">{t('newApp.chooseAppType', { ns: 'app' })}</span>
|
||||
</div>
|
||||
<div className="flex w-[660px] flex-col gap-4">
|
||||
<div>
|
||||
|
|
@ -160,7 +159,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
className="flex cursor-pointer items-center border-0 bg-transparent p-0"
|
||||
onClick={() => setIsAppTypeExpanded(!isAppTypeExpanded)}
|
||||
>
|
||||
<span className="system-2xs-medium-uppercase text-text-tertiary">{t('newApp.forBeginners', { ns: 'app' })}</span>
|
||||
<span className="text-text-tertiary system-2xs-medium-uppercase">{t('newApp.forBeginners', { ns: 'app' })}</span>
|
||||
<RiArrowRightSLine className={`ml-1 h-4 w-4 text-text-tertiary transition-transform ${isAppTypeExpanded ? 'rotate-90' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -212,7 +211,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
<div className="flex items-center space-x-3">
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 flex h-6 items-center">
|
||||
<label className="system-sm-semibold text-text-secondary">{t('newApp.captionName', { ns: 'app' })}</label>
|
||||
<label className="text-text-secondary system-sm-semibold">{t('newApp.captionName', { ns: 'app' })}</label>
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
|
|
@ -243,8 +242,8 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
</div>
|
||||
<div>
|
||||
<div className="mb-1 flex h-6 items-center">
|
||||
<label className="system-sm-semibold text-text-secondary">{t('newApp.captionDescription', { ns: 'app' })}</label>
|
||||
<span className="system-xs-regular ml-1 text-text-tertiary">
|
||||
<label className="text-text-secondary system-sm-semibold">{t('newApp.captionDescription', { ns: 'app' })}</label>
|
||||
<span className="ml-1 text-text-tertiary system-xs-regular">
|
||||
(
|
||||
{t('newApp.optional', { ns: 'app' })}
|
||||
)
|
||||
|
|
@ -260,7 +259,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
|||
</div>
|
||||
{isAppsFull && <AppsFull className="mt-4" loc="app-create" />}
|
||||
<div className="flex items-center justify-between pb-10 pt-5">
|
||||
<div className="system-xs-regular flex cursor-pointer items-center gap-1 text-text-tertiary" onClick={onCreateFromTemplate}>
|
||||
<div className="flex cursor-pointer items-center gap-1 text-text-tertiary system-xs-regular" onClick={onCreateFromTemplate}>
|
||||
<span>{t('newApp.noIdeaTip', { ns: 'app' })}</span>
|
||||
<div className="p-[1px]">
|
||||
<RiArrowRightLine className="h-3.5 w-3.5" />
|
||||
|
|
@ -334,8 +333,8 @@ function AppTypeCard({ icon, title, description, active, onClick }: AppTypeCardP
|
|||
onClick={onClick}
|
||||
>
|
||||
{icon}
|
||||
<div className="system-sm-semibold mb-0.5 mt-2 text-text-secondary">{title}</div>
|
||||
<div className="system-xs-regular line-clamp-2 text-text-tertiary" title={description}>{description}</div>
|
||||
<div className="mb-0.5 mt-2 text-text-secondary system-sm-semibold">{title}</div>
|
||||
<div className="line-clamp-2 text-text-tertiary system-xs-regular" title={description}>{description}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -367,8 +366,8 @@ function AppPreview({ mode }: { mode: AppModeEnum }) {
|
|||
const previewInfo = modeToPreviewInfoMap[mode]
|
||||
return (
|
||||
<div className="px-8 py-4">
|
||||
<h4 className="system-sm-semibold-uppercase text-text-secondary">{previewInfo.title}</h4>
|
||||
<div className="system-xs-regular mt-1 min-h-8 max-w-96 text-text-tertiary">
|
||||
<h4 className="text-text-secondary system-sm-semibold-uppercase">{previewInfo.title}</h4>
|
||||
<div className="mt-1 min-h-8 max-w-96 text-text-tertiary system-xs-regular">
|
||||
<span>{previewInfo.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -389,7 +388,7 @@ function AppScreenShot({ mode, show }: { mode: AppModeEnum, show: boolean }) {
|
|||
<source media="(resolution: 1x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`} />
|
||||
<source media="(resolution: 2x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}@2x.png`} />
|
||||
<source media="(resolution: 3x)" srcSet={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}@3x.png`} />
|
||||
<Image
|
||||
<img
|
||||
className={show ? '' : 'hidden'}
|
||||
src={`${basePath}/screenshots/${theme}/${modeToImageMap[mode]}.png`}
|
||||
alt="App Screen Shot"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/* eslint-disable next/no-img-element */
|
||||
import type { ImgHTMLAttributes } from 'react'
|
||||
import type { EmbeddedChatbotContextValue } from '../../context'
|
||||
import type { AppData } from '@/models/share'
|
||||
import type { SystemFeatures } from '@/types/feature'
|
||||
|
|
@ -22,15 +20,6 @@ vi.mock('@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropd
|
|||
default: () => <div data-testid="view-form-dropdown" />,
|
||||
}))
|
||||
|
||||
// Mock next/image to render a normal img tag for testing
|
||||
vi.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: (props: ImgHTMLAttributes<HTMLImageElement> & { unoptimized?: boolean }) => {
|
||||
const { unoptimized: _, ...rest } = props
|
||||
return <img {...rest} />
|
||||
},
|
||||
}))
|
||||
|
||||
type GlobalPublicStoreMock = {
|
||||
systemFeatures: SystemFeatures
|
||||
setSystemFeatures: (systemFeatures: SystemFeatures) => void
|
||||
|
|
|
|||
|
|
@ -1,13 +1,7 @@
|
|||
/* eslint-disable next/no-img-element */
|
||||
import type { ImgHTMLAttributes } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import CheckboxList from '..'
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
default: (props: ImgHTMLAttributes<HTMLImageElement>) => <img {...props} />,
|
||||
}))
|
||||
|
||||
describe('checkbox list component', () => {
|
||||
const options = [
|
||||
{ label: 'Option 1', value: 'option1' },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
|
@ -169,7 +168,7 @@ const CheckboxList: FC<CheckboxListProps> = ({
|
|||
{searchQuery
|
||||
? (
|
||||
<div className="flex flex-col items-center justify-center gap-2">
|
||||
<Image alt="search menu" src={SearchMenu} width={32} />
|
||||
<img alt="search menu" src={SearchMenu.src} width={32} />
|
||||
<span className="text-text-secondary system-sm-regular">{t('operation.noSearchResults', { ns: 'common', content: title })}</span>
|
||||
<Button variant="secondary-accent" size="small" onClick={() => setSearchQuery('')}>{t('operation.resetKeywords', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,7 @@
|
|||
/* eslint-disable next/no-img-element */
|
||||
import type { ImgHTMLAttributes } from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import FileThumb from '../index'
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
__esModule: true,
|
||||
default: (props: ImgHTMLAttributes<HTMLImageElement>) => <img {...props} />,
|
||||
}))
|
||||
|
||||
describe('FileThumb Component', () => {
|
||||
const mockImageFile = {
|
||||
name: 'test-image.jpg',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,6 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import WithIconCardItem from './with-icon-card-item'
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
default: ({ unoptimized: _unoptimized, ...props }: React.ImgHTMLAttributes<HTMLImageElement> & { unoptimized?: boolean }) => <img {...props} />,
|
||||
}))
|
||||
|
||||
describe('WithIconCardItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import type { WithIconCardItemProps } from './markdown-with-directive-schema'
|
||||
import Image from 'next/image'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type WithIconItemProps = WithIconCardItemProps & {
|
||||
|
|
@ -11,18 +10,13 @@ type WithIconItemProps = WithIconCardItemProps & {
|
|||
function WithIconCardItem({ icon, children, className, iconAlt }: WithIconItemProps) {
|
||||
return (
|
||||
<div className={cn('flex h-11 items-center space-x-3 rounded-lg bg-background-section px-2', className)}>
|
||||
{/*
|
||||
* unoptimized to "url parameter is not allowed" for external domains despite correct remotePatterns configuration.
|
||||
* https://github.com/vercel/next.js/issues/88873
|
||||
*/}
|
||||
<Image
|
||||
<img
|
||||
src={icon}
|
||||
className="!border-none object-contain"
|
||||
alt={iconAlt ?? ''}
|
||||
aria-hidden={iconAlt ? undefined : true}
|
||||
width={40}
|
||||
height={40}
|
||||
unoptimized
|
||||
/>
|
||||
<div className="min-w-0 grow overflow-hidden text-text-secondary system-sm-medium [&_p]:!m-0 [&_p]:block [&_p]:w-full [&_p]:overflow-hidden [&_p]:text-ellipsis [&_p]:whitespace-nowrap">
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,6 @@ import { MarkdownWithDirective } from './index'
|
|||
|
||||
const FOUR_COLON_RE = /:{4}/
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => <img {...props} />,
|
||||
}))
|
||||
|
||||
function expectDecorativeIcon(container: HTMLElement, src: string) {
|
||||
const icon = container.querySelector('img')
|
||||
expect(icon).toBeInTheDocument()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'
|
|||
import { describe, expect, it, vi } from 'vitest'
|
||||
import CredentialSelector from '../index'
|
||||
|
||||
// Mock CredentialIcon since it's likely a complex component or uses next/image
|
||||
// Mock CredentialIcon since it's likely a complex component.
|
||||
vi.mock('@/app/components/datasets/common/credential-icon', () => ({
|
||||
CredentialIcon: ({ name }: { name: string }) => <div data-testid="credential-icon">{name}</div>,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -4,13 +4,6 @@ import { RETRIEVE_METHOD } from '@/types/app'
|
|||
import { retrievalIcon } from '../../../create/icons'
|
||||
import RetrievalMethodInfo, { getIcon } from '../index'
|
||||
|
||||
// Override global next/image auto-mock: tests assert on rendered <img> src attributes via data-testid
|
||||
vi.mock('next/image', () => ({
|
||||
default: ({ src, alt, className }: { src: string, alt: string, className?: string }) => (
|
||||
<img src={src} alt={alt || ''} className={className} data-testid="method-icon" />
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock RadioCard
|
||||
vi.mock('@/app/components/base/radio-card', () => ({
|
||||
default: ({ title, description, chosenConfig, icon }: { title: string, description: string, chosenConfig: ReactNode, icon: ReactNode }) => (
|
||||
|
|
@ -50,7 +43,7 @@ describe('RetrievalMethodInfo', () => {
|
|||
})
|
||||
|
||||
it('should render correctly with full config', () => {
|
||||
render(<RetrievalMethodInfo value={defaultConfig} />)
|
||||
const { container } = render(<RetrievalMethodInfo value={defaultConfig} />)
|
||||
|
||||
expect(screen.getByTestId('radio-card')).toBeInTheDocument()
|
||||
|
||||
|
|
@ -59,7 +52,7 @@ describe('RetrievalMethodInfo', () => {
|
|||
expect(screen.getByTestId('card-description')).toHaveTextContent('dataset.retrieval.semantic_search.description')
|
||||
|
||||
// Check Icon
|
||||
const icon = screen.getByTestId('method-icon')
|
||||
const icon = container.querySelector('img')
|
||||
expect(icon).toHaveAttribute('src', 'vector-icon.png')
|
||||
|
||||
// Check Config Details
|
||||
|
|
@ -87,18 +80,18 @@ describe('RetrievalMethodInfo', () => {
|
|||
it('should handle different retrieval methods', () => {
|
||||
// Test Hybrid
|
||||
const hybridConfig = { ...defaultConfig, search_method: RETRIEVE_METHOD.hybrid }
|
||||
const { unmount } = render(<RetrievalMethodInfo value={hybridConfig} />)
|
||||
const { container, unmount } = render(<RetrievalMethodInfo value={hybridConfig} />)
|
||||
|
||||
expect(screen.getByTestId('card-title')).toHaveTextContent('dataset.retrieval.hybrid_search.title')
|
||||
expect(screen.getByTestId('method-icon')).toHaveAttribute('src', 'hybrid-icon.png')
|
||||
expect(container.querySelector('img')).toHaveAttribute('src', 'hybrid-icon.png')
|
||||
|
||||
unmount()
|
||||
|
||||
// Test FullText
|
||||
const fullTextConfig = { ...defaultConfig, search_method: RETRIEVE_METHOD.fullText }
|
||||
render(<RetrievalMethodInfo value={fullTextConfig} />)
|
||||
const { container: fullTextContainer } = render(<RetrievalMethodInfo value={fullTextConfig} />)
|
||||
expect(screen.getByTestId('card-title')).toHaveTextContent('dataset.retrieval.full_text_search.title')
|
||||
expect(screen.getByTestId('method-icon')).toHaveAttribute('src', 'fulltext-icon.png')
|
||||
expect(fullTextContainer.querySelector('img')).toHaveAttribute('src', 'fulltext-icon.png')
|
||||
})
|
||||
|
||||
describe('getIcon utility', () => {
|
||||
|
|
@ -132,17 +125,17 @@ describe('RetrievalMethodInfo', () => {
|
|||
|
||||
it('should render correctly with invertedIndex search method', () => {
|
||||
const invertedIndexConfig = { ...defaultConfig, search_method: RETRIEVE_METHOD.invertedIndex }
|
||||
render(<RetrievalMethodInfo value={invertedIndexConfig} />)
|
||||
const { container } = render(<RetrievalMethodInfo value={invertedIndexConfig} />)
|
||||
|
||||
// invertedIndex uses vector icon
|
||||
expect(screen.getByTestId('method-icon')).toHaveAttribute('src', 'vector-icon.png')
|
||||
expect(container.querySelector('img')).toHaveAttribute('src', 'vector-icon.png')
|
||||
})
|
||||
|
||||
it('should render correctly with keywordSearch search method', () => {
|
||||
const keywordSearchConfig = { ...defaultConfig, search_method: RETRIEVE_METHOD.keywordSearch }
|
||||
render(<RetrievalMethodInfo value={keywordSearchConfig} />)
|
||||
const { container } = render(<RetrievalMethodInfo value={keywordSearchConfig} />)
|
||||
|
||||
// keywordSearch uses vector icon
|
||||
expect(screen.getByTestId('method-icon')).toHaveAttribute('src', 'vector-icon.png')
|
||||
expect(container.querySelector('img')).toHaveAttribute('src', 'vector-icon.png')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import Image from 'next/image'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RadioCard from '@/app/components/base/radio-card'
|
||||
|
|
@ -28,7 +27,7 @@ const EconomicalRetrievalMethodConfig: FC<Props> = ({
|
|||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const type = value.search_method
|
||||
const icon = <Image className="size-3.5 text-util-colors-purple-purple-600" src={getIcon(type)} alt="" />
|
||||
const icon = <img className="size-3.5 text-util-colors-purple-purple-600" src={getIcon(type)} alt="" />
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<RadioCard
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import Image from 'next/image'
|
||||
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
|
@ -127,7 +126,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
|||
/>
|
||||
)}
|
||||
<div className="flex items-center">
|
||||
<span className="system-sm-semibold mr-0.5 text-text-secondary">{t('modelProvider.rerankModel.key', { ns: 'common' })}</span>
|
||||
<span className="mr-0.5 text-text-secondary system-sm-semibold">{t('modelProvider.rerankModel.key', { ns: 'common' })}</span>
|
||||
<Tooltip
|
||||
popupContent={
|
||||
<div className="w-[200px]">{t('modelProvider.rerankModel.tip', { ns: 'common' })}</div>
|
||||
|
|
@ -157,7 +156,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
|||
<div className="p-1">
|
||||
<AlertTriangle className="size-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<span className="system-xs-medium text-text-primary">
|
||||
<span className="text-text-primary system-xs-medium">
|
||||
{t('form.retrievalSetting.multiModalTip', { ns: 'datasetSettings' })}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -215,11 +214,11 @@ const RetrievalParamConfig: FC<Props> = ({
|
|||
isChosen={value.reranking_mode === option.value}
|
||||
onChosen={() => handleChangeRerankMode(option.value)}
|
||||
icon={(
|
||||
<Image
|
||||
<img
|
||||
src={
|
||||
option.value === RerankingModeEnum.WeightedScore
|
||||
? ProgressIndicator
|
||||
: Reranking
|
||||
? ProgressIndicator.src
|
||||
: Reranking.src
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
|
|
@ -281,7 +280,7 @@ const RetrievalParamConfig: FC<Props> = ({
|
|||
<div className="p-1">
|
||||
<AlertTriangle className="size-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<span className="system-xs-medium text-text-primary">
|
||||
<span className="text-text-primary system-xs-medium">
|
||||
{t('form.retrievalSetting.multiModalTip', { ns: 'datasetSettings' })}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,14 +20,6 @@ vi.mock('next/navigation', () => ({
|
|||
useRouter: () => mockRouter,
|
||||
}))
|
||||
|
||||
// Override global next/image auto-mock: test asserts on data-testid="next-image"
|
||||
vi.mock('next/image', () => ({
|
||||
default: ({ src, alt, className }: { src: string, alt: string, className?: string }) => (
|
||||
// eslint-disable-next-line next/no-img-element
|
||||
<img src={src} alt={alt} className={className} data-testid="next-image" />
|
||||
),
|
||||
}))
|
||||
|
||||
// Mock API service
|
||||
const mockFetchIndexingStatusBatch = vi.fn()
|
||||
vi.mock('@/service/datasets', () => ({
|
||||
|
|
@ -979,9 +971,9 @@ describe('RuleDetail', () => {
|
|||
})
|
||||
|
||||
it('should render correct icon for indexing type', () => {
|
||||
render(<RuleDetail indexingType="high_quality" />)
|
||||
const { container } = render(<RuleDetail indexingType="high_quality" />)
|
||||
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
const images = container.querySelectorAll('img')
|
||||
expect(images.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { FC } from 'react'
|
||||
import type { ProcessRuleResponse } from '@/models/datasets'
|
||||
import Image from 'next/image'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { FieldInfo } from '@/app/components/datasets/documents/detail/metadata'
|
||||
|
|
@ -119,12 +118,12 @@ const RuleDetail: FC<RuleDetailProps> = ({ sourceData, indexingType, retrievalMe
|
|||
<FieldInfo
|
||||
label={t('stepTwo.indexMode', { ns: 'datasetCreation' })}
|
||||
displayedValue={indexModeLabel}
|
||||
valueIcon={<Image className="size-4" src={indexMethodIconSrc} alt="" />}
|
||||
valueIcon={<img className="size-4" src={indexMethodIconSrc} alt="" />}
|
||||
/>
|
||||
<FieldInfo
|
||||
label={t('form.retrievalSetting.title', { ns: 'datasetSettings' })}
|
||||
displayedValue={retrievalLabel}
|
||||
valueIcon={<Image className="size-4" src={retrievalIconSrc} alt="" />}
|
||||
valueIcon={<img className="size-4" src={retrievalIconSrc} alt="" />}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,12 @@ import Research from './assets/research-mod.svg'
|
|||
import Selection from './assets/selection-mod.svg'
|
||||
|
||||
export const indexMethodIcon = {
|
||||
high_quality: GoldIcon,
|
||||
economical: Piggybank,
|
||||
high_quality: GoldIcon.src,
|
||||
economical: Piggybank.src,
|
||||
}
|
||||
|
||||
export const retrievalIcon = {
|
||||
vector: Selection,
|
||||
fullText: Research,
|
||||
hybrid: PatternRecognition,
|
||||
vector: Selection.src,
|
||||
fullText: Research.src,
|
||||
hybrid: PatternRecognition.src,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,6 @@ import { fireEvent, render, screen } from '@testing-library/react'
|
|||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { OptionCard, OptionCardHeader } from '../option-card'
|
||||
|
||||
// Override global next/image auto-mock: tests assert on rendered <img> elements
|
||||
vi.mock('next/image', () => ({
|
||||
default: ({ src, alt, ...props }: { src?: string, alt?: string, width?: number, height?: number }) => (
|
||||
<img src={src} alt={alt} {...props} />
|
||||
),
|
||||
}))
|
||||
|
||||
describe('OptionCardHeader', () => {
|
||||
const defaultProps = {
|
||||
icon: <span data-testid="icon">icon</span>,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import {
|
|||
RiAlertFill,
|
||||
RiSearchEyeLine,
|
||||
} from '@remixicon/react'
|
||||
import Image from 'next/image'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
|
|
@ -26,7 +25,7 @@ type TextLabelProps = {
|
|||
}
|
||||
|
||||
const TextLabel: FC<TextLabelProps> = ({ children }) => {
|
||||
return <label className="system-sm-semibold text-text-secondary">{children}</label>
|
||||
return <label className="text-text-secondary system-sm-semibold">{children}</label>
|
||||
}
|
||||
|
||||
type GeneralChunkingOptionsProps = {
|
||||
|
|
@ -97,7 +96,7 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
|
|||
<OptionCard
|
||||
className="mb-2 bg-background-section"
|
||||
title={t('stepTwo.general', { ns: 'datasetCreation' })}
|
||||
icon={<Image width={20} height={20} src={SettingCog} alt={t('stepTwo.general', { ns: 'datasetCreation' })} />}
|
||||
icon={<img width={20} height={20} src={SettingCog.src} alt={t('stepTwo.general', { ns: 'datasetCreation' })} />}
|
||||
activeHeaderClassName="bg-dataset-option-card-blue-gradient"
|
||||
description={t('stepTwo.generalTip', { ns: 'datasetCreation' })}
|
||||
isActive={isActive}
|
||||
|
|
@ -148,7 +147,7 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
|
|||
onClick={() => onRuleToggle(rule.id)}
|
||||
>
|
||||
<Checkbox checked={rule.enabled} />
|
||||
<label className="system-sm-regular ml-2 cursor-pointer text-text-secondary">
|
||||
<label className="ml-2 cursor-pointer text-text-secondary system-sm-regular">
|
||||
{getRuleName(rule.id)}
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -183,7 +182,7 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
|
|||
checked={currentDocForm === ChunkingMode.qa}
|
||||
disabled={hasCurrentDatasetDocForm}
|
||||
/>
|
||||
<label className="system-sm-regular ml-2 cursor-pointer text-text-secondary">
|
||||
<label className="ml-2 cursor-pointer text-text-secondary system-sm-regular">
|
||||
{t('stepTwo.useQALanguage', { ns: 'datasetCreation' })}
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -202,7 +201,7 @@ export const GeneralChunkingOptions: FC<GeneralChunkingOptionsProps> = ({
|
|||
className="mt-2 flex h-10 items-center gap-2 rounded-xl border border-components-panel-border px-3 text-xs shadow-xs backdrop-blur-[5px]"
|
||||
>
|
||||
<RiAlertFill className="size-4 text-text-warning-secondary" />
|
||||
<span className="system-xs-medium text-text-primary">
|
||||
<span className="text-text-primary system-xs-medium">
|
||||
{t('stepTwo.QATip', { ns: 'datasetCreation' })}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import type { FC } from 'react'
|
||||
import type { DefaultModel, Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { RetrievalConfig } from '@/types/app'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
|
@ -70,7 +69,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
return (
|
||||
<>
|
||||
{/* Index Mode */}
|
||||
<div className="system-md-semibold mb-1 text-text-secondary">
|
||||
<div className="mb-1 text-text-secondary system-md-semibold">
|
||||
{t('stepTwo.indexMode', { ns: 'datasetCreation' })}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -98,7 +97,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
</div>
|
||||
)}
|
||||
description={t('stepTwo.qualifiedTip', { ns: 'datasetCreation' })}
|
||||
icon={<Image src={indexMethodIcon.high_quality} alt="" />}
|
||||
icon={<img src={indexMethodIcon.high_quality} alt="" />}
|
||||
isActive={!hasSetIndexType && indexType === IndexingType.QUALIFIED}
|
||||
disabled={hasSetIndexType}
|
||||
onSwitched={() => onIndexTypeChange(IndexingType.QUALIFIED)}
|
||||
|
|
@ -143,7 +142,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
className="h-full"
|
||||
title={t('stepTwo.economical', { ns: 'datasetCreation' })}
|
||||
description={t('stepTwo.economicalTip', { ns: 'datasetCreation' })}
|
||||
icon={<Image src={indexMethodIcon.economical} alt="" />}
|
||||
icon={<img src={indexMethodIcon.economical} alt="" />}
|
||||
isActive={!hasSetIndexType && indexType === IndexingType.ECONOMICAL}
|
||||
disabled={hasSetIndexType || docForm !== ChunkingMode.text}
|
||||
onSwitched={() => onIndexTypeChange(IndexingType.ECONOMICAL)}
|
||||
|
|
@ -160,7 +159,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
<div className="p-1">
|
||||
<AlertTriangle className="size-4 text-text-warning-secondary" />
|
||||
</div>
|
||||
<span className="system-xs-medium text-text-primary">
|
||||
<span className="text-text-primary system-xs-medium">
|
||||
{t('stepTwo.highQualityTip', { ns: 'datasetCreation' })}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -168,7 +167,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
|
||||
{/* Economical index setting tip */}
|
||||
{hasSetIndexType && indexType === IndexingType.ECONOMICAL && (
|
||||
<div className="system-xs-medium mt-2 text-text-tertiary">
|
||||
<div className="mt-2 text-text-tertiary system-xs-medium">
|
||||
{t('stepTwo.indexSettingTip', { ns: 'datasetCreation' })}
|
||||
<Link className="text-text-accent" href={`/datasets/${datasetId}/settings`}>
|
||||
{t('stepTwo.datasetSettingLink', { ns: 'datasetCreation' })}
|
||||
|
|
@ -179,7 +178,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
{/* Embedding model */}
|
||||
{indexType === IndexingType.QUALIFIED && (
|
||||
<div className="mt-5">
|
||||
<div className={cn('system-md-semibold mb-1 text-text-secondary', datasetId && 'flex items-center justify-between')}>
|
||||
<div className={cn('mb-1 text-text-secondary system-md-semibold', datasetId && 'flex items-center justify-between')}>
|
||||
{t('form.embeddingModel', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
<ModelSelector
|
||||
|
|
@ -190,7 +189,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
onSelect={onEmbeddingModelChange}
|
||||
/>
|
||||
{isModelAndRetrievalConfigDisabled && (
|
||||
<div className="system-xs-medium mt-2 text-text-tertiary">
|
||||
<div className="mt-2 text-text-tertiary system-xs-medium">
|
||||
{t('stepTwo.indexSettingTip', { ns: 'datasetCreation' })}
|
||||
<Link className="text-text-accent" href={`/datasets/${datasetId}/settings`}>
|
||||
{t('stepTwo.datasetSettingLink', { ns: 'datasetCreation' })}
|
||||
|
|
@ -207,10 +206,10 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
{!isModelAndRetrievalConfigDisabled
|
||||
? (
|
||||
<div className="mb-1">
|
||||
<div className="system-md-semibold mb-0.5 text-text-secondary">
|
||||
<div className="mb-0.5 text-text-secondary system-md-semibold">
|
||||
{t('form.retrievalSetting.title', { ns: 'datasetSettings' })}
|
||||
</div>
|
||||
<div className="body-xs-regular text-text-tertiary">
|
||||
<div className="text-text-tertiary body-xs-regular">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
|
@ -224,7 +223,7 @@ export const IndexingModeSection: FC<IndexingModeSectionProps> = ({
|
|||
</div>
|
||||
)
|
||||
: (
|
||||
<div className={cn('system-md-semibold mb-0.5 text-text-secondary', 'flex items-center justify-between')}>
|
||||
<div className={cn('mb-0.5 text-text-secondary system-md-semibold', 'flex items-center justify-between')}>
|
||||
<div>{t('form.retrievalSetting.title', { ns: 'datasetSettings' })}</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { ComponentProps, FC, ReactNode } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const TriangleArrow: FC<ComponentProps<'svg'>> = props => (
|
||||
|
|
@ -23,7 +22,7 @@ export const OptionCardHeader: FC<OptionCardHeaderProps> = (props) => {
|
|||
return (
|
||||
<div className={cn('relative flex h-full overflow-hidden rounded-t-xl', isActive && activeClassName, !disabled && 'cursor-pointer')}>
|
||||
<div className="relative flex size-14 items-center justify-center overflow-hidden">
|
||||
{isActive && effectImg && <Image src={effectImg} className="absolute left-0 top-0 h-full w-full" alt="" width={56} height={56} />}
|
||||
{isActive && effectImg && <img src={effectImg} className="absolute left-0 top-0 h-full w-full" alt="" width={56} height={56} />}
|
||||
<div className="p-1">
|
||||
<div className="flex size-8 justify-center rounded-lg border border-components-panel-border-subtle bg-background-default-dodge p-1.5 shadow-md">
|
||||
{icon}
|
||||
|
|
@ -34,8 +33,8 @@ export const OptionCardHeader: FC<OptionCardHeaderProps> = (props) => {
|
|||
className={cn('absolute -bottom-1.5 left-4 text-transparent', isActive && 'text-components-panel-bg')}
|
||||
/>
|
||||
<div className="flex-1 space-y-0.5 py-3 pr-4">
|
||||
<div className="system-md-semibold text-text-secondary">{title}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{description}</div>
|
||||
<div className="text-text-secondary system-md-semibold">{title}</div>
|
||||
<div className="text-text-tertiary system-xs-regular">{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import type { FC } from 'react'
|
|||
import type { ParentChildConfig } from '../hooks'
|
||||
import type { ParentMode, PreProcessingRule, SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
|
||||
import { RiSearchEyeLine } from '@remixicon/react'
|
||||
import Image from 'next/image'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
|
|
@ -26,7 +25,7 @@ type TextLabelProps = {
|
|||
}
|
||||
|
||||
const TextLabel: FC<TextLabelProps> = ({ children }) => {
|
||||
return <label className="system-sm-semibold text-text-secondary">{children}</label>
|
||||
return <label className="text-text-secondary system-sm-semibold">{children}</label>
|
||||
}
|
||||
|
||||
type ParentChildOptionsProps = {
|
||||
|
|
@ -118,7 +117,7 @@ export const ParentChildOptions: FC<ParentChildOptionsProps> = ({
|
|||
</div>
|
||||
<RadioCard
|
||||
className="mt-1"
|
||||
icon={<Image src={Note} alt="" />}
|
||||
icon={<img src={Note.src} alt="" />}
|
||||
title={t('stepTwo.paragraph', { ns: 'datasetCreation' })}
|
||||
description={t('stepTwo.paragraphTip', { ns: 'datasetCreation' })}
|
||||
isChosen={parentChildConfig.chunkForContext === 'paragraph'}
|
||||
|
|
@ -140,7 +139,7 @@ export const ParentChildOptions: FC<ParentChildOptionsProps> = ({
|
|||
/>
|
||||
<RadioCard
|
||||
className="mt-2"
|
||||
icon={<Image src={FileList} alt="" />}
|
||||
icon={<img src={FileList.src} alt="" />}
|
||||
title={t('stepTwo.fullDoc', { ns: 'datasetCreation' })}
|
||||
description={t('stepTwo.fullDocTip', { ns: 'datasetCreation' })}
|
||||
onChosen={() => onChunkForContextChange('full-doc')}
|
||||
|
|
@ -186,7 +185,7 @@ export const ParentChildOptions: FC<ParentChildOptionsProps> = ({
|
|||
onClick={() => onRuleToggle(rule.id)}
|
||||
>
|
||||
<Checkbox checked={rule.enabled} />
|
||||
<label className="system-sm-regular ml-2 cursor-pointer text-text-secondary">
|
||||
<label className="ml-2 cursor-pointer text-text-secondary system-sm-regular">
|
||||
{getRuleName(rule.id)}
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,14 +6,6 @@ import { ProcessMode } from '@/models/datasets'
|
|||
import { RETRIEVE_METHOD } from '@/types/app'
|
||||
import RuleDetail from '../rule-detail'
|
||||
|
||||
// Override global next/image auto-mock: tests assert on data-testid="next-image" and src attributes
|
||||
vi.mock('next/image', () => ({
|
||||
default: function MockImage({ src, alt, className }: { src: string, alt: string, className?: string }) {
|
||||
// eslint-disable-next-line next/no-img-element
|
||||
return <img src={src} alt={alt} className={className} data-testid="next-image" />
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock FieldInfo component
|
||||
vi.mock('@/app/components/datasets/documents/detail/metadata', () => ({
|
||||
FieldInfo: ({ label, displayedValue, valueIcon }: { label: string, displayedValue: string, valueIcon?: React.ReactNode }) => (
|
||||
|
|
@ -184,16 +176,16 @@ describe('RuleDetail', () => {
|
|||
})
|
||||
|
||||
it('should show high_quality icon for qualified indexing', () => {
|
||||
render(<RuleDetail indexingType={IndexingType.QUALIFIED} />)
|
||||
const { container } = render(<RuleDetail indexingType={IndexingType.QUALIFIED} />)
|
||||
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
const images = container.querySelectorAll('img')
|
||||
expect(images[0]).toHaveAttribute('src', '/icons/high_quality.svg')
|
||||
})
|
||||
|
||||
it('should show economical icon for economical indexing', () => {
|
||||
render(<RuleDetail indexingType={IndexingType.ECONOMICAL} />)
|
||||
const { container } = render(<RuleDetail indexingType={IndexingType.ECONOMICAL} />)
|
||||
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
const images = container.querySelectorAll('img')
|
||||
expect(images[0]).toHaveAttribute('src', '/icons/economical.svg')
|
||||
})
|
||||
})
|
||||
|
|
@ -256,38 +248,38 @@ describe('RuleDetail', () => {
|
|||
})
|
||||
|
||||
it('should show vector icon for semantic search', () => {
|
||||
render(
|
||||
const { container } = render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.semantic}
|
||||
/>,
|
||||
)
|
||||
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
const images = container.querySelectorAll('img')
|
||||
expect(images[1]).toHaveAttribute('src', '/icons/vector.svg')
|
||||
})
|
||||
|
||||
it('should show fullText icon for full text search', () => {
|
||||
render(
|
||||
const { container } = render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.fullText}
|
||||
/>,
|
||||
)
|
||||
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
const images = container.querySelectorAll('img')
|
||||
expect(images[1]).toHaveAttribute('src', '/icons/fullText.svg')
|
||||
})
|
||||
|
||||
it('should show hybrid icon for hybrid search', () => {
|
||||
render(
|
||||
const { container } = render(
|
||||
<RuleDetail
|
||||
indexingType={IndexingType.QUALIFIED}
|
||||
retrievalMethod={RETRIEVE_METHOD.hybrid}
|
||||
/>,
|
||||
)
|
||||
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
const images = container.querySelectorAll('img')
|
||||
expect(images[1]).toHaveAttribute('src', '/icons/hybrid.svg')
|
||||
})
|
||||
})
|
||||
|
|
@ -308,9 +300,9 @@ describe('RuleDetail', () => {
|
|||
})
|
||||
|
||||
it('should handle undefined retrievalMethod with defined indexingType', () => {
|
||||
render(<RuleDetail indexingType={IndexingType.QUALIFIED} />)
|
||||
const { container } = render(<RuleDetail indexingType={IndexingType.QUALIFIED} />)
|
||||
|
||||
const images = screen.getAllByTestId('next-image')
|
||||
const images = container.querySelectorAll('img')
|
||||
// When retrievalMethod is undefined, vector icon is used as default
|
||||
expect(images[1]).toHaveAttribute('src', '/icons/vector.svg')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { ProcessRuleResponse } from '@/models/datasets'
|
||||
import Image from 'next/image'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -50,7 +49,7 @@ const RuleDetail = ({
|
|||
label={t('stepTwo.indexMode', { ns: 'datasetCreation' })}
|
||||
displayedValue={t(`stepTwo.${indexingType === IndexingType.ECONOMICAL ? 'economical' : 'qualified'}`, { ns: 'datasetCreation' }) as string}
|
||||
valueIcon={(
|
||||
<Image
|
||||
<img
|
||||
className="size-4"
|
||||
src={
|
||||
indexingType === IndexingType.ECONOMICAL
|
||||
|
|
@ -65,7 +64,7 @@ const RuleDetail = ({
|
|||
label={t('form.retrievalSetting.title', { ns: 'datasetSettings' })}
|
||||
displayedValue={t(`retrieval.${indexingType === IndexingType.ECONOMICAL ? 'keyword_search' : retrievalMethod ?? 'semantic_search'}.title`, { ns: 'dataset' })}
|
||||
valueIcon={(
|
||||
<Image
|
||||
<img
|
||||
className="size-4"
|
||||
src={
|
||||
retrievalMethod === RETRIEVE_METHOD.fullText
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import type { FC } from 'react'
|
||||
import type { ProcessRuleResponse } from '@/models/datasets'
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import Image from 'next/image'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -101,7 +100,7 @@ const RuleDetail: FC<RuleDetailProps> = React.memo(({
|
|||
label={t('stepTwo.indexMode', { ns: 'datasetCreation' })}
|
||||
displayedValue={t(`stepTwo.${isEconomical ? 'economical' : 'qualified'}`, { ns: 'datasetCreation' }) as string}
|
||||
valueIcon={(
|
||||
<Image
|
||||
<img
|
||||
className="size-4"
|
||||
src={isEconomical ? indexMethodIcon.economical : indexMethodIcon.high_quality}
|
||||
alt=""
|
||||
|
|
@ -112,7 +111,7 @@ const RuleDetail: FC<RuleDetailProps> = React.memo(({
|
|||
label={t('form.retrievalSetting.title', { ns: 'datasetSettings' })}
|
||||
displayedValue={t(`retrieval.${isEconomical ? 'keyword_search' : retrievalMethod ?? 'semantic_search'}.title`, { ns: 'dataset' })}
|
||||
valueIcon={(
|
||||
<Image
|
||||
<img
|
||||
className="size-4"
|
||||
src={getRetrievalIcon(retrievalMethod)}
|
||||
alt=""
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import {
|
|||
RiEqualizer2Line,
|
||||
RiPlayCircleLine,
|
||||
} from '@remixicon/react'
|
||||
import Image from 'next/image'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -178,7 +177,7 @@ const QueryInput = ({
|
|||
}, [text, externalRetrievalSettings, externalKnowledgeBaseHitTestingMutation, onUpdateList, setExternalHitResult])
|
||||
|
||||
const retrievalMethod = isEconomy ? RETRIEVE_METHOD.keywordSearch : retrievalConfig.search_method
|
||||
const icon = <Image className="size-3.5 text-util-colors-purple-purple-600" src={getIcon(retrievalMethod)} alt="" />
|
||||
const icon = <img className="size-3.5 text-util-colors-purple-purple-600" src={getIcon(retrievalMethod)} alt="" />
|
||||
const TextAreaComp = useMemo(() => {
|
||||
return (
|
||||
<Textarea
|
||||
|
|
@ -206,7 +205,7 @@ const QueryInput = ({
|
|||
<div className={cn('relative flex h-80 shrink-0 flex-col overflow-hidden rounded-xl bg-gradient-to-r from-components-input-border-active-prompt-1 to-components-input-border-active-prompt-2 p-0.5 shadow-xs')}>
|
||||
<div className="flex h-full flex-col overflow-hidden rounded-[10px] bg-background-section-burn">
|
||||
<div className="relative flex shrink-0 items-center justify-between p-1.5 pb-1 pl-3">
|
||||
<span className="system-sm-semibold-uppercase text-text-secondary">
|
||||
<span className="text-text-secondary system-sm-semibold-uppercase">
|
||||
{t('input.title', { ns: 'datasetHitTesting' })}
|
||||
</span>
|
||||
{isExternal
|
||||
|
|
@ -218,7 +217,7 @@ const QueryInput = ({
|
|||
>
|
||||
<RiEqualizer2Line className="h-3.5 w-3.5 text-components-button-secondary-text" />
|
||||
<div className="flex items-center justify-center gap-1 px-[3px]">
|
||||
<span className="system-xs-medium text-components-button-secondary-text">{t('settingTitle', { ns: 'datasetHitTesting' })}</span>
|
||||
<span className="text-components-button-secondary-text system-xs-medium">{t('settingTitle', { ns: 'datasetHitTesting' })}</span>
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import type { ImgHTMLAttributes } from 'react'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
|
|
@ -11,21 +10,6 @@ vi.mock('../use-get-requirements', () => ({
|
|||
default: (...args: unknown[]) => mockUseGetRequirements(...args),
|
||||
}))
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
default: ({
|
||||
src,
|
||||
alt,
|
||||
unoptimized: _unoptimized,
|
||||
...rest
|
||||
}: {
|
||||
src: string
|
||||
alt: string
|
||||
unoptimized?: boolean
|
||||
} & ImgHTMLAttributes<HTMLImageElement>) => (
|
||||
React.createElement('img', { src, alt, ...rest })
|
||||
),
|
||||
}))
|
||||
|
||||
const createMockAppDetail = (mode: string, overrides: Partial<TryAppInfo> = {}): TryAppInfo => ({
|
||||
id: 'test-app-id',
|
||||
name: 'Test App Name',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { TryAppInfo } from '@/service/try-app'
|
||||
import Image from 'next/image'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AppTypeIcon } from '@/app/components/app/type-selector'
|
||||
|
|
@ -38,14 +37,13 @@ const RequirementIcon: FC<RequirementIconProps> = ({ iconUrl }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Image
|
||||
<img
|
||||
className="size-5 rounded-md object-cover shadow-xs"
|
||||
src={iconUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
width={requirementIconSize}
|
||||
height={requirementIconSize}
|
||||
unoptimized
|
||||
onError={() => setFailedSource(iconUrl)}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { Form, ValidateValue } from '../key-validator/declarations'
|
||||
import type { PluginProvider } from '@/models/common'
|
||||
import Image from 'next/image'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useToastContext } from '@/app/components/base/toast/context'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
|
|
@ -64,7 +63,7 @@ const SerpapiPlugin = ({
|
|||
return (
|
||||
<KeyValidator
|
||||
type="serpapi"
|
||||
title={<Image alt="serpapi logo" src={SerpapiLogo} width={64} />}
|
||||
title={<img alt="serpapi logo" src={SerpapiLogo.src} width={64} />}
|
||||
status={plugin.credentials?.api_key ? 'success' : 'add'}
|
||||
forms={forms}
|
||||
keyFrom={{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import Image from 'next/image'
|
||||
import * as React from 'react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
|
@ -11,7 +10,7 @@ const PipelineScreenShot = () => {
|
|||
<source media="(resolution: 1x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline.png`} />
|
||||
<source media="(resolution: 2x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline@2x.png`} />
|
||||
<source media="(resolution: 3x)" srcSet={`${basePath}/screenshots/${theme}/Pipeline@3x.png`} />
|
||||
<Image
|
||||
<img
|
||||
src={`${basePath}/screenshots/${theme}/Pipeline.png`}
|
||||
alt="Pipeline Screenshot"
|
||||
width={692}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ pnpm test path/to/file.spec.tsx
|
|||
## Project Test Setup
|
||||
|
||||
- **Configuration**: `vitest.config.ts` sets the `jsdom` environment, loads the Testing Library presets, and respects our path aliases (`@/...`). Check this file before adding new transformers or module name mappers.
|
||||
- **Global setup**: `vitest.setup.ts` already imports `@testing-library/jest-dom`, runs `cleanup()` after every test, and defines shared mocks (for example `react-i18next`, `next/image`). Add any environment-level mocks (for example `ResizeObserver`, `matchMedia`, `IntersectionObserver`, `TextEncoder`, `crypto`) here so they are shared consistently.
|
||||
- **Global setup**: `vitest.setup.ts` already imports `@testing-library/jest-dom`, runs `cleanup()` after every test, and defines shared mocks (for example `react-i18next`). Add any environment-level mocks (for example `ResizeObserver`, `matchMedia`, `IntersectionObserver`, `TextEncoder`, `crypto`) here so they are shared consistently.
|
||||
- **Reusable mocks**: Place shared mock factories inside `web/__mocks__/` and use `vi.mock('module-name')` to point to them rather than redefining mocks in every spec.
|
||||
- **Mocking behavior**: Modules are not mocked automatically. Use `vi.mock(...)` in tests, or place global mocks in `vitest.setup.ts`.
|
||||
- **Script utilities**: `web/scripts/analyze-component.js` analyzes component complexity and generates test prompts for AI assistants. Commands:
|
||||
|
|
|
|||
|
|
@ -1119,9 +1119,6 @@
|
|||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 11
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
|
|
@ -2993,9 +2990,6 @@
|
|||
"app/components/datasets/common/retrieval-param-config/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/dsl-confirm-modal.tsx": {
|
||||
|
|
@ -3139,17 +3133,11 @@
|
|||
"app/components/datasets/create/step-two/components/general-chunking-options.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 4
|
||||
}
|
||||
},
|
||||
"app/components/datasets/create/step-two/components/indexing-mode-section.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 2
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 8
|
||||
}
|
||||
},
|
||||
"app/components/datasets/create/step-two/components/inputs.tsx": {
|
||||
|
|
@ -3160,16 +3148,6 @@
|
|||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/datasets/create/step-two/components/option-card.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/datasets/create/step-two/components/parent-child-options.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/datasets/create/step-two/hooks/use-indexing-config.ts": {
|
||||
"react-hooks-extra/no-direct-set-state-in-use-effect": {
|
||||
"count": 3
|
||||
|
|
@ -3884,11 +3862,6 @@
|
|||
"count": 3
|
||||
}
|
||||
},
|
||||
"app/components/datasets/hit-testing/components/query-input/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"app/components/datasets/hit-testing/components/query-input/textarea.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
|
|
|||
|
|
@ -14,6 +14,79 @@ process.env.TAILWIND_MODE ??= 'ESLINT'
|
|||
|
||||
const disableRuleAutoFix = !(isInEditorEnv() || isInGitHooksOrLintStaged())
|
||||
|
||||
const NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS = [
|
||||
{
|
||||
group: ['next/image'],
|
||||
message: 'Do not import next/image. Use native img tags instead.',
|
||||
},
|
||||
{
|
||||
group: ['next/font', 'next/font/*'],
|
||||
message: 'Do not import next/font. Use the project font styles instead.',
|
||||
},
|
||||
]
|
||||
|
||||
const OVERLAY_RESTRICTED_IMPORT_PATTERNS = [
|
||||
{
|
||||
group: [
|
||||
'**/portal-to-follow-elem',
|
||||
'**/portal-to-follow-elem/index',
|
||||
],
|
||||
message: 'Deprecated: use semantic overlay primitives from @/app/components/base/ui/ instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'**/base/tooltip',
|
||||
'**/base/tooltip/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/tooltip instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'**/base/modal',
|
||||
'**/base/modal/index',
|
||||
'**/base/modal/modal',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/dialog instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'**/base/select',
|
||||
'**/base/select/index',
|
||||
'**/base/select/custom',
|
||||
'**/base/select/pure',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/select instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'**/base/confirm',
|
||||
'**/base/confirm/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/alert-dialog instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'**/base/popover',
|
||||
'**/base/popover/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/popover instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'**/base/dropdown',
|
||||
'**/base/dropdown/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/dropdown-menu instead. See issue #32767.',
|
||||
},
|
||||
{
|
||||
group: [
|
||||
'**/base/dialog',
|
||||
'**/base/dialog/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/dialog instead. See issue #32767.',
|
||||
},
|
||||
]
|
||||
|
||||
export default antfu(
|
||||
{
|
||||
react: {
|
||||
|
|
@ -53,6 +126,7 @@ export default antfu(
|
|||
{
|
||||
rules: {
|
||||
'node/prefer-global/process': 'off',
|
||||
'next/no-img-element': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -156,6 +230,15 @@ export default antfu(
|
|||
'react-refresh/only-export-components': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dify/no-next-image-or-font',
|
||||
files: [GLOB_TS, GLOB_TSX],
|
||||
rules: {
|
||||
'no-restricted-imports': ['error', {
|
||||
patterns: NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS,
|
||||
}],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'dify/overlay-migration',
|
||||
files: [GLOB_TS, GLOB_TSX],
|
||||
|
|
@ -165,58 +248,10 @@ export default antfu(
|
|||
],
|
||||
rules: {
|
||||
'no-restricted-imports': ['error', {
|
||||
patterns: [{
|
||||
group: [
|
||||
'**/portal-to-follow-elem',
|
||||
'**/portal-to-follow-elem/index',
|
||||
patterns: [
|
||||
...NEXT_PLATFORM_RESTRICTED_IMPORT_PATTERNS,
|
||||
...OVERLAY_RESTRICTED_IMPORT_PATTERNS,
|
||||
],
|
||||
message: 'Deprecated: use semantic overlay primitives from @/app/components/base/ui/ instead. See issue #32767.',
|
||||
}, {
|
||||
group: [
|
||||
'**/base/tooltip',
|
||||
'**/base/tooltip/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/tooltip instead. See issue #32767.',
|
||||
}, {
|
||||
group: [
|
||||
'**/base/modal',
|
||||
'**/base/modal/index',
|
||||
'**/base/modal/modal',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/dialog instead. See issue #32767.',
|
||||
}, {
|
||||
group: [
|
||||
'**/base/select',
|
||||
'**/base/select/index',
|
||||
'**/base/select/custom',
|
||||
'**/base/select/pure',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/select instead. See issue #32767.',
|
||||
}, {
|
||||
group: [
|
||||
'**/base/confirm',
|
||||
'**/base/confirm/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/alert-dialog instead. See issue #32767.',
|
||||
}, {
|
||||
group: [
|
||||
'**/base/popover',
|
||||
'**/base/popover/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/popover instead. See issue #32767.',
|
||||
}, {
|
||||
group: [
|
||||
'**/base/dropdown',
|
||||
'**/base/dropdown/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/dropdown-menu instead. See issue #32767.',
|
||||
}, {
|
||||
group: [
|
||||
'**/base/dialog',
|
||||
'**/base/dialog/index',
|
||||
],
|
||||
message: 'Deprecated: use @/app/components/base/ui/dialog instead. See issue #32767.',
|
||||
}],
|
||||
}],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,12 +6,6 @@ import { env } from './env'
|
|||
const isDev = process.env.NODE_ENV === 'development'
|
||||
const withMDX = createMDX()
|
||||
|
||||
// the default url to prevent parse url error when running jest
|
||||
const hasSetWebPrefix = env.NEXT_PUBLIC_WEB_PREFIX
|
||||
const port = env.PORT
|
||||
const locImageURLs = !hasSetWebPrefix ? [new URL(`http://localhost:${port}/**`), new URL(`http://127.0.0.1:${port}/**`)] : []
|
||||
const remoteImageURLs = ([hasSetWebPrefix ? new URL(`${env.NEXT_PUBLIC_WEB_PREFIX}/**`) : '', ...locImageURLs].filter(item => !!item)) as URL[]
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
basePath: env.NEXT_PUBLIC_BASE_PATH,
|
||||
transpilePackages: ['@t3-oss/env-core', '@t3-oss/env-nextjs', 'echarts', 'zrender'],
|
||||
|
|
@ -23,16 +17,6 @@ const nextConfig: NextConfig = {
|
|||
productionBrowserSourceMaps: false, // enable browser source map generation during the production build
|
||||
// Configure pageExtensions to include md and mdx
|
||||
pageExtensions: ['ts', 'tsx', 'js', 'jsx', 'md', 'mdx'],
|
||||
// https://nextjs.org/docs/messages/next-image-unconfigured-host
|
||||
images: {
|
||||
remotePatterns: remoteImageURLs.map(remoteImageURL => ({
|
||||
protocol: remoteImageURL.protocol.replace(':', '') as 'http' | 'https',
|
||||
hostname: remoteImageURL.hostname,
|
||||
port: remoteImageURL.port,
|
||||
pathname: remoteImageURL.pathname,
|
||||
search: '',
|
||||
})),
|
||||
},
|
||||
typescript: {
|
||||
// https://nextjs.org/docs/api-reference/next.config.js/ignoring-typescript-errors
|
||||
ignoreBuildErrors: true,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
import type { Plugin } from 'vite'
|
||||
import path from 'node:path'
|
||||
import { normalizeViteModuleId } from './utils'
|
||||
|
||||
type NextStaticImageTestPluginOptions = {
|
||||
projectRoot: string
|
||||
}
|
||||
|
||||
const STATIC_ASSET_RE = /\.(?:svg|png|jpe?g|gif)$/i
|
||||
const EXCLUDED_QUERY_RE = /[?&](?:raw|url)\b/
|
||||
|
||||
export const nextStaticImageTestPlugin = ({ projectRoot }: NextStaticImageTestPluginOptions): Plugin => {
|
||||
return {
|
||||
name: 'next-static-image-test',
|
||||
enforce: 'pre',
|
||||
load(id) {
|
||||
if (EXCLUDED_QUERY_RE.test(id))
|
||||
return null
|
||||
|
||||
const cleanId = normalizeViteModuleId(id)
|
||||
if (!cleanId.startsWith(projectRoot) || !STATIC_ASSET_RE.test(cleanId))
|
||||
return null
|
||||
|
||||
const relativePath = path.relative(projectRoot, cleanId).split(path.sep).join('/')
|
||||
const src = `/__static__/${relativePath}`
|
||||
|
||||
return `export default { src: ${JSON.stringify(src)} }\n`
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -72,12 +72,10 @@ export const config = {
|
|||
* Match all request paths except for the ones starting with:
|
||||
* - api (API routes)
|
||||
* - _next/static (static files)
|
||||
* - _next/image (image optimization files)
|
||||
* - favicon.ico (favicon file)
|
||||
*/
|
||||
{
|
||||
// source: '/((?!api|_next/static|_next/image|favicon.ico).*)',
|
||||
source: '/((?!_next/static|_next/image|favicon.ico).*)',
|
||||
source: '/((?!_next/static|favicon.ico).*)',
|
||||
// source: '/(.*)',
|
||||
// missing: [
|
||||
// { type: 'header', key: 'next-router-prefetch' },
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import Inspect from 'vite-plugin-inspect'
|
|||
import { defineConfig } from 'vite-plus'
|
||||
import { createCodeInspectorPlugin, createForceInspectorClientInjectionPlugin } from './plugins/vite/code-inspector'
|
||||
import { customI18nHmrPlugin } from './plugins/vite/custom-i18n-hmr'
|
||||
import { nextStaticImageTestPlugin } from './plugins/vite/next-static-image-test'
|
||||
import { collectComponentCoverageExcludedFiles } from './scripts/component-coverage-filters.mjs'
|
||||
import { EXCLUDED_COMPONENT_MODULES } from './scripts/components-coverage-thresholds.mjs'
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ export default defineConfig(({ mode }) => {
|
|||
return {
|
||||
plugins: isTest
|
||||
? [
|
||||
nextStaticImageTestPlugin({ projectRoot }),
|
||||
react(),
|
||||
{
|
||||
// Stub .mdx files so components importing them can be unit-tested
|
||||
|
|
|
|||
|
|
@ -100,9 +100,6 @@ afterEach(async () => {
|
|||
})
|
||||
})
|
||||
|
||||
// mock next/image to avoid width/height requirements for data URLs
|
||||
vi.mock('next/image')
|
||||
|
||||
// mock foxact/use-clipboard - not available in test environment
|
||||
vi.mock('foxact/use-clipboard', () => ({
|
||||
useClipboard: () => ({
|
||||
|
|
|
|||
Loading…
Reference in New Issue