From 7c12e923b6cd5e7e082241e2add8266b426b56d4 Mon Sep 17 00:00:00 2001 From: zyssyz123 <916125788@qq.com> Date: Mon, 26 Jan 2026 11:52:05 +0800 Subject: [PATCH 01/21] feat: add trial model list in system features (#31313) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: hj24 --- api/enums/hosted_provider.py | 21 +++++++++++++++++++++ api/services/feature_service.py | 14 ++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 api/enums/hosted_provider.py diff --git a/api/enums/hosted_provider.py b/api/enums/hosted_provider.py new file mode 100644 index 0000000000..c6d3715dc1 --- /dev/null +++ b/api/enums/hosted_provider.py @@ -0,0 +1,21 @@ +from enum import StrEnum + + +class HostedTrialProvider(StrEnum): + """ + Enum representing hosted model provider names for trial access. + """ + + OPENAI = "langgenius/openai/openai" + ANTHROPIC = "langgenius/anthropic/anthropic" + GEMINI = "langgenius/gemini/google" + X = "langgenius/x/x" + DEEPSEEK = "langgenius/deepseek/deepseek" + TONGYI = "langgenius/tongyi/tongyi" + + @property + def config_key(self) -> str: + """Return the config key used in dify_config (e.g., HOSTED_{config_key}_PAID_ENABLED).""" + if self == HostedTrialProvider.X: + return "XAI" + return self.name diff --git a/api/services/feature_service.py b/api/services/feature_service.py index b2fb3784e8..d94ae49d91 100644 --- a/api/services/feature_service.py +++ b/api/services/feature_service.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, ConfigDict, Field from configs import dify_config from enums.cloud_plan import CloudPlan +from enums.hosted_provider import HostedTrialProvider from services.billing_service import BillingService from services.enterprise.enterprise_service import EnterpriseService @@ -170,6 +171,7 @@ class SystemFeatureModel(BaseModel): plugin_installation_permission: PluginInstallationPermissionModel = PluginInstallationPermissionModel() enable_change_email: bool = True plugin_manager: PluginManagerModel = PluginManagerModel() + trial_models: list[str] = [] enable_trial_app: bool = False enable_explore_banner: bool = False @@ -227,9 +229,21 @@ class FeatureService: system_features.is_allow_register = dify_config.ALLOW_REGISTER system_features.is_allow_create_workspace = dify_config.ALLOW_CREATE_WORKSPACE system_features.is_email_setup = dify_config.MAIL_TYPE is not None and dify_config.MAIL_TYPE != "" + system_features.trial_models = cls._fulfill_trial_models_from_env() system_features.enable_trial_app = dify_config.ENABLE_TRIAL_APP system_features.enable_explore_banner = dify_config.ENABLE_EXPLORE_BANNER + @classmethod + def _fulfill_trial_models_from_env(cls) -> list[str]: + return [ + provider.value + for provider in HostedTrialProvider + if ( + getattr(dify_config, f"HOSTED_{provider.config_key}_PAID_ENABLED", False) + and getattr(dify_config, f"HOSTED_{provider.config_key}_TRIAL_ENABLED", False) + ) + ] + @classmethod def _fulfill_params_from_env(cls, features: FeatureModel): features.can_replace_logo = dify_config.CAN_REPLACE_LOGO From a43d2ec4f022d4446732527ac6565ffe691f493f Mon Sep 17 00:00:00 2001 From: Coding On Star <447357187@qq.com> Date: Mon, 26 Jan 2026 14:03:51 +0800 Subject: [PATCH 02/21] refactor: restructure Completed component (#31435) Co-authored-by: CodingOnStar --- .../app/overview/settings/index.spec.tsx | 3 + .../create/website/watercrawl/index.spec.tsx | 3 + .../completed/child-segment-list.spec.tsx | 499 +++++ .../detail/completed/child-segment-list.tsx | 229 +- .../detail/completed/common/drawer.tsx | 93 +- .../detail/completed/common/empty.spec.tsx | 129 ++ .../completed/components/drawer-group.tsx | 151 ++ .../detail/completed/components/index.ts | 3 + .../detail/completed/components/menu-bar.tsx | 76 + .../components/segment-list-content.tsx | 127 ++ .../documents/detail/completed/hooks/index.ts | 14 + .../hooks/use-child-segment-data.spec.ts | 568 +++++ .../completed/hooks/use-child-segment-data.ts | 241 ++ .../detail/completed/hooks/use-modal-state.ts | 141 ++ .../completed/hooks/use-search-filter.ts | 85 + .../hooks/use-segment-list-data.spec.ts | 942 ++++++++ .../completed/hooks/use-segment-list-data.ts | 363 +++ .../completed/hooks/use-segment-selection.ts | 58 + .../documents/detail/completed/index.spec.tsx | 1863 ++++++++++++++++ .../documents/detail/completed/index.tsx | 871 ++------ .../detail/completed/segment-list-context.ts | 34 + .../skeleton/full-doc-list-skeleton.spec.tsx | 93 + web/app/components/tools/provider/detail.tsx | 3 +- .../workflow-tool/configure-button.spec.tsx | 1975 +++++++++++++++++ .../components/tools/workflow-tool/index.tsx | 26 +- web/eslint-suppressions.json | 13 - 26 files changed, 7751 insertions(+), 852 deletions(-) create mode 100644 web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/common/empty.spec.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/components/drawer-group.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/components/index.ts create mode 100644 web/app/components/datasets/documents/detail/completed/components/menu-bar.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/components/segment-list-content.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/index.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.spec.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-child-segment-data.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-modal-state.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-search-filter.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.spec.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-segment-list-data.ts create mode 100644 web/app/components/datasets/documents/detail/completed/hooks/use-segment-selection.ts create mode 100644 web/app/components/datasets/documents/detail/completed/index.spec.tsx create mode 100644 web/app/components/datasets/documents/detail/completed/segment-list-context.ts create mode 100644 web/app/components/datasets/documents/detail/completed/skeleton/full-doc-list-skeleton.spec.tsx create mode 100644 web/app/components/tools/workflow-tool/configure-button.spec.tsx diff --git a/web/app/components/app/overview/settings/index.spec.tsx b/web/app/components/app/overview/settings/index.spec.tsx index 776c55d149..c9cbe0b724 100644 --- a/web/app/components/app/overview/settings/index.spec.tsx +++ b/web/app/components/app/overview/settings/index.spec.tsx @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import type { ReactNode } from 'react' import type { ModalContextState } from '@/context/modal-context' import type { ProviderContextState } from '@/context/provider-context' diff --git a/web/app/components/datasets/create/website/watercrawl/index.spec.tsx b/web/app/components/datasets/create/website/watercrawl/index.spec.tsx index 4bb8267cea..646c59eb75 100644 --- a/web/app/components/datasets/create/website/watercrawl/index.spec.tsx +++ b/web/app/components/datasets/create/website/watercrawl/index.spec.tsx @@ -1,3 +1,6 @@ +/** + * @vitest-environment jsdom + */ import type { Mock } from 'vitest' import type { CrawlOptions, CrawlResultItem } from '@/models/datasets' import { fireEvent, render, screen, waitFor } from '@testing-library/react' diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx new file mode 100644 index 0000000000..aa3e300322 --- /dev/null +++ b/web/app/components/datasets/documents/detail/completed/child-segment-list.spec.tsx @@ -0,0 +1,499 @@ +import type { DocumentContextValue } from '@/app/components/datasets/documents/detail/context' +import type { ChildChunkDetail, ChunkingMode, ParentMode } from '@/models/datasets' +import { fireEvent, render, screen } from '@testing-library/react' +import * as React from 'react' +import ChildSegmentList from './child-segment-list' + +// ============================================================================ +// Hoisted Mocks +// ============================================================================ + +const { + mockParentMode, + mockCurrChildChunk, +} = vi.hoisted(() => ({ + mockParentMode: { current: 'paragraph' as ParentMode }, + mockCurrChildChunk: { current: { childChunkInfo: undefined, showModal: false } as { childChunkInfo?: ChildChunkDetail, showModal: boolean } }, +})) + +// Mock react-i18next +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, options?: { count?: number, ns?: string }) => { + if (key === 'segment.childChunks') + return options?.count === 1 ? 'child chunk' : 'child chunks' + if (key === 'segment.searchResults') + return 'search results' + if (key === 'segment.edited') + return 'edited' + if (key === 'operation.add') + return 'Add' + const prefix = options?.ns ? `${options.ns}.` : '' + return `${prefix}${key}` + }, + }), +})) + +// Mock document context +vi.mock('../context', () => ({ + useDocumentContext: (selector: (value: DocumentContextValue) => unknown) => { + const value: DocumentContextValue = { + datasetId: 'test-dataset-id', + documentId: 'test-document-id', + docForm: 'text' as ChunkingMode, + parentMode: mockParentMode.current, + } + return selector(value) + }, +})) + +// Mock segment list context +vi.mock('./index', () => ({ + useSegmentListContext: (selector: (value: { currChildChunk: { childChunkInfo?: ChildChunkDetail, showModal: boolean } }) => unknown) => { + return selector({ currChildChunk: mockCurrChildChunk.current }) + }, +})) + +// Mock skeleton component +vi.mock('./skeleton/full-doc-list-skeleton', () => ({ + default: () =>
Loading...
, +})) + +// Mock Empty component +vi.mock('./common/empty', () => ({ + default: ({ onClearFilter }: { onClearFilter: () => void }) => ( +
+ +
+ ), +})) + +// Mock FormattedText and EditSlice +vi.mock('../../../formatted-text/formatted', () => ({ + FormattedText: ({ children, className }: { children: React.ReactNode, className?: string }) => ( +
{children}
+ ), +})) + +vi.mock('../../../formatted-text/flavours/edit-slice', () => ({ + EditSlice: ({ label, text, onDelete, onClick, labelClassName, contentClassName }: { + label: string + text: string + onDelete: () => void + onClick: (e: React.MouseEvent) => void + labelClassName?: string + contentClassName?: string + }) => ( +
+ {label} + {text} + +
+ ), +})) + +// ============================================================================ +// Test Data Factories +// ============================================================================ + +const createMockChildChunk = (overrides: Partial = {}): ChildChunkDetail => ({ + id: `child-${Math.random().toString(36).substr(2, 9)}`, + position: 1, + segment_id: 'segment-1', + content: 'Child chunk content', + word_count: 100, + created_at: 1700000000, + updated_at: 1700000000, + type: 'automatic', + ...overrides, +}) + +// ============================================================================ +// Tests +// ============================================================================ + +describe('ChildSegmentList', () => { + const defaultProps = { + childChunks: [] as ChildChunkDetail[], + parentChunkId: 'parent-1', + enabled: true, + } + + beforeEach(() => { + vi.clearAllMocks() + mockParentMode.current = 'paragraph' + mockCurrChildChunk.current = { childChunkInfo: undefined, showModal: false } + }) + + describe('Rendering', () => { + it('should render with empty child chunks', () => { + render() + + expect(screen.getByText(/child chunks/i)).toBeInTheDocument() + }) + + it('should render child chunks when provided', () => { + const childChunks = [ + createMockChildChunk({ id: 'child-1', position: 1, content: 'First chunk' }), + createMockChildChunk({ id: 'child-2', position: 2, content: 'Second chunk' }), + ] + + render() + + // In paragraph mode, content is collapsed by default + expect(screen.getByText(/2 child chunks/i)).toBeInTheDocument() + }) + + it('should render total count correctly with total prop in full-doc mode', () => { + mockParentMode.current = 'full-doc' + const childChunks = [createMockChildChunk()] + + // Pass inputValue="" to ensure isSearching is false + render() + + expect(screen.getByText(/5 child chunks/i)).toBeInTheDocument() + }) + + it('should render loading skeleton in full-doc mode when loading', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.getByTestId('full-doc-list-skeleton')).toBeInTheDocument() + }) + + it('should not render loading skeleton when not loading', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.queryByTestId('full-doc-list-skeleton')).not.toBeInTheDocument() + }) + }) + + describe('Paragraph Mode', () => { + beforeEach(() => { + mockParentMode.current = 'paragraph' + }) + + it('should show collapse icon in paragraph mode', () => { + const childChunks = [createMockChildChunk()] + + render() + + // Check for collapse/expand behavior + const totalRow = screen.getByText(/1 child chunk/i).closest('div') + expect(totalRow).toBeInTheDocument() + }) + + it('should toggle collapsed state when clicked', () => { + const childChunks = [createMockChildChunk({ content: 'Test content' })] + + render() + + // Initially collapsed in paragraph mode - content should not be visible + expect(screen.queryByTestId('formatted-text')).not.toBeInTheDocument() + + // Find and click the toggle area + const toggleArea = screen.getByText(/1 child chunk/i).closest('div') + + // Click to expand + if (toggleArea) + fireEvent.click(toggleArea) + + // After expansion, content should be visible + expect(screen.getByTestId('formatted-text')).toBeInTheDocument() + }) + + it('should apply opacity when disabled', () => { + const { container } = render() + + const wrapper = container.firstChild + expect(wrapper).toHaveClass('opacity-50') + }) + + it('should not apply opacity when enabled', () => { + const { container } = render() + + const wrapper = container.firstChild + expect(wrapper).not.toHaveClass('opacity-50') + }) + }) + + describe('Full-Doc Mode', () => { + beforeEach(() => { + mockParentMode.current = 'full-doc' + }) + + it('should show content by default in full-doc mode', () => { + const childChunks = [createMockChildChunk({ content: 'Full doc content' })] + + render() + + expect(screen.getByTestId('formatted-text')).toBeInTheDocument() + }) + + it('should render search input in full-doc mode', () => { + render() + + const input = document.querySelector('input') + expect(input).toBeInTheDocument() + }) + + it('should call handleInputChange when input changes', () => { + const handleInputChange = vi.fn() + + render() + + const input = document.querySelector('input') + if (input) { + fireEvent.change(input, { target: { value: 'test search' } }) + expect(handleInputChange).toHaveBeenCalledWith('test search') + } + }) + + it('should show search results text when searching', () => { + render() + + expect(screen.getByText(/3 search results/i)).toBeInTheDocument() + }) + + it('should show empty component when no results and searching', () => { + render( + , + ) + + expect(screen.getByTestId('empty-component')).toBeInTheDocument() + }) + + it('should call onClearFilter when clear button clicked in empty state', () => { + const onClearFilter = vi.fn() + + render( + , + ) + + const clearButton = screen.getByText('Clear Filter') + fireEvent.click(clearButton) + + expect(onClearFilter).toHaveBeenCalled() + }) + }) + + describe('Child Chunk Items', () => { + it('should render edited label when chunk is edited', () => { + mockParentMode.current = 'full-doc' + const editedChunk = createMockChildChunk({ + id: 'edited-chunk', + position: 1, + created_at: 1700000000, + updated_at: 1700000001, // Different from created_at + }) + + render() + + expect(screen.getByText(/C-1 · edited/i)).toBeInTheDocument() + }) + + it('should not show edited label when chunk is not edited', () => { + mockParentMode.current = 'full-doc' + const normalChunk = createMockChildChunk({ + id: 'normal-chunk', + position: 2, + created_at: 1700000000, + updated_at: 1700000000, // Same as created_at + }) + + render() + + expect(screen.getByText('C-2')).toBeInTheDocument() + expect(screen.queryByText(/edited/i)).not.toBeInTheDocument() + }) + + it('should call onClickSlice when chunk is clicked', () => { + mockParentMode.current = 'full-doc' + const onClickSlice = vi.fn() + const chunk = createMockChildChunk({ id: 'clickable-chunk' }) + + render( + , + ) + + const editSlice = screen.getByTestId('edit-slice') + fireEvent.click(editSlice) + + expect(onClickSlice).toHaveBeenCalledWith(chunk) + }) + + it('should call onDelete when delete button is clicked', () => { + mockParentMode.current = 'full-doc' + const onDelete = vi.fn() + const chunk = createMockChildChunk({ id: 'deletable-chunk', segment_id: 'seg-1' }) + + render( + , + ) + + const deleteButton = screen.getByTestId('delete-button') + fireEvent.click(deleteButton) + + expect(onDelete).toHaveBeenCalledWith('seg-1', 'deletable-chunk') + }) + + it('should apply focused styles when chunk is currently selected', () => { + mockParentMode.current = 'full-doc' + const chunk = createMockChildChunk({ id: 'focused-chunk' }) + mockCurrChildChunk.current = { childChunkInfo: chunk, showModal: true } + + render() + + const label = screen.getByTestId('edit-slice-label') + expect(label).toHaveClass('bg-state-accent-solid') + }) + }) + + describe('Add Button', () => { + it('should call handleAddNewChildChunk when Add button is clicked', () => { + const handleAddNewChildChunk = vi.fn() + + render( + , + ) + + const addButton = screen.getByText('Add') + fireEvent.click(addButton) + + expect(handleAddNewChildChunk).toHaveBeenCalledWith('parent-123') + }) + + it('should disable Add button when loading in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + const addButton = screen.getByText('Add') + expect(addButton).toBeDisabled() + }) + + it('should stop propagation when Add button is clicked', () => { + const handleAddNewChildChunk = vi.fn() + const parentClickHandler = vi.fn() + + render( +
+ +
, + ) + + const addButton = screen.getByText('Add') + fireEvent.click(addButton) + + expect(handleAddNewChildChunk).toHaveBeenCalled() + // Parent should not be called due to stopPropagation + }) + }) + + describe('computeTotalInfo function', () => { + it('should return search results when searching in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + expect(screen.getByText(/10 search results/i)).toBeInTheDocument() + }) + + it('should return "--" when total is 0 in full-doc mode', () => { + mockParentMode.current = 'full-doc' + + render() + + // When total is 0, displayText is '--' + expect(screen.getByText(/--/)).toBeInTheDocument() + }) + + it('should use childChunks length in paragraph mode', () => { + mockParentMode.current = 'paragraph' + const childChunks = [ + createMockChildChunk(), + createMockChildChunk(), + createMockChildChunk(), + ] + + render() + + expect(screen.getByText(/3 child chunks/i)).toBeInTheDocument() + }) + }) + + describe('Focused State', () => { + it('should not apply opacity when focused even if disabled', () => { + const { container } = render( + , + ) + + const wrapper = container.firstChild + expect(wrapper).not.toHaveClass('opacity-50') + }) + }) + + describe('Input clear button', () => { + it('should call handleInputChange with empty string when clear is clicked', () => { + mockParentMode.current = 'full-doc' + const handleInputChange = vi.fn() + + render( + , + ) + + // Find the clear button (it's the showClearIcon button in Input) + const input = document.querySelector('input') + if (input) { + // Trigger clear by simulating the input's onClear + const clearButton = document.querySelector('[class*="cursor-pointer"]') + if (clearButton) + fireEvent.click(clearButton) + } + }) + }) +}) diff --git a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx index b23aac6af9..fd6fd338d0 100644 --- a/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx +++ b/web/app/components/datasets/documents/detail/completed/child-segment-list.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import type { ChildChunkDetail } from '@/models/datasets' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' -import { useMemo, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import Input from '@/app/components/base/input' @@ -29,6 +29,37 @@ type IChildSegmentCardProps = { focused?: boolean } +function computeTotalInfo( + isFullDocMode: boolean, + isSearching: boolean, + total: number | undefined, + childChunksLength: number, +): { displayText: string, count: number, translationKey: 'segment.searchResults' | 'segment.childChunks' } { + if (isSearching) { + const count = total ?? 0 + return { + displayText: count === 0 ? '--' : String(formatNumber(count)), + count, + translationKey: 'segment.searchResults', + } + } + + if (isFullDocMode) { + const count = total ?? 0 + return { + displayText: count === 0 ? '--' : String(formatNumber(count)), + count, + translationKey: 'segment.childChunks', + } + } + + return { + displayText: String(formatNumber(childChunksLength)), + count: childChunksLength, + translationKey: 'segment.childChunks', + } +} + const ChildSegmentList: FC = ({ childChunks, parentChunkId, @@ -49,59 +80,87 @@ const ChildSegmentList: FC = ({ const [collapsed, setCollapsed] = useState(true) - const toggleCollapse = () => { - setCollapsed(!collapsed) + const isParagraphMode = parentMode === 'paragraph' + const isFullDocMode = parentMode === 'full-doc' + const isSearching = inputValue !== '' && isFullDocMode + const contentOpacity = (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100' + const { displayText, count, translationKey } = computeTotalInfo(isFullDocMode, isSearching, total, childChunks.length) + const totalText = `${displayText} ${t(translationKey, { ns: 'datasetDocuments', count })}` + + const toggleCollapse = () => setCollapsed(prev => !prev) + const showContent = (isFullDocMode && !isLoading) || !collapsed + const hoverVisibleClass = isParagraphMode ? 'hidden group-hover/card:inline-block' : '' + + const renderCollapseIcon = () => { + if (!isParagraphMode) + return null + const Icon = collapsed ? RiArrowRightSLine : RiArrowDownSLine + return } - const isParagraphMode = useMemo(() => { - return parentMode === 'paragraph' - }, [parentMode]) + const renderChildChunkItem = (childChunk: ChildChunkDetail) => { + const isEdited = childChunk.updated_at !== childChunk.created_at + const isFocused = currChildChunk?.childChunkInfo?.id === childChunk.id + const label = isEdited + ? `C-${childChunk.position} · ${t('segment.edited', { ns: 'datasetDocuments' })}` + : `C-${childChunk.position}` - const isFullDocMode = useMemo(() => { - return parentMode === 'full-doc' - }, [parentMode]) + return ( + onDelete?.(childChunk.segment_id, childChunk.id)} + className="child-chunk" + labelClassName={isFocused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''} + labelInnerClassName="text-[10px] font-semibold align-bottom leading-6" + contentClassName={cn('!leading-6', isFocused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')} + showDivider={false} + onClick={(e) => { + e.stopPropagation() + onClickSlice?.(childChunk) + }} + offsetOptions={({ rects }) => ({ + mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width, + crossAxis: (20 - rects.floating.height) / 2, + })} + /> + ) + } - const contentOpacity = useMemo(() => { - return (enabled || focused) ? '' : 'opacity-50 group-hover/card:opacity-100' - }, [enabled, focused]) - - const totalText = useMemo(() => { - const isSearch = inputValue !== '' && isFullDocMode - if (!isSearch) { - const text = isFullDocMode - ? !total - ? '--' - : formatNumber(total) - : formatNumber(childChunks.length) - const count = isFullDocMode - ? text === '--' - ? 0 - : total - : childChunks.length - return `${text} ${t('segment.childChunks', { ns: 'datasetDocuments', count })}` + const renderContent = () => { + if (childChunks.length > 0) { + return ( + + {childChunks.map(renderChildChunkItem)} + + ) } - else { - const text = !total ? '--' : formatNumber(total) - const count = text === '--' ? 0 : total - return `${count} ${t('segment.searchResults', { ns: 'datasetDocuments', count })}` + if (inputValue !== '') { + return ( +
+ +
+ ) } - }, [isFullDocMode, total, childChunks.length, inputValue]) + return null + } return (
- {isFullDocMode ? : null} -
+ {isFullDocMode && } +
{ @@ -109,23 +168,15 @@ const ChildSegmentList: FC = ({ toggleCollapse() }} > - { - isParagraphMode - ? collapsed - ? ( - - ) - : () - : null - } + {renderCollapseIcon()} {totalText} - · + ·
- {isFullDocMode - ? ( - handleInputChange?.(e.target.value)} - onClear={() => handleInputChange?.('')} - /> - ) - : null} + {isFullDocMode && ( + handleInputChange?.(e.target.value)} + onClear={() => handleInputChange?.('')} + /> + )}
- {isLoading ? : null} - {((isFullDocMode && !isLoading) || !collapsed) - ? ( -
- {isParagraphMode && ( -
- -
- )} - {childChunks.length > 0 - ? ( - - {childChunks.map((childChunk) => { - const edited = childChunk.updated_at !== childChunk.created_at - const focused = currChildChunk?.childChunkInfo?.id === childChunk.id - return ( - onDelete?.(childChunk.segment_id, childChunk.id)} - className="child-chunk" - labelClassName={focused ? 'bg-state-accent-solid text-text-primary-on-surface' : ''} - labelInnerClassName="text-[10px] font-semibold align-bottom leading-6" - contentClassName={cn('!leading-6', focused ? 'bg-state-accent-hover-alt text-text-primary' : 'text-text-secondary')} - showDivider={false} - onClick={(e) => { - e.stopPropagation() - onClickSlice?.(childChunk) - }} - offsetOptions={({ rects }) => { - return { - mainAxis: isFullDocMode ? -rects.floating.width : 12 - rects.floating.width, - crossAxis: (20 - rects.floating.height) / 2, - } - }} - /> - ) - })} - - ) - : inputValue !== '' - ? ( -
- -
- ) - : null} + {isLoading && } + {showContent && ( +
+ {isParagraphMode && ( +
+
- ) - : null} + )} + {renderContent()} +
+ )}
) } diff --git a/web/app/components/datasets/documents/detail/completed/common/drawer.tsx b/web/app/components/datasets/documents/detail/completed/common/drawer.tsx index dc1b7192c3..a68742890a 100644 --- a/web/app/components/datasets/documents/detail/completed/common/drawer.tsx +++ b/web/app/components/datasets/documents/detail/completed/common/drawer.tsx @@ -17,6 +17,31 @@ type DrawerProps = { needCheckChunks?: boolean } +const SIDE_POSITION_CLASS = { + right: 'right-0', + left: 'left-0', + bottom: 'bottom-0', + top: 'top-0', +} as const + +function containsTarget(selector: string, target: Node | null): boolean { + const elements = document.querySelectorAll(selector) + return Array.from(elements).some(el => el?.contains(target)) +} + +function shouldReopenChunkDetail( + isClickOnChunk: boolean, + isClickOnChildChunk: boolean, + segmentModalOpen: boolean, + childChunkModalOpen: boolean, +): boolean { + if (segmentModalOpen && isClickOnChildChunk) + return true + if (childChunkModalOpen && isClickOnChunk && !isClickOnChildChunk) + return true + return !isClickOnChunk && !isClickOnChildChunk +} + const Drawer = ({ open, onClose, @@ -41,22 +66,22 @@ const Drawer = ({ const shouldCloseDrawer = useCallback((target: Node | null) => { const panelContent = panelContentRef.current - if (!panelContent) + if (!panelContent || !target) return false - const chunks = document.querySelectorAll('.chunk-card') - const childChunks = document.querySelectorAll('.child-chunk') - const imagePreviewer = document.querySelector('.image-previewer') - const isClickOnChunk = Array.from(chunks).some((chunk) => { - return chunk && chunk.contains(target) - }) - const isClickOnChildChunk = Array.from(childChunks).some((chunk) => { - return chunk && chunk.contains(target) - }) - const reopenChunkDetail = (currSegment.showModal && isClickOnChildChunk) - || (currChildChunk.showModal && isClickOnChunk && !isClickOnChildChunk) || (!isClickOnChunk && !isClickOnChildChunk) - const isClickOnImagePreviewer = imagePreviewer && imagePreviewer.contains(target) - return target && !panelContent.contains(target) && (!needCheckChunks || reopenChunkDetail) && !isClickOnImagePreviewer - }, [currSegment, currChildChunk, needCheckChunks]) + + if (panelContent.contains(target)) + return false + + if (containsTarget('.image-previewer', target)) + return false + + if (!needCheckChunks) + return true + + const isClickOnChunk = containsTarget('.chunk-card', target) + const isClickOnChildChunk = containsTarget('.child-chunk', target) + return shouldReopenChunkDetail(isClickOnChunk, isClickOnChildChunk, currSegment.showModal, currChildChunk.showModal) + }, [currSegment.showModal, currChildChunk.showModal, needCheckChunks]) const onDownCapture = useCallback((e: PointerEvent) => { if (!open || modal) @@ -77,32 +102,27 @@ const Drawer = ({ const isHorizontal = side === 'left' || side === 'right' + const overlayPointerEvents = modal && open ? 'pointer-events-auto' : 'pointer-events-none' + const content = (
- {showOverlay - ? ( -