diff --git a/.agents/skills/frontend-code-review/references/accessibility-ui.md b/.agents/skills/frontend-code-review/references/accessibility-ui.md index e8a841502e0..ed3dc42b6e2 100644 --- a/.agents/skills/frontend-code-review/references/accessibility-ui.md +++ b/.agents/skills/frontend-code-review/references/accessibility-ui.md @@ -26,6 +26,14 @@ Flag: Prefer semantic HTML before ARIA. +## Page Landmarks + +When reviewing Web page layouts, navigation, or landmark changes, read [Page landmarks]. +Inspect the composed page with its parent layouts for missing or duplicate main regions, +inappropriate nesting or roles, and ambiguous or broken landmark names. Check affected +unit and E2E locators when semantic elements change. Web owns these composition rules; +use the existing naming contract for label-source decisions. + ## Accessible Names And Descriptions Read [Accessible names and descriptions] when a change affects labels, ARIA naming, help/error relationships, or hidden text. That document owns the shared implementation and review contract. @@ -123,4 +131,5 @@ Flag: - Hardcoded dates, times, numbers, or currency formats instead of `Intl.*`. [Accessible names and descriptions]: ../../../../packages/dify-ui/docs/accessible-names-and-descriptions.md +[Page landmarks]: ../../../../web/docs/landmarks.md [Web Interface Guidelines]: https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md diff --git a/web/AGENTS.md b/web/AGENTS.md index e87014aa21a..dc59e66c9be 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -9,6 +9,7 @@ Web owns application-specific requirements and consumes shared architecture guidance from skills and primitive contracts from Dify UI. Link to those owners instead of redefining their rules here. - For truncated text disclosure and native `title` decisions, follow [Truncated Text Disclosure]. +- When adding or changing page layouts, navigation, or landmark elements, follow [Page landmarks] and inspect the composed page, including parent layouts. - User-facing strings must use `web/i18n/en-US/` keys. When adding or renaming a key, update every supported locale with the correct localized value. - For new backend calls and migrated surfaces, use generated `consoleQuery` / `consoleClient` APIs from `@/service/console`. Do not add handwritten REST helpers or DTO mirrors, mock-backed app state, or direct edits to generated contracts. - Prefer `@langgenius/dify-ui/*` primitives, data attributes, and design tokens. Use the [Dify UI package index] to find a primitive; read the relevant contract directly when it is already known. Preserve a visible focus indicator on the final focusable element. @@ -36,5 +37,6 @@ This block is written and re-added by `next dev` — verify at `node_modules/nex [Dify UI package index]: ../packages/dify-ui/README.md [IconButton contract]: ../packages/dify-ui/src/icon-button/README.md [Input Group contract]: ../packages/dify-ui/src/input-group/README.md +[Page landmarks]: docs/landmarks.md [Truncated Text Disclosure]: docs/truncated-text-disclosure.md [form contract]: ../packages/dify-ui/docs/forms.md diff --git a/web/app/components/app/access-config/index.tsx b/web/app/components/app/access-config/index.tsx index 1452615d87b..79acb7e7b3c 100644 --- a/web/app/components/app/access-config/index.tsx +++ b/web/app/components/app/access-config/index.tsx @@ -202,7 +202,7 @@ const AppAccessConfigContent = ({ appId, maintainerId }: AppAccessConfigContentP {t(($) => $['accessRule.appDescription'], { ns: 'permission' })}

-
+
-
+ ) } diff --git a/web/app/components/app/access-point/index.tsx b/web/app/components/app/access-point/index.tsx index cda649d7c58..6aa7750dc16 100644 --- a/web/app/components/app/access-point/index.tsx +++ b/web/app/components/app/access-point/index.tsx @@ -67,7 +67,7 @@ function AccessPointContent({ environment === selectedEnvironment ? highlightedAccessPoint : null return ( -
+
@@ -145,7 +145,7 @@ function AccessPointContent({ -
+ ) } diff --git a/web/app/components/app/deploy/index.tsx b/web/app/components/app/deploy/index.tsx index 8ef0fa7f57e..2b82aabfb7f 100644 --- a/web/app/components/app/deploy/index.tsx +++ b/web/app/components/app/deploy/index.tsx @@ -108,7 +108,7 @@ function AppDeployContent({ return ( <> -
+

@@ -140,7 +140,7 @@ function AppDeployContent({ onUndeploy={undeployWorkflow} />

-
+ {deploymentRequest && ( {t(($) => $['accessRule.datasetDescription'], { ns: 'permission' })}

-
+
onBatchRemoveAccessPolicyMemberBindings={handleBatchRemoveAccessPolicyMemberBindings} onAddAccessSubject={handleAddAccessSubject} /> -
+ ) } diff --git a/web/app/components/datasets/list/__tests__/datasets.spec.tsx b/web/app/components/datasets/list/__tests__/datasets.spec.tsx index c039b886710..8e282b8ecc6 100644 --- a/web/app/components/datasets/list/__tests__/datasets.spec.tsx +++ b/web/app/components/datasets/list/__tests__/datasets.spec.tsx @@ -299,33 +299,22 @@ describe('Datasets', () => { expect(screen.queryByRole('status', { name: /common\.loading/ })).not.toBeInTheDocument() expect(screen.getByText('Dataset 1')).toBeInTheDocument() }) - - it('should show Loading component when isFetchingNextPage is true', () => { - render() - expect(screen.getByRole('navigation')).toBeInTheDocument() - }) - - it('should NOT show Loading component when isFetchingNextPage is false', () => { - render() - expect(screen.getByRole('navigation')).toBeInTheDocument() - }) }) describe('DatasetList null handling', () => { - it('should handle null datasetList gracefully', () => { - render() - expect(screen.getByRole('navigation')).toBeInTheDocument() - }) - - it('should handle undefined datasetList gracefully', () => { - render() - expect(screen.getByRole('navigation')).toBeInTheDocument() - }) - - it('should handle empty pages array', () => { - render() - expect(screen.getByRole('navigation')).toBeInTheDocument() - }) + it.each([null, undefined, createDatasetListData([])])( + 'shows the empty state when dataset data is unavailable or empty (%j)', + (datasetList) => { + render( + No knowledge bases

} + />, + ) + expect(screen.getByText('No knowledge bases')).toBeInTheDocument() + }, + ) }) describe('IntersectionObserver', () => { @@ -409,20 +398,18 @@ describe('Datasets', () => { }) }) - describe('Styles', () => { - it('should have correct grid styling', () => { - render() - const nav = screen.getByRole('navigation') - expect(nav).toHaveClass( - 'relative', - 'grid', - 'grow', - 'grid-cols-[repeat(auto-fill,minmax(296px,1fr))]', - 'content-start', - 'gap-3', - 'px-8', - 'pt-2', + describe('Landmarks', () => { + it('keeps knowledge cards in the page content without creating another navigation landmark', () => { + render( + , ) + expect(screen.getByText('Dataset 1')).toBeInTheDocument() + expect(screen.queryByRole('navigation')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/datasets/list/datasets.tsx b/web/app/components/datasets/list/datasets.tsx index 28c7fbee3c9..fbf6ea88c33 100644 --- a/web/app/components/datasets/list/datasets.tsx +++ b/web/app/components/datasets/list/datasets.tsx @@ -69,7 +69,7 @@ const Datasets = ({ return ( <> - + ) } diff --git a/web/app/components/integrations/__tests__/page.spec.tsx b/web/app/components/integrations/__tests__/page.spec.tsx index d17044f0bfc..e1e85cd0650 100644 --- a/web/app/components/integrations/__tests__/page.spec.tsx +++ b/web/app/components/integrations/__tests__/page.spec.tsx @@ -366,7 +366,10 @@ describe('IntegrationsPage', () => { expect(screen.getByTestId('model-provider-page')).toBeInTheDocument() expect(screen.getAllByText('common.settings.provider')).toHaveLength(2) expect(container.firstElementChild).toHaveClass('bg-components-panel-bg') - expect(container.querySelector('aside')).toHaveClass('bg-components-panel-bg') + expect( + screen.getByRole('navigation', { name: 'common.settings.integrations' }), + ).toBeInTheDocument() + expect(screen.queryByRole('complementary')).not.toBeInTheDocument() }) it('does not replace the document title when embedded in a modal', () => { diff --git a/web/app/components/integrations/index.tsx b/web/app/components/integrations/index.tsx index 9a4f5bcbe55..16aa348862c 100644 --- a/web/app/components/integrations/index.tsx +++ b/web/app/components/integrations/index.tsx @@ -12,7 +12,7 @@ import { ScrollAreaThumb, ScrollAreaViewport, } from '@langgenius/dify-ui/scroll-area' -import { useEffect, useState } from 'react' +import { useEffect, useId, useState } from 'react' import { useTranslation } from 'react-i18next' import UpdateSettingDialog from '@/app/components/header/account-setting/update-setting-dialog' import { @@ -126,6 +126,7 @@ export default function IntegrationsPage({ syncDocumentTitle = false, }: IntegrationsPageProps) { const { t } = useTranslation() + const navigationTitleId = useId() const docLink = useDocLink() const router = useRouter() const section = useIntegrationSection(routeSection) @@ -253,7 +254,7 @@ export default function IntegrationsPage({ {syncDocumentTitle && ( )} - +
{useFillLayout ? (
diff --git a/web/app/components/main-nav/__tests__/index.spec.tsx b/web/app/components/main-nav/__tests__/index.spec.tsx index e727ea48c4f..98dd2b88bf6 100644 --- a/web/app/components/main-nav/__tests__/index.spec.tsx +++ b/web/app/components/main-nav/__tests__/index.spec.tsx @@ -710,7 +710,7 @@ describe('MainNav', () => { marketplaceLink.querySelector('.i-custom-vender-main-nav-marketplace-v2'), ).toBeInTheDocument() expect( - within(screen.getByRole('navigation')) + within(screen.getByRole('navigation', { name: 'common.navigation.primary' })) .getAllByRole('link') .map((link) => link.getAttribute('href')), ).toEqual([ diff --git a/web/app/components/main-nav/index.tsx b/web/app/components/main-nav/index.tsx index a2c59cbe291..5aa79c5f73a 100644 --- a/web/app/components/main-nav/index.tsx +++ b/web/app/components/main-nav/index.tsx @@ -118,7 +118,10 @@ export function MainNav({ className }: MainNavProps) {
-
) } diff --git a/web/app/components/plugins/marketplace/creator-profile/view.tsx b/web/app/components/plugins/marketplace/creator-profile/view.tsx index fec3ac7e8d8..e93f490f187 100644 --- a/web/app/components/plugins/marketplace/creator-profile/view.tsx +++ b/web/app/components/plugins/marketplace/creator-profile/view.tsx @@ -45,7 +45,7 @@ export default function CreatorProfileView({ return (
{header} -
- + ) } diff --git a/web/app/signin/__tests__/layout.spec.tsx b/web/app/signin/__tests__/layout.spec.tsx new file mode 100644 index 00000000000..43b748d468b --- /dev/null +++ b/web/app/signin/__tests__/layout.spec.tsx @@ -0,0 +1,21 @@ +import { screen, within } from '@testing-library/react' +import { renderWithConsoleQuery as render } from '@/test/console/query-data' +import SignInLayout from '../layout' + +describe('SignInLayout landmarks', () => { + it('separates the site header, sign-in content, and copyright', () => { + render( + +

Sign in

+
, + { systemFeatures: { branding: { enabled: false } } }, + ) + + const main = screen.getByRole('main') + expect(within(main).getByRole('heading', { name: 'Sign in' })).toBeInTheDocument() + expect(screen.getByRole('banner')).not.toContainElement(main) + expect(screen.getByRole('contentinfo')).toHaveTextContent('LangGenius') + expect(main).not.toContainElement(screen.getByRole('banner')) + expect(main).not.toContainElement(screen.getByRole('contentinfo')) + }) +}) diff --git a/web/app/signin/_header.tsx b/web/app/signin/_header.tsx index 6bb0baed44e..93bbd93ceb6 100644 --- a/web/app/signin/_header.tsx +++ b/web/app/signin/_header.tsx @@ -19,7 +19,7 @@ const Header = () => { const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) return ( -
+
{systemFeatures.branding.enabled && systemFeatures.branding.login_page_logo ? ( {
- + ) } diff --git a/web/app/signin/layout.tsx b/web/app/signin/layout.tsx index 5252ab9441d..b1eb5541c67 100644 --- a/web/app/signin/layout.tsx +++ b/web/app/signin/layout.tsx @@ -15,15 +15,15 @@ export default function SignInLayout({ children }: any) { )} >
-
{children}
-
+ {systemFeatures.branding.enabled === false && ( -
+
© {new Date().getFullYear()} LangGenius, Inc. All rights reserved. -
+ )} diff --git a/web/docs/landmarks.md b/web/docs/landmarks.md new file mode 100644 index 00000000000..a64d1b4fe1f --- /dev/null +++ b/web/docs/landmarks.md @@ -0,0 +1,57 @@ +# Page Landmarks + +Web owns landmark composition across route layouts and feature components. Use the +[W3C landmark guide] for role semantics and the Dify UI [naming contract] for accessible +names. This guide records how those contracts apply to Web pages. + +## Page Ownership + +- Place perceivable page content in meaningful landmark regions and check coverage after + composing the layout and its children. +- Trace the parent layouts before adding a page-level landmark. Inspect the composed + page, including parallel route slots and portals, rather than judging a component alone. +- [MainNavLayout] provides the console's `main` and skip-navigation target. Descendant + pages and editors must not create another `main`. Hiding the main navigation does not + remove this wrapper. A route outside this layout must establish its own main-content + boundary in its owning layout or page. +- Keep the skip link and its target under the same layout owner. Preserve the target ID + and focus behavior when changing wrappers; a landmark role alone does not move focus. +- Keep page and section headings meaningful inside these boundaries. A heading does not + create a landmark or automatically name its surrounding section. + +## Choose Roles by Content + +- Use `nav` for a meaningful group of navigation links. A grid of resource cards is not + automatically navigation merely because its cards can open detail pages. +- A visual sidebar is not automatically complementary content. File controls, editors, + and builder or version panels are parts of the current task. Use ordinary containers + or named sections according to their navigation value. Follow APG's recommendation + to keep genuine complementary landmarks at the top level. +- Reserve named `section` regions for content useful to reach directly. Avoid turning + every wrapper, card, or modal body into a landmark. Prefer an existing visible heading + as the naming source, following the [naming contract]. +- Give repeated landmarks distinct names by purpose unless their content and purpose + are identical. Keep names localized and avoid repeating the role in the name. +- Page-level `header` and `footer` can expose `banner` and `contentinfo`. Their implicit + roles depend on ancestors; local panel headers and footers do not necessarily expose + those landmarks. Check the composed context before adding explicit roles. + +## Verify the Composed Page + +- Check the main-content boundary, landmark hierarchy, names, and heading relationships + in the relevant loaded, empty, collapsed, and open-overlay states. Exclude hidden + skeletons from the exposed landmark inventory. Check that label references still + resolve when conditional content changes. +- When changing a semantic element, inspect affected unit tests and E2E locators, + including CSS selectors such as `closest('main')`. Locate the intended feature or + control instead of relying on an extra page landmark or positional selection among + duplicate landmarks. +- Follow [Web testing policy] for regression coverage. At composition boundaries, keep + the components that own landmark semantics real; a mocked sidebar cannot prove the + real sidebar's role. Distinguish DOM assertions and Cucumber dry-runs from actual + browser or assistive-technology verification. + +[MainNavLayout]: ../app/components/main-nav/layout.tsx +[W3C landmark guide]: https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions +[Web testing policy]: test.md +[naming contract]: ../../packages/dify-ui/docs/accessible-names-and-descriptions.md diff --git a/web/features/new-rag/add-source-page.tsx b/web/features/new-rag/add-source-page.tsx index f5146738950..43899bb3db6 100644 --- a/web/features/new-rag/add-source-page.tsx +++ b/web/features/new-rag/add-source-page.tsx @@ -960,7 +960,7 @@ export function AddSourcePage({ return ( <> -
+

{t(($) => $['newKnowledge.addSource'])} @@ -1111,7 +1111,7 @@ export function AddSourcePage({

)}

-
+ +

@@ -749,6 +749,6 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) ) : null} )} - +

) } diff --git a/web/features/skills/__tests__/detail-page-builder.spec.tsx b/web/features/skills/__tests__/detail-page-builder.spec.tsx index 10bf5f6ba92..fbeb3586b5e 100644 --- a/web/features/skills/__tests__/detail-page-builder.spec.tsx +++ b/web/features/skills/__tests__/detail-page-builder.spec.tsx @@ -93,7 +93,7 @@ describe('SkillDetailPage builder', () => { ).not.toBeInTheDocument() }) - it('moves the collapsed Skill Builder entry into the file tab header', async () => { + it('reopens Skill Builder from the collapsed entry', async () => { const user = userEvent.setup() renderSkillDetailPage() @@ -106,12 +106,14 @@ describe('SkillDetailPage builder', () => { const openBuilderButton = screen.getByRole('button', { name: 'skill.skillManagement.detail.builder.open', }) - expect(openBuilderButton.closest('main')).toBeInTheDocument() + expect( + screen.queryByRole('region', { name: 'skill.skillManagement.detail.builder.title' }), + ).not.toBeInTheDocument() await user.click(openBuilderButton) expect( - await screen.findByRole('button', { - name: 'skill.skillManagement.detail.builder.close', + await screen.findByRole('region', { + name: 'skill.skillManagement.detail.builder.title', }), ).toBeInTheDocument() }) diff --git a/web/features/skills/__tests__/detail-page-clipboard.spec.tsx b/web/features/skills/__tests__/detail-page-clipboard.spec.tsx index fdfbe6cde15..dc2f4b459cb 100644 --- a/web/features/skills/__tests__/detail-page-clipboard.spec.tsx +++ b/web/features/skills/__tests__/detail-page-clipboard.spec.tsx @@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from 'vite-plus/test' import { createDataTransfer, createSkillDetail, + getFileTabButton, getFileTreeButton, getFileTreeContextRegion, getFileTreeItem, @@ -247,13 +248,7 @@ describe('SkillDetailPage clipboard', () => { ) }) await waitFor(() => { - const editorMain = screen.getAllByRole('main').at(-1) - if (!editorMain) throw new Error('file editor not found') - expect( - within(editorMain).getByRole('button', { - name: 'SKILL copy.md', - }), - ).toBeInTheDocument() + expect(getFileTabButton('SKILL copy.md')).toHaveAccessibleName('SKILL copy.md') }) }) diff --git a/web/features/skills/__tests__/detail-page-navigation.spec.tsx b/web/features/skills/__tests__/detail-page-navigation.spec.tsx index cfb50da5893..f33b8ceac72 100644 --- a/web/features/skills/__tests__/detail-page-navigation.spec.tsx +++ b/web/features/skills/__tests__/detail-page-navigation.spec.tsx @@ -51,12 +51,21 @@ describe('SkillDetailPage navigation', () => { const user = userEvent.setup() renderSkillDetailPage() + expect( + await screen.findByRole('region', { name: /skillManagement\.detail\.fileCount/ }), + ).toBeInTheDocument() + expect(screen.queryByRole('complementary')).not.toBeInTheDocument() + expect(screen.queryByRole('main')).not.toBeInTheDocument() + await user.click( await screen.findByRole('button', { name: 'skill.skillManagement.detail.collapseSidebar', }), ) expect(screen.queryByTestId('skill-detail-sidebar-header')).not.toBeInTheDocument() + expect( + screen.queryByRole('region', { name: /skillManagement\.detail\.fileCount/ }), + ).not.toBeInTheDocument() fireEvent.click( screen.getByRole('button', { @@ -64,6 +73,9 @@ describe('SkillDetailPage navigation', () => { }), ) expect(await screen.findByTestId('skill-detail-sidebar-header')).toBeInTheDocument() + expect( + screen.getByRole('region', { name: /skillManagement\.detail\.fileCount/ }), + ).toBeInTheDocument() }) it('shows the sidebar while the collapsed rail is hovered', async () => { diff --git a/web/features/skills/__tests__/detail-page-publishing.spec.tsx b/web/features/skills/__tests__/detail-page-publishing.spec.tsx index 55e56ccdb55..3e9de6dd156 100644 --- a/web/features/skills/__tests__/detail-page-publishing.spec.tsx +++ b/web/features/skills/__tests__/detail-page-publishing.spec.tsx @@ -517,7 +517,7 @@ describe('SkillDetailPage publishing', () => { await screen.findByRole('button', { name: 'skill.skillManagement.detail.versionHistory' }), ) - await screen.findByText('skill.skillManagement.detail.versions') + await screen.findByRole('region', { name: 'skill.skillManagement.detail.versions' }) expect(screen.getAllByRole('button', { current: true })).toHaveLength(1) await user.click( diff --git a/web/features/skills/__tests__/detail-page.fixture.tsx b/web/features/skills/__tests__/detail-page.fixture.tsx index f3d49ed6adc..595c1959233 100644 --- a/web/features/skills/__tests__/detail-page.fixture.tsx +++ b/web/features/skills/__tests__/detail-page.fixture.tsx @@ -645,13 +645,9 @@ export async function openFileTreeActions(user: ReturnType) { - const triggers = Array.from(document.querySelectorAll('aside .i-ri-add-line')) - .map((icon) => icon.closest('button')) - .filter((button): button is HTMLButtonElement => button instanceof HTMLButtonElement) - const trigger = triggers.at(-1) - if (!(trigger instanceof HTMLButtonElement)) throw new Error('root create menu trigger not found') + const fileTree = screen.getByRole('region', { name: /skillManagement\.detail\.fileCount/ }) - await user.click(trigger) + await user.click(within(fileTree).getByRole('button', { name: 'common.operation.add' })) } export async function confirmUploadReview() { diff --git a/web/features/skills/detail/builder-panel.tsx b/web/features/skills/detail/builder-panel.tsx index 7cf354b2e52..766abead7eb 100644 --- a/web/features/skills/detail/builder-panel.tsx +++ b/web/features/skills/detail/builder-panel.tsx @@ -11,7 +11,7 @@ import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' import { toast } from '@langgenius/dify-ui/toast' import { useQuery, useQueryClient } from '@tanstack/react-query' -import { useEffect, useMemo, useRef, useState } from 'react' +import { useEffect, useId, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Markdown } from '@/app/components/base/markdown' import { @@ -298,6 +298,7 @@ export function SkillBuilderPanel({ }) { const { t } = useTranslation('skill') const queryClient = useQueryClient() + const titleId = useId() const [prompt, setPrompt] = useState('') const initialBuilderModeRef = useRef({ isEditMode: !isDefaultSkillBuilderDraft(detail), @@ -803,7 +804,10 @@ export function SkillBuilderPanel({ } return ( - +
) } diff --git a/web/features/skills/detail/file-editor.tsx b/web/features/skills/detail/file-editor.tsx index ccb19b09196..0f7767bcb81 100644 --- a/web/features/skills/detail/file-editor.tsx +++ b/web/features/skills/detail/file-editor.tsx @@ -1075,7 +1075,7 @@ export function FileEditor({ } return ( -
+
)} -
+ ) } diff --git a/web/features/skills/detail/file-tree.tsx b/web/features/skills/detail/file-tree.tsx index 6a40d2cb225..80a4ab271c9 100644 --- a/web/features/skills/detail/file-tree.tsx +++ b/web/features/skills/detail/file-tree.tsx @@ -55,7 +55,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too import { formatForDisplay, matchesKeyboardEvent, useHotkey } from '@tanstack/react-hotkeys' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import copy from 'copy-to-clipboard' -import { useCallback, useEffect, useEffectEvent, useRef, useState } from 'react' +import { useCallback, useEffect, useEffectEvent, useId, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import SidebarLeftArrowIcon from '@/app/components/base/icons/src/vender/SidebarLeftArrowIcon' import { gotoAnythingDialogHandle } from '@/app/components/goto-anything/dialog-handle' @@ -175,6 +175,7 @@ export function FileTree({ const { t: tCommon } = useTranslation('common') const queryClient = useQueryClient() const sidebarRef = useRef(null) + const filesTitleId = useId() const uploadInputRef = useRef(null) const [inlineAction, setInlineAction] = useState() const [draggingPaths, setDraggingPaths] = useState([]) @@ -1105,7 +1106,7 @@ export function FileTree({ const creatorName = detail?.created_by_name ?? detail?.created_by ?? '-' if (collapsed && !sidebarFloating) { return ( - + ) } return ( <> - + ) } diff --git a/web/features/skills/detail/shell.tsx b/web/features/skills/detail/shell.tsx index 5c467a0ec06..c380b180207 100644 --- a/web/features/skills/detail/shell.tsx +++ b/web/features/skills/detail/shell.tsx @@ -6,7 +6,7 @@ import { SkillBuilderGridTexture } from './builder-grid-texture' export function DetailSkeleton() { return (
- +
-
@@ -68,9 +68,9 @@ export function DetailSkeleton() { -
+ - + ) } diff --git a/web/features/skills/detail/version-panel.tsx b/web/features/skills/detail/version-panel.tsx index 1c1d8c39e6c..207733753d6 100644 --- a/web/features/skills/detail/version-panel.tsx +++ b/web/features/skills/detail/version-panel.tsx @@ -33,7 +33,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/pop import { Textarea } from '@langgenius/dify-ui/textarea' import { toast } from '@langgenius/dify-ui/toast' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { useState } from 'react' +import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' import useTimestamp from '@/hooks/use-timestamp' import { consoleQuery } from '@/service/console' @@ -506,6 +506,7 @@ export function VersionPanel({ const { t } = useTranslation('skill') const { t: tWorkflow } = useTranslation('workflow') const [filterValue, setFilterValue] = useState('all') + const titleId = useId() const filteredVersions = versions.filter((version) => { if (filterValue === 'onlyNamed') return !!version.version_name @@ -513,10 +514,10 @@ export function VersionPanel({ }) return ( - + ) } diff --git a/web/i18n/ar-TN/common.json b/web/i18n/ar-TN/common.json index cb58e11eacb..9c6632d6811 100644 --- a/web/i18n/ar-TN/common.json +++ b/web/i18n/ar-TN/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "تعيين النموذج الافتراضي لإدخال تحويل النص إلى كلام في المحادثة.", "modelProvider.upgradeForLoadBalancing": "قم بترقية خطتك لتمكين موازنة التحميل.", "modelProvider.used": "مستخدم", + "navigation.primary": "القائمة الرئيسية", "navigation.skipToMain": "الانتقال إلى المحتوى الرئيسي", "noData": "لا توجد بيانات", "operation.add": "إضافة", diff --git a/web/i18n/de-DE/common.json b/web/i18n/de-DE/common.json index 3eca0f3ce2f..c583056c60f 100644 --- a/web/i18n/de-DE/common.json +++ b/web/i18n/de-DE/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Legen Sie das Standardmodell für die Text-zu-Sprache-Eingabe in Konversationen fest.", "modelProvider.upgradeForLoadBalancing": "Aktualisieren Sie Ihren Plan, um den Lastenausgleich zu aktivieren.", "modelProvider.used": "verwendet", + "navigation.primary": "Hauptmenü", "navigation.skipToMain": "Zum Hauptinhalt springen", "noData": "Keine Daten", "operation.add": "Hinzufügen", diff --git a/web/i18n/en-US/common.json b/web/i18n/en-US/common.json index f7f58407bab..46aecf806ea 100644 --- a/web/i18n/en-US/common.json +++ b/web/i18n/en-US/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Set the default model for text-to-speech input in conversation.", "modelProvider.upgradeForLoadBalancing": "Upgrade your plan to enable Load Balancing.", "modelProvider.used": "used", + "navigation.primary": "Main menu", "navigation.skipToMain": "Skip to main content", "noData": "No data", "operation.add": "Add", diff --git a/web/i18n/es-ES/common.json b/web/i18n/es-ES/common.json index 87bba9f0858..f323f0b3a7a 100644 --- a/web/i18n/es-ES/common.json +++ b/web/i18n/es-ES/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Establece el modelo predeterminado para la entrada de texto a voz en la conversación.", "modelProvider.upgradeForLoadBalancing": "Actualiza tu plan para habilitar el Balanceo de Carga.", "modelProvider.used": "usados", + "navigation.primary": "Menú principal", "navigation.skipToMain": "Saltar al contenido principal", "noData": "Sin datos", "operation.add": "Agregar", diff --git a/web/i18n/fa-IR/common.json b/web/i18n/fa-IR/common.json index 69c94939366..af9f507a995 100644 --- a/web/i18n/fa-IR/common.json +++ b/web/i18n/fa-IR/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "مدل پیش‌فرض را برای ورودی متن به گفتار در مکالمه تنظیم کنید.", "modelProvider.upgradeForLoadBalancing": "برای فعال کردن تعادل بار، طرح خود را ارتقا دهید.", "modelProvider.used": "استفاده‌شده", + "navigation.primary": "منوی اصلی", "navigation.skipToMain": "پرش به محتوای اصلی", "noData": "بدون داده", "operation.add": "افزودن", diff --git a/web/i18n/fr-FR/common.json b/web/i18n/fr-FR/common.json index 4640a108c3a..81174aa8c38 100644 --- a/web/i18n/fr-FR/common.json +++ b/web/i18n/fr-FR/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Définissez le modèle par défaut pour l'entrée de texte à la parole dans une conversation.", "modelProvider.upgradeForLoadBalancing": "Mettez à niveau votre plan pour activer l’équilibrage de charge.", "modelProvider.used": "utilisés", + "navigation.primary": "Menu principal", "navigation.skipToMain": "Aller au contenu principal", "noData": "Aucune donnée", "operation.add": "Ajouter", diff --git a/web/i18n/hi-IN/common.json b/web/i18n/hi-IN/common.json index 275177d98f1..2e3dd49ffe6 100644 --- a/web/i18n/hi-IN/common.json +++ b/web/i18n/hi-IN/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "संवाद में पाठ-से-भाषण इनपुट के लिए डिफ़ॉल्ट मॉडल सेट करें।", "modelProvider.upgradeForLoadBalancing": "लोड बैलेंसिंग सक्षम करने के लिए अपनी योजना अपग्रेड करें।", "modelProvider.used": "उपयोग किया गया", + "navigation.primary": "मुख्य मेन्यू", "navigation.skipToMain": "मुख्य सामग्री पर जाएं", "noData": "कोई डेटा नहीं", "operation.add": "जोड़ें", diff --git a/web/i18n/id-ID/common.json b/web/i18n/id-ID/common.json index 586dc35e203..7d07124d432 100644 --- a/web/i18n/id-ID/common.json +++ b/web/i18n/id-ID/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Atur model default untuk input teks-ke-ucapan dalam percakapan.", "modelProvider.upgradeForLoadBalancing": "Tingkatkan paket Anda untuk mengaktifkan Penyeimbangan Beban.", "modelProvider.used": "digunakan", + "navigation.primary": "Menu utama", "navigation.skipToMain": "Lewati ke konten utama", "noData": "Tidak ada data", "operation.add": "Tambah", diff --git a/web/i18n/it-IT/common.json b/web/i18n/it-IT/common.json index eab7932c4e4..1c64dab4cc2 100644 --- a/web/i18n/it-IT/common.json +++ b/web/i18n/it-IT/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Imposta il modello predefinito per l'input da testo a voce nella conversazione.", "modelProvider.upgradeForLoadBalancing": "Aggiorna il tuo piano per abilitare il Bilanciamento del Carico.", "modelProvider.used": "utilizzati", + "navigation.primary": "Menu principale", "navigation.skipToMain": "Vai al contenuto principale", "noData": "Nessun dato", "operation.add": "Aggiungi", diff --git a/web/i18n/ja-JP/common.json b/web/i18n/ja-JP/common.json index 364681fcde2..84bc0b27994 100644 --- a/web/i18n/ja-JP/common.json +++ b/web/i18n/ja-JP/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "会話でのテキスト-to-音声入力に使用するデフォルトモデルを設定します。", "modelProvider.upgradeForLoadBalancing": "負荷分散を利用するには、プランのアップグレードが必要です。", "modelProvider.used": "使用済み", + "navigation.primary": "メインメニュー", "navigation.skipToMain": "メインコンテンツへスキップ", "noData": "データなし", "operation.add": "追加", diff --git a/web/i18n/ko-KR/common.json b/web/i18n/ko-KR/common.json index 31dfd475da1..3dd3122a045 100644 --- a/web/i18n/ko-KR/common.json +++ b/web/i18n/ko-KR/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "대화에서의 텍스트-to-음성 입력에 사용되는 기본 모델을 설정합니다.", "modelProvider.upgradeForLoadBalancing": "로드 밸런싱을 사용하도록 계획을 업그레이드합니다.", "modelProvider.used": "사용됨", + "navigation.primary": "주 메뉴", "navigation.skipToMain": "본문으로 건너뛰기", "noData": "데이터 없음", "operation.add": "추가", diff --git a/web/i18n/lo-LA/common.json b/web/i18n/lo-LA/common.json index 55b0e58ecd2..14ee09c918f 100644 --- a/web/i18n/lo-LA/common.json +++ b/web/i18n/lo-LA/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "ຕັ້ງຄ່າໂມເດວເລີ່ມຕົ້ນສຳລັບການປ່ຽນຂໍ້ຄວາມເປັນສຽງໃນການສົນທະນາ.", "modelProvider.upgradeForLoadBalancing": "ອັບເກຣດແຜນຂອງທ່ານເພື່ອເປີດໃຊ້ Load Balancing.", "modelProvider.used": "ໃຊ້ໄປແລ້ວ", + "navigation.primary": "ເມນູຫຼັກ", "navigation.skipToMain": "ຂ້າມໄປເນື້ອຫາຫຼັກ", "noData": "ບໍ່ມີຂໍ້ມູນ", "operation.add": "ເພີ່ມ", diff --git a/web/i18n/nl-NL/common.json b/web/i18n/nl-NL/common.json index 52fb04cec72..940df2dcee4 100644 --- a/web/i18n/nl-NL/common.json +++ b/web/i18n/nl-NL/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Set the default model for text-to-speech input in conversation.", "modelProvider.upgradeForLoadBalancing": "Upgrade your plan to enable Load Balancing.", "modelProvider.used": "gebruikt", + "navigation.primary": "Hoofdmenu", "navigation.skipToMain": "Naar hoofdinhoud springen", "noData": "No data", "operation.add": "Add", diff --git a/web/i18n/pl-PL/common.json b/web/i18n/pl-PL/common.json index 9068c36ff1d..94e6a92e7a7 100644 --- a/web/i18n/pl-PL/common.json +++ b/web/i18n/pl-PL/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Ustaw domyślny model dla konwersji tekstu na mowę w rozmowach.", "modelProvider.upgradeForLoadBalancing": "Uaktualnij swój plan, aby włączyć równoważenie obciążenia.", "modelProvider.used": "wykorzystano", + "navigation.primary": "Menu główne", "navigation.skipToMain": "Przejdź do głównej treści", "noData": "Brak danych", "operation.add": "Dodaj", diff --git a/web/i18n/pt-BR/common.json b/web/i18n/pt-BR/common.json index 2a603c397c5..f62115dca2d 100644 --- a/web/i18n/pt-BR/common.json +++ b/web/i18n/pt-BR/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Defina o modelo padrão para entrada de texto para fala na conversa.", "modelProvider.upgradeForLoadBalancing": "Atualize seu plano para habilitar o balanceamento de carga.", "modelProvider.used": "usados", + "navigation.primary": "Menu principal", "navigation.skipToMain": "Pular para o conteúdo principal", "noData": "Sem dados", "operation.add": "Adicionar", diff --git a/web/i18n/ro-RO/common.json b/web/i18n/ro-RO/common.json index 18c8e7ac479..8fe4a66677b 100644 --- a/web/i18n/ro-RO/common.json +++ b/web/i18n/ro-RO/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Setați modelul implicit pentru intrarea de conversie vorbire-la-text în conversație.", "modelProvider.upgradeForLoadBalancing": "Actualizați-vă planul pentru a activa Load Balancing.", "modelProvider.used": "utilizate", + "navigation.primary": "Meniu principal", "navigation.skipToMain": "Sări la conținutul principal", "noData": "Fără date", "operation.add": "Adaugă", diff --git a/web/i18n/ru-RU/common.json b/web/i18n/ru-RU/common.json index 2ed10ac7214..e9518fc0c85 100644 --- a/web/i18n/ru-RU/common.json +++ b/web/i18n/ru-RU/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Установите модель по умолчанию для ввода текста в речь в разговоре.", "modelProvider.upgradeForLoadBalancing": "Обновите свой тарифный план, чтобы включить балансировку нагрузки.", "modelProvider.used": "использовано", + "navigation.primary": "Главное меню", "navigation.skipToMain": "Перейти к основному содержанию", "noData": "Нет данных", "operation.add": "Добавить", diff --git a/web/i18n/sl-SI/common.json b/web/i18n/sl-SI/common.json index b2d67f7400d..b52e8034294 100644 --- a/web/i18n/sl-SI/common.json +++ b/web/i18n/sl-SI/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Nastavite privzeti model za pretvorbo besedila v govor v pogovoru.", "modelProvider.upgradeForLoadBalancing": "Nadgradite svoj načrt, da omogočite uravnoteženje obremenitev.", "modelProvider.used": "porabljeno", + "navigation.primary": "Glavni meni", "navigation.skipToMain": "Preskoči na glavno vsebino", "noData": "Ni podatkov", "operation.add": "Dodaj", diff --git a/web/i18n/th-TH/common.json b/web/i18n/th-TH/common.json index ebe96553cb6..484ba053ea6 100644 --- a/web/i18n/th-TH/common.json +++ b/web/i18n/th-TH/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "ตั้งค่าโมเดลเริ่มต้นสําหรับการป้อนข้อมูลเป็นข้อความเป็นคําพูดในการสนทนา", "modelProvider.upgradeForLoadBalancing": "อัปเกรดแผนของคุณเพื่อเปิดใช้งานการปรับสมดุลโหลด", "modelProvider.used": "ใช้แล้ว", + "navigation.primary": "เมนูหลัก", "navigation.skipToMain": "ข้ามไปยังเนื้อหาหลัก", "noData": "ไม่มีข้อมูล", "operation.add": "เพิ่ม", diff --git a/web/i18n/tr-TR/common.json b/web/i18n/tr-TR/common.json index 1a979ec22ea..1e77c007587 100644 --- a/web/i18n/tr-TR/common.json +++ b/web/i18n/tr-TR/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Konuşmada metinden konuşmaya giriş için varsayılan modeli ayarlayın.", "modelProvider.upgradeForLoadBalancing": "Yük Dengelemeyi etkinleştirmek için planınızı yükseltin.", "modelProvider.used": "kullanıldı", + "navigation.primary": "Ana menü", "navigation.skipToMain": "Ana içeriğe geç", "noData": "Veri yok", "operation.add": "Ekle", diff --git a/web/i18n/uk-UA/common.json b/web/i18n/uk-UA/common.json index a97f8aacce3..13cbc6f247e 100644 --- a/web/i18n/uk-UA/common.json +++ b/web/i18n/uk-UA/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Встановіть модель за замовчуванням для введення тексту в мовлення в розмові.", "modelProvider.upgradeForLoadBalancing": "Оновіть свій план, щоб увімкнути балансування навантаження.", "modelProvider.used": "використано", + "navigation.primary": "Головне меню", "navigation.skipToMain": "Перейти до основного вмісту", "noData": "Немає даних", "operation.add": "Додати", diff --git a/web/i18n/vi-VN/common.json b/web/i18n/vi-VN/common.json index 300eb97cbbc..3187f278e78 100644 --- a/web/i18n/vi-VN/common.json +++ b/web/i18n/vi-VN/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "Thiết lập mô hình mặc định cho đầu vào văn bản thành tiếng nói trong cuộc trò chuyện.", "modelProvider.upgradeForLoadBalancing": "Nâng cấp gói của bạn để bật Cân bằng tải.", "modelProvider.used": "đã dùng", + "navigation.primary": "Menu chính", "navigation.skipToMain": "Chuyển đến nội dung chính", "noData": "Không có dữ liệu", "operation.add": "Thêm", diff --git a/web/i18n/zh-Hans/common.json b/web/i18n/zh-Hans/common.json index 26552a33ee8..e50818cf9a1 100644 --- a/web/i18n/zh-Hans/common.json +++ b/web/i18n/zh-Hans/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "设置对话中文字转语音输出的默认使用模型。", "modelProvider.upgradeForLoadBalancing": "升级以解锁负载均衡功能", "modelProvider.used": "已用", + "navigation.primary": "主菜单", "navigation.skipToMain": "跳转到主要内容", "noData": "暂无数据", "operation.add": "添加", diff --git a/web/i18n/zh-Hant/common.json b/web/i18n/zh-Hant/common.json index 7b001d7afb5..b59a431d51f 100644 --- a/web/i18n/zh-Hant/common.json +++ b/web/i18n/zh-Hant/common.json @@ -441,6 +441,7 @@ "modelProvider.ttsModel.tip": "設定對話中文字轉語音輸出的預設使用模型。", "modelProvider.upgradeForLoadBalancing": "升級您的計劃以啟用 Load Balancing。", "modelProvider.used": "已用", + "navigation.primary": "主選單", "navigation.skipToMain": "跳至主要內容", "noData": "無資料", "operation.add": "新增",