From 995c43f3dd0c88be298237673ac385d3c1e01cb8 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 5 May 2026 22:53:38 +0800 Subject: [PATCH 001/202] refactor: migrate workflow queries to contracts (#35799) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/controllers/console/app/app.py | 38 ++- .../console/app/test_app_response_models.py | 97 ++++++ eslint-suppressions.json | 26 -- .../apps/app-list-browsing-flow.test.tsx | 35 +- web/__tests__/apps/create-app-flow.test.tsx | 35 +- .../(commonLayout)/account-page/index.tsx | 16 +- .../components/apps/__tests__/list.spec.tsx | 92 +++++- .../apps/hooks/use-workflow-online-users.ts | 49 +++ web/app/components/apps/list.tsx | 118 +++---- .../header/app-nav/__tests__/index.spec.tsx | 53 ++- web/app/components/header/app-nav/index.tsx | 98 +++--- .../app-selector/__tests__/index.spec.tsx | 54 +++- .../app-selector/index.tsx | 27 +- .../workflow/comment/comment-icon.spec.tsx | 2 +- .../workflow/comment/comment-icon.tsx | 2 +- .../workflow/comment/comment-preview.spec.tsx | 4 +- .../workflow/comment/comment-preview.tsx | 5 +- .../workflow/comment/mention-input.spec.tsx | 49 ++- .../workflow/comment/mention-input.tsx | 33 +- .../workflow/comment/thread.spec.tsx | 2 +- .../components/workflow/comment/thread.tsx | 2 +- .../__tests__/use-workflow-comment.spec.ts | 303 +++++++++++++++--- .../workflow/hooks/use-workflow-comment.ts | 120 ++++--- .../__tests__/input-var-list.spec.tsx | 29 +- .../comments-panel/__tests__/index.spec.tsx | 25 +- .../workflow/panel/comments-panel/index.tsx | 21 +- .../workflow/store/workflow/comment-slice.ts | 2 +- web/contract/console/apps.ts | 22 +- web/contract/console/workflow-comment.ts | 40 +-- web/contract/router.ts | 3 +- web/service/apps.ts | 29 +- web/service/use-apps.ts | 72 +---- web/service/workflow-comment.ts | 103 ------ 33 files changed, 1013 insertions(+), 593 deletions(-) create mode 100644 web/app/components/apps/hooks/use-workflow-online-users.ts delete mode 100644 web/service/workflow-comment.ts diff --git a/api/controllers/console/app/app.py b/api/controllers/console/app/app.py index a736fc8bc8..c8334bfd18 100644 --- a/api/controllers/console/app/app.py +++ b/api/controllers/console/app/app.py @@ -1,4 +1,5 @@ import logging +import re import uuid from datetime import datetime from typing import Any, Literal @@ -8,6 +9,7 @@ from flask_restx import Resource from pydantic import AliasChoices, BaseModel, Field, computed_field, field_validator from sqlalchemy import select from sqlalchemy.orm import Session +from werkzeug.datastructures import MultiDict from werkzeug.exceptions import BadRequest from controllers.common.helpers import FileInfo @@ -57,6 +59,7 @@ ALLOW_CREATE_APP_MODES = ["chat", "agent-chat", "advanced-chat", "workflow", "co register_enum_models(console_ns, IconType) _logger = logging.getLogger(__name__) +_TAG_IDS_BRACKET_PATTERN = re.compile(r"^tag_ids\[(\d+)\]$") class AppListQuery(BaseModel): @@ -66,22 +69,19 @@ class AppListQuery(BaseModel): default="all", description="App mode filter" ) name: str | None = Field(default=None, description="Filter by app name") - tag_ids: list[str] | None = Field(default=None, description="Comma-separated tag IDs") + tag_ids: list[str] | None = Field(default=None, description="Filter by tag IDs") is_created_by_me: bool | None = Field(default=None, description="Filter by creator") @field_validator("tag_ids", mode="before") @classmethod - def validate_tag_ids(cls, value: str | list[str] | None) -> list[str] | None: + def validate_tag_ids(cls, value: list[str] | None) -> list[str] | None: if not value: return None - if isinstance(value, str): - items = [item.strip() for item in value.split(",") if item.strip()] - elif isinstance(value, list): - items = [str(item).strip() for item in value if item and str(item).strip()] - else: - raise TypeError("Unsupported tag_ids type.") + if not isinstance(value, list): + raise ValueError("Unsupported tag_ids type.") + items = [str(item).strip() for item in value if item and str(item).strip()] if not items: return None @@ -91,6 +91,26 @@ class AppListQuery(BaseModel): raise ValueError("Invalid UUID format in tag_ids.") from exc +def _normalize_app_list_query_args(query_args: MultiDict[str, str]) -> dict[str, str | list[str]]: + normalized: dict[str, str | list[str]] = {} + indexed_tag_ids: list[tuple[int, str]] = [] + + for key in query_args: + match = _TAG_IDS_BRACKET_PATTERN.fullmatch(key) + if match: + indexed_tag_ids.extend((int(match.group(1)), value) for value in query_args.getlist(key)) + continue + + value = query_args.get(key) + if value is not None: + normalized[key] = value + + if indexed_tag_ids: + normalized["tag_ids"] = [value for _, value in sorted(indexed_tag_ids)] + + return normalized + + class CreateAppPayload(BaseModel): name: str = Field(..., min_length=1, description="App name") description: str | None = Field(default=None, description="App description (max 400 chars)", max_length=400) @@ -455,7 +475,7 @@ class AppListApi(Resource): """Get app list""" current_user, current_tenant_id = current_account_with_tenant() - args = AppListQuery.model_validate(request.args.to_dict(flat=True)) # type: ignore + args = AppListQuery.model_validate(_normalize_app_list_query_args(request.args)) args_dict = args.model_dump() # get app list diff --git a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py index 35d07a987d..80e7c41a9e 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py +++ b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py @@ -10,6 +10,8 @@ from typing import Any import pytest from flask.views import MethodView +from pydantic import ValidationError +from werkzeug.datastructures import MultiDict # kombu references MethodView as a global when importing celery/kombu pools. if not hasattr(builtins, "MethodView"): @@ -174,6 +176,101 @@ def _dummy_workflow(): ) +def test_app_list_query_normalizes_orpc_bracket_tag_ids(app_module): + first_tag_id = "8c4ef3d1-58a1-4d94-8a1c-1c171d889e08" + second_tag_id = "3c39395b-6d1f-4030-8b17-eaa7cc85221c" + query_args = MultiDict( + [ + ("page", "1"), + ("limit", "30"), + ("tag_ids[1]", second_tag_id), + ("tag_ids[0]", first_tag_id), + ] + ) + + normalized = app_module._normalize_app_list_query_args(query_args) + query = app_module.AppListQuery.model_validate(normalized) + + assert query.tag_ids == [first_tag_id, second_tag_id] + + +def test_app_list_query_preserves_regular_query_params(app_module): + query_args = MultiDict( + [ + ("page", "2"), + ("limit", "50"), + ("mode", "chat"), + ("name", "Sales Copilot"), + ("is_created_by_me", "true"), + ] + ) + + normalized = app_module._normalize_app_list_query_args(query_args) + query = app_module.AppListQuery.model_validate(normalized) + + assert normalized == { + "page": "2", + "limit": "50", + "mode": "chat", + "name": "Sales Copilot", + "is_created_by_me": "true", + } + assert query.page == 2 + assert query.limit == 50 + assert query.mode == "chat" + assert query.name == "Sales Copilot" + assert query.is_created_by_me is True + assert query.tag_ids is None + + +def test_app_list_query_normalizes_empty_bracket_tag_ids_to_none(app_module): + query_args = MultiDict( + [ + ("tag_ids[0]", ""), + ("tag_ids[1]", " "), + ] + ) + + normalized = app_module._normalize_app_list_query_args(query_args) + query = app_module.AppListQuery.model_validate(normalized) + + assert normalized == {"tag_ids": ["", " "]} + assert query.tag_ids is None + + +def test_app_list_query_rejects_invalid_bracket_tag_id(app_module): + normalized = app_module._normalize_app_list_query_args(MultiDict([("tag_ids[0]", "not-a-uuid")])) + + with pytest.raises(ValidationError): + app_module.AppListQuery.model_validate(normalized) + + +def test_app_list_query_sorts_bracket_tag_ids_by_index(app_module): + first_tag_id = "8c4ef3d1-58a1-4d94-8a1c-1c171d889e08" + second_tag_id = "3c39395b-6d1f-4030-8b17-eaa7cc85221c" + third_tag_id = "9d5ec0f7-4f2b-4e7f-9c13-1e7a034d0eb1" + query_args = MultiDict( + [ + ("tag_ids[2]", third_tag_id), + ("tag_ids[1]", second_tag_id), + ("tag_ids[0]", first_tag_id), + ] + ) + + normalized = app_module._normalize_app_list_query_args(query_args) + query = app_module.AppListQuery.model_validate(normalized) + + assert query.tag_ids == [first_tag_id, second_tag_id, third_tag_id] + + +def test_app_list_query_rejects_flat_tag_ids(app_module): + tag_id = "8c4ef3d1-58a1-4d94-8a1c-1c171d889e08" + normalized = app_module._normalize_app_list_query_args(MultiDict([("tag_ids", tag_id)])) + + with pytest.raises(ValidationError): + app_module.AppListQuery.model_validate(normalized) + + def test_app_partial_serialization_uses_aliases(app_models): AppPartial = app_models.AppPartial created_at = _ts() diff --git a/eslint-suppressions.json b/eslint-suppressions.json index bbb5cd5af9..3c86fb2b7c 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -162,11 +162,6 @@ "count": 5 } }, - "web/app/account/(commonLayout)/account-page/index.tsx": { - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/account/(commonLayout)/delete-account/components/feed-back.tsx": { "no-restricted-imports": { "count": 1 @@ -653,14 +648,6 @@ "count": 2 } }, - "web/app/components/apps/list.tsx": { - "react-hooks/exhaustive-deps": { - "count": 1 - }, - "react/unsupported-syntax": { - "count": 2 - } - }, "web/app/components/apps/new-app-card.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 @@ -2824,14 +2811,6 @@ "count": 4 } }, - "web/app/components/header/app-nav/index.tsx": { - "react/set-state-in-effect": { - "count": 2 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "web/app/components/header/header-wrapper.tsx": { "ts/no-explicit-any": { "count": 1 @@ -5480,11 +5459,6 @@ "count": 2 } }, - "web/service/use-apps.ts": { - "ts/no-explicit-any": { - "count": 1 - } - }, "web/service/use-common.ts": { "ts/no-empty-object-type": { "count": 1 diff --git a/web/__tests__/apps/app-list-browsing-flow.test.tsx b/web/__tests__/apps/app-list-browsing-flow.test.tsx index 768420f00d..e6b83bd69d 100644 --- a/web/__tests__/apps/app-list-browsing-flow.test.tsx +++ b/web/__tests__/apps/app-list-browsing-flow.test.tsx @@ -88,27 +88,36 @@ vi.mock('@/service/tag', () => ({ fetchTagList: vi.fn().mockResolvedValue([]), })) -vi.mock('@/service/apps', () => ({ - fetchWorkflowOnlineUsers: vi.fn().mockResolvedValue({}), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useInfiniteQuery: () => ({ + data: { pages: mockPages }, + isLoading: mockIsLoading, + isFetching: mockIsFetching, + isFetchingNextPage: mockIsFetchingNextPage, + fetchNextPage: mockFetchNextPage, + hasNextPage: mockHasNextPage, + error: mockError, + refetch: mockRefetch, + }), + } +}) vi.mock('@/service/use-apps', () => ({ - useInfiniteAppList: () => ({ - data: { pages: mockPages }, - isLoading: mockIsLoading, - isFetching: mockIsFetching, - isFetchingNextPage: mockIsFetchingNextPage, - fetchNextPage: mockFetchNextPage, - hasNextPage: mockHasNextPage, - error: mockError, - refetch: mockRefetch, - }), useDeleteAppMutation: () => ({ mutateAsync: vi.fn(), isPending: false, }), })) +vi.mock('@/app/components/apps/hooks/use-workflow-online-users', () => ({ + useWorkflowOnlineUsers: () => ({ + onlineUsersMap: {}, + }), +})) + vi.mock('@/hooks/use-pay', () => ({ CheckModal: () => null, })) diff --git a/web/__tests__/apps/create-app-flow.test.tsx b/web/__tests__/apps/create-app-flow.test.tsx index e480db06ea..079ea9949a 100644 --- a/web/__tests__/apps/create-app-flow.test.tsx +++ b/web/__tests__/apps/create-app-flow.test.tsx @@ -75,27 +75,36 @@ vi.mock('@/service/tag', () => ({ fetchTagList: vi.fn().mockResolvedValue([]), })) -vi.mock('@/service/apps', () => ({ - fetchWorkflowOnlineUsers: vi.fn().mockResolvedValue({}), -})) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useInfiniteQuery: () => ({ + data: { pages: mockPages }, + isLoading: mockIsLoading, + isFetching: mockIsFetching, + isFetchingNextPage: false, + fetchNextPage: mockFetchNextPage, + hasNextPage: false, + error: null, + refetch: mockRefetch, + }), + } +}) vi.mock('@/service/use-apps', () => ({ - useInfiniteAppList: () => ({ - data: { pages: mockPages }, - isLoading: mockIsLoading, - isFetching: mockIsFetching, - isFetchingNextPage: false, - fetchNextPage: mockFetchNextPage, - hasNextPage: false, - error: null, - refetch: mockRefetch, - }), useDeleteAppMutation: () => ({ mutateAsync: vi.fn(), isPending: false, }), })) +vi.mock('@/app/components/apps/hooks/use-workflow-online-users', () => ({ + useWorkflowOnlineUsers: () => ({ + onlineUsersMap: {}, + }), +})) + vi.mock('@/hooks/use-pay', () => ({ CheckModal: () => null, })) diff --git a/web/app/account/(commonLayout)/account-page/index.tsx b/web/app/account/(commonLayout)/account-page/index.tsx index 09c083b60b..75d4e5afa8 100644 --- a/web/app/account/(commonLayout)/account-page/index.tsx +++ b/web/app/account/(commonLayout)/account-page/index.tsx @@ -7,7 +7,7 @@ import { toast } from '@langgenius/dify-ui/toast' import { RiGraduationCapFill, } from '@remixicon/react' -import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query' +import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query' import { useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' @@ -16,9 +16,9 @@ import PremiumBadge from '@/app/components/base/premium-badge' import Collapse from '@/app/components/header/account-setting/collapse' import { IS_CE_EDITION, validPassword } from '@/config' import { useProviderContext } from '@/context/provider-context' +import { consoleQuery } from '@/service/client' import { updateUserProfile } from '@/service/common' import { systemFeaturesQueryOptions } from '@/service/system-features' -import { useAppList } from '@/service/use-apps' import { commonQueryKeys, userProfileQueryOptions } from '@/service/use-common' import DeleteAccount from '../delete-account' @@ -35,7 +35,15 @@ const descriptionClassName = ` export default function AccountPage() { const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) - const { data: appList } = useAppList({ page: 1, limit: 100, name: '' }) + const { data: appList } = useQuery(consoleQuery.apps.list.queryOptions({ + input: { + query: { + page: 1, + limit: 100, + name: '', + }, + }, + })) const apps = appList?.data || [] const queryClient = useQueryClient() // Cache is warmed by AppContextProvider's useSuspenseQuery; this hits cache synchronously. @@ -129,7 +137,7 @@ export default function AccountPage() { } const renderAppItem = (item: IItem) => { - const { icon, icon_background, icon_type, icon_url } = item as any + const { icon, icon_background, icon_type, icon_url } = item as IItem & Pick return (
diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index c3ce96255a..9d1b39ef06 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -7,6 +7,11 @@ import { AppModeEnum } from '@/types/app' import List from '../list' +const mockAppListInfiniteOptions = vi.hoisted(() => vi.fn((options: unknown) => options)) +const mockUseWorkflowOnlineUsers = vi.hoisted(() => vi.fn((_options: unknown) => ({ + onlineUsersMap: {}, +}))) + const mockReplace = vi.fn() const mockRouter = { replace: mockReplace } vi.mock('@/next/navigation', () => ({ @@ -14,6 +19,22 @@ vi.mock('@/next/navigation', () => ({ useSearchParams: () => new URLSearchParams(''), })) +vi.mock('@/service/client', () => ({ + consoleClient: { + systemFeatures: vi.fn(), + }, + consoleQuery: { + apps: { + list: { + infiniteOptions: (options: unknown) => mockAppListInfiniteOptions(options), + }, + }, + systemFeatures: { + queryKey: () => ['console', 'systemFeatures'], + }, + }, +})) + const mockIsCurrentWorkspaceEditor = vi.fn(() => true) const mockIsCurrentWorkspaceDatasetOperator = vi.fn(() => false) vi.mock('@/context/app-context', () => ({ @@ -45,12 +66,17 @@ vi.mock('../hooks/use-dsl-drag-drop', () => ({ }, })) +vi.mock('../hooks/use-workflow-online-users', () => ({ + useWorkflowOnlineUsers: (options: unknown) => mockUseWorkflowOnlineUsers(options), +})) + const mockRefetch = vi.fn() const mockFetchNextPage = vi.fn() const mockServiceState = { error: null as Error | null, hasNextPage: false, + isFetching: false, isLoading: false, isFetchingNextPage: false, } @@ -89,16 +115,24 @@ const defaultAppData = { }], } +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useInfiniteQuery: () => ({ + data: defaultAppData, + isLoading: mockServiceState.isLoading, + isFetching: mockServiceState.isFetching, + isFetchingNextPage: mockServiceState.isFetchingNextPage, + fetchNextPage: mockFetchNextPage, + hasNextPage: mockServiceState.hasNextPage, + error: mockServiceState.error, + refetch: mockRefetch, + }), + } +}) + vi.mock('@/service/use-apps', () => ({ - useInfiniteAppList: () => ({ - data: defaultAppData, - isLoading: mockServiceState.isLoading, - isFetchingNextPage: mockServiceState.isFetchingNextPage, - fetchNextPage: mockFetchNextPage, - hasNextPage: mockServiceState.hasNextPage, - error: mockServiceState.error, - refetch: mockRefetch, - }), useDeleteAppMutation: () => ({ mutateAsync: vi.fn(), isPending: false, @@ -194,6 +228,11 @@ const renderList = (searchParams = '') => { return renderWithNuqs(, { searchParams }) } +type AppListInfiniteOptions = { + input: (pageParam: number) => { query: Record } + getNextPageParam: (lastPage: { has_more: boolean, page: number }) => number | undefined +} + describe('List', () => { beforeEach(() => { vi.clearAllMocks() @@ -212,6 +251,7 @@ describe('List', () => { mockQueryState.tagIDs = [] mockQueryState.keywords = '' mockQueryState.isCreatedByMe = false + mockUseWorkflowOnlineUsers.mockClear() intersectionCallback = null localStorage.clear() }) @@ -269,6 +309,15 @@ describe('List', () => { renderList() expect(screen.getByText('app.newApp.dropDSLToCreateApp'))!.toBeInTheDocument() }) + + it('should pass workflow app ids to online users hook', () => { + renderList() + + expect(mockUseWorkflowOnlineUsers).toHaveBeenCalledWith({ + appIds: ['app-2'], + enabled: expect.any(Boolean), + }) + }) }) describe('Tab Navigation', () => { @@ -323,6 +372,31 @@ describe('List', () => { }) }) + describe('App List Query', () => { + it('should build paged query input from active filters', () => { + mockQueryState.tagIDs = ['tag-1'] + mockQueryState.keywords = 'sales' + mockQueryState.isCreatedByMe = true + + renderList('?category=workflow') + + const options = mockAppListInfiniteOptions.mock.calls.at(-1)?.[0] as AppListInfiniteOptions + + expect(options.input(2)).toEqual({ + query: { + page: 2, + limit: 30, + name: 'sales', + tag_ids: ['tag-1'], + is_created_by_me: true, + mode: AppModeEnum.WORKFLOW, + }, + }) + expect(options.getNextPageParam({ has_more: true, page: 2 })).toBe(3) + expect(options.getNextPageParam({ has_more: false, page: 2 })).toBeUndefined() + }) + }) + describe('Tag Filter', () => { it('should render tag filter component', () => { renderList() diff --git a/web/app/components/apps/hooks/use-workflow-online-users.ts b/web/app/components/apps/hooks/use-workflow-online-users.ts new file mode 100644 index 0000000000..a1778306f3 --- /dev/null +++ b/web/app/components/apps/hooks/use-workflow-online-users.ts @@ -0,0 +1,49 @@ +import type { WorkflowOnlineUser, WorkflowOnlineUsersResponse } from '@/models/app' +import { skipToken, useQuery } from '@tanstack/react-query' +import { consoleQuery } from '@/service/client' + +type WorkflowOnlineUsersMap = Record + +type UseWorkflowOnlineUsersParams = { + appIds: string[] + enabled: boolean +} + +const normalizeWorkflowOnlineUsers = (response?: WorkflowOnlineUsersResponse): WorkflowOnlineUsersMap => { + const data = response?.data + + if (!data) + return {} + + if (Array.isArray(data)) { + return data.reduce((acc, item) => { + if (item?.app_id) + acc[item.app_id] = item.users || [] + return acc + }, {}) + } + + return Object.entries(data).reduce((acc, [appId, users]) => { + if (appId) + acc[appId] = users || [] + return acc + }, {}) +} + +export const useWorkflowOnlineUsers = ({ + appIds, + enabled, +}: UseWorkflowOnlineUsersParams) => { + const shouldFetch = enabled && appIds.length > 0 + const { data: onlineUsersMap = {} } = useQuery(consoleQuery.apps.workflowOnlineUsers.queryOptions({ + input: shouldFetch + ? { body: { app_ids: appIds } } + : skipToken, + select: normalizeWorkflowOnlineUsers, + refetchInterval: shouldFetch ? 10000 : false, + })) + + return { + onlineUsersMap, + } +} diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index b744fe77aa..728ef38ba5 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -1,9 +1,9 @@ 'use client' import type { FC } from 'react' -import type { WorkflowOnlineUser } from '@/models/app' +import type { AppListQuery } from '@/contract/console/apps' import { cn } from '@langgenius/dify-ui/cn' -import { useSuspenseQuery } from '@tanstack/react-query' +import { keepPreviousData, useInfiniteQuery, useSuspenseQuery } from '@tanstack/react-query' import { useDebounceFn } from 'ahooks' import { parseAsStringLiteral, useQueryState } from 'nuqs' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -17,9 +17,8 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { CheckModal } from '@/hooks/use-pay' import dynamic from '@/next/dynamic' -import { fetchWorkflowOnlineUsers } from '@/service/apps' +import { consoleQuery } from '@/service/client' import { systemFeaturesQueryOptions } from '@/service/system-features' -import { useInfiniteAppList } from '@/service/use-apps' import { AppModeEnum, AppModes } from '@/types/app' import AppCard from './app-card' import { AppCardSkeleton } from './app-card-skeleton' @@ -27,6 +26,7 @@ import Empty from './empty' import Footer from './footer' import useAppsQueryState from './hooks/use-apps-query-state' import { useDSLDragDrop } from './hooks/use-dsl-drag-drop' +import { useWorkflowOnlineUsers } from './hooks/use-workflow-online-users' import NewAppCard from './new-app-card' const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), { @@ -71,7 +71,6 @@ const List: FC = ({ const containerRef = useRef(null) const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false) const [droppedDSLFile, setDroppedDSLFile] = useState() - const [workflowOnlineUsersMap, setWorkflowOnlineUsersMap] = useState>({}) const setKeywords = useCallback((keywords: string) => { setQuery(prev => ({ ...prev, keywords })) }, [setQuery]) @@ -90,14 +89,14 @@ const List: FC = ({ enabled: isCurrentWorkspaceEditor, }) - const appListQueryParams = { + const appListQuery = useMemo(() => ({ page: 1, limit: 30, name: searchKeywords, - tag_ids: tagIDs, - is_created_by_me: isCreatedByMe, + ...(tagIDs.length ? { tag_ids: tagIDs } : {}), + ...(isCreatedByMe ? { is_created_by_me: isCreatedByMe } : {}), ...(activeTab !== 'all' ? { mode: activeTab } : {}), - } + }), [activeTab, isCreatedByMe, searchKeywords, tagIDs]) const { data, @@ -108,14 +107,27 @@ const List: FC = ({ hasNextPage, error, refetch, - } = useInfiniteAppList(appListQueryParams, { enabled: !isCurrentWorkspaceDatasetOperator }) + } = useInfiniteQuery({ + ...consoleQuery.apps.list.infiniteOptions({ + input: pageParam => ({ + query: { + ...appListQuery, + page: Number(pageParam), + }, + }), + getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined, + initialPageParam: 1, + placeholderData: keepPreviousData, + }), + enabled: !isCurrentWorkspaceDatasetOperator, + refetchInterval: systemFeatures.enable_collaboration_mode ? 10000 : false, + }) useEffect(() => { if (controlRefreshList > 0) { refetch() } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [controlRefreshList]) + }, [controlRefreshList, refetch]) const anchorRef = useRef(null) const options = [ @@ -187,53 +199,23 @@ const List: FC = ({ }, [isCreatedByMe, setQuery]) const pages = useMemo(() => data?.pages ?? [], [data?.pages]) + const apps = useMemo(() => pages.flatMap(({ data: pageApps }) => pageApps), [pages]) const workflowOnlineUserAppIds = useMemo(() => { const appIds = new Set() - pages.forEach(({ data: apps }) => { - apps.forEach((app) => { - if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT) - appIds.add(app.id) - }) + apps.forEach((app) => { + if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT) + appIds.add(app.id) }) return Array.from(appIds) - }, [pages]) + }, [apps]) - const refreshWorkflowOnlineUsers = useCallback(async () => { - if (!systemFeatures.enable_collaboration_mode) { - setWorkflowOnlineUsersMap({}) - return - } - - if (!workflowOnlineUserAppIds.length) { - setWorkflowOnlineUsersMap({}) - return - } - - try { - const onlineUsersMap = await fetchWorkflowOnlineUsers({ appIds: workflowOnlineUserAppIds }) - setWorkflowOnlineUsersMap(onlineUsersMap) - } - catch { - setWorkflowOnlineUsersMap({}) - } - }, [systemFeatures.enable_collaboration_mode, workflowOnlineUserAppIds]) - - useEffect(() => { - void refreshWorkflowOnlineUsers() - }, [refreshWorkflowOnlineUsers]) - - useEffect(() => { - if (!systemFeatures.enable_collaboration_mode) - return - - const timer = window.setInterval(() => { - void refetch() - void refreshWorkflowOnlineUsers() - }, 10000) - - return () => window.clearInterval(timer) - }, [refetch, refreshWorkflowOnlineUsers, systemFeatures.enable_collaboration_mode]) + const { + onlineUsersMap: workflowOnlineUsersMap, + } = useWorkflowOnlineUsers({ + appIds: workflowOnlineUserAppIds, + enabled: systemFeatures.enable_collaboration_mode, + }) const hasAnyApp = (pages[0]?.total ?? 0) > 0 // Show skeleton during initial load or when refetching with no previous data @@ -288,24 +270,18 @@ const List: FC = ({ className={cn(!hasAnyApp && 'z-10')} /> )} - {(() => { - if (showSkeleton) - return - - if (hasAnyApp) { - return pages.flatMap(({ data: apps }) => apps).map(app => ( - - )) - } - - // No apps - show empty state - return - })()} + {showSkeleton + ? + : hasAnyApp + ? apps.map(app => ( + + )) + : } {isFetchingNextPage && ( )} diff --git a/web/app/components/header/app-nav/__tests__/index.spec.tsx b/web/app/components/header/app-nav/__tests__/index.spec.tsx index 03f8edfacf..c2b0207b83 100644 --- a/web/app/components/header/app-nav/__tests__/index.spec.tsx +++ b/web/app/components/header/app-nav/__tests__/index.spec.tsx @@ -1,12 +1,14 @@ +import { useInfiniteQuery } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { useStore as useAppStore } from '@/app/components/app/store' import { useAppContext } from '@/context/app-context' import { useParams } from '@/next/navigation' -import { useInfiniteAppList } from '@/service/use-apps' import { AppModeEnum } from '@/types/app' import AppNav from '../index' +const mockAppListInfiniteOptions = vi.hoisted(() => vi.fn((options: unknown) => options)) + vi.mock('@/next/navigation', () => ({ useParams: vi.fn(), })) @@ -25,10 +27,24 @@ vi.mock('@/app/components/app/store', () => ({ useStore: vi.fn(), })) -vi.mock('@/service/use-apps', () => ({ - useInfiniteAppList: vi.fn(), +vi.mock('@/service/client', () => ({ + consoleQuery: { + apps: { + list: { + infiniteOptions: (options: unknown) => mockAppListInfiniteOptions(options), + }, + }, + }, })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useInfiniteQuery: vi.fn(), + } +}) + vi.mock('@/app/components/app/create-app-dialog', () => ({ default: ({ show, onClose, onSuccess }: { show: boolean, onClose: () => void, onSuccess: () => void }) => show @@ -130,8 +146,12 @@ const mockAppData = [ const mockUseParams = vi.mocked(useParams) const mockUseAppContext = vi.mocked(useAppContext) const mockUseAppStore = vi.mocked(useAppStore) -const mockUseInfiniteAppList = vi.mocked(useInfiniteAppList) +const mockUseInfiniteQuery = vi.mocked(useInfiniteQuery) let mockAppDetail: { id: string, name: string } | null = null +type AppListInfiniteOptions = { + input: (pageParam: number) => { query: { page: number, limit: number, name: string } } + getNextPageParam: (lastPage: { has_more: boolean, page: number }) => number | undefined +} const setupDefaultMocks = (options?: { hasNextPage?: boolean @@ -146,13 +166,13 @@ const setupDefaultMocks = (options?: { mockUseParams.mockReturnValue({ appId: 'app-1' } as ReturnType) mockUseAppContext.mockReturnValue({ isCurrentWorkspaceEditor: options?.isEditor ?? false } as ReturnType) mockUseAppStore.mockImplementation((selector: unknown) => (selector as (state: { appDetail: { id: string, name: string } | null }) => unknown)({ appDetail: mockAppDetail })) - mockUseInfiniteAppList.mockReturnValue({ + mockUseInfiniteQuery.mockReturnValue({ data: { pages: [{ data: options?.appData ?? mockAppData }] }, fetchNextPage, hasNextPage: options?.hasNextPage ?? false, isFetchingNextPage: false, refetch, - } as ReturnType) + } as ReturnType) return { refetch, fetchNextPage } } @@ -164,6 +184,23 @@ describe('AppNav', () => { setupDefaultMocks() }) + it('should configure paged app list query options', () => { + setupDefaultMocks() + render() + + const options = mockAppListInfiniteOptions.mock.calls.at(-1)?.[0] as AppListInfiniteOptions + + expect(options.input(3)).toEqual({ + query: { + page: 3, + limit: 30, + name: '', + }, + }) + expect(options.getNextPageParam({ has_more: true, page: 3 })).toBe(4) + expect(options.getNextPageParam({ has_more: false, page: 3 })).toBeUndefined() + }) + it('should build editor links and update app name when app detail changes', async () => { setupDefaultMocks({ isEditor: true, @@ -282,13 +319,13 @@ describe('AppNav', () => { // Arrange setupDefaultMocks() mockUseParams.mockReturnValue({} as ReturnType) - mockUseInfiniteAppList.mockReturnValue({ + mockUseInfiniteQuery.mockReturnValue({ data: undefined, fetchNextPage: vi.fn(), hasNextPage: false, isFetchingNextPage: false, refetch: vi.fn(), - } as unknown as ReturnType) + } as unknown as ReturnType) // Act render() diff --git a/web/app/components/header/app-nav/index.tsx b/web/app/components/header/app-nav/index.tsx index 2e7a77a891..a65477e4df 100644 --- a/web/app/components/header/app-nav/index.tsx +++ b/web/app/components/header/app-nav/index.tsx @@ -1,19 +1,19 @@ 'use client' import type { NavItem } from '../nav/nav-selector' +import type { AppListQuery } from '@/contract/console/apps' import { RiRobot2Fill, RiRobot2Line, } from '@remixicon/react' -import { flatten } from 'es-toolkit/compat' -import { produce } from 'immer' -import { useCallback, useEffect, useState } from 'react' +import { keepPreviousData, useInfiniteQuery } from '@tanstack/react-query' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' import { useAppContext } from '@/context/app-context' import dynamic from '@/next/dynamic' import { useParams } from '@/next/navigation' -import { useInfiniteAppList } from '@/service/use-apps' +import { consoleQuery } from '@/service/client' import { AppModeEnum } from '@/types/app' import Nav from '../nav' @@ -21,6 +21,22 @@ const CreateAppTemplateDialog = dynamic(() => import('@/app/components/app/creat const CreateAppModal = dynamic(() => import('@/app/components/app/create-app-modal'), { ssr: false }) const CreateFromDSLModal = dynamic(() => import('@/app/components/app/create-from-dsl-modal'), { ssr: false }) +const appNavListQuery = { + page: 1, + limit: 30, + name: '', +} satisfies AppListQuery + +const getAppLink = (isCurrentWorkspaceEditor: boolean, appId: string, appMode: AppModeEnum) => { + if (!isCurrentWorkspaceEditor) + return `/app/${appId}/overview` + + if (appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT) + return `/app/${appId}/workflow` + + return `/app/${appId}/configuration` +} + const AppNav = () => { const { t } = useTranslation() const { appId } = useParams() @@ -29,7 +45,6 @@ const AppNav = () => { const [showNewAppDialog, setShowNewAppDialog] = useState(false) const [showNewAppTemplateDialog, setShowNewAppTemplateDialog] = useState(false) const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false) - const [navItems, setNavItems] = useState([]) const { data: appsData, @@ -37,11 +52,20 @@ const AppNav = () => { hasNextPage, isFetchingNextPage, refetch, - } = useInfiniteAppList({ - page: 1, - limit: 30, - name: '', - }, { enabled: !!appId }) + } = useInfiniteQuery({ + ...consoleQuery.apps.list.infiniteOptions({ + input: pageParam => ({ + query: { + ...appNavListQuery, + page: Number(pageParam), + }, + }), + getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined, + initialPageParam: 1, + placeholderData: keepPreviousData, + }), + enabled: !!appId, + }) const handleLoadMore = useCallback(() => { if (hasNextPage) @@ -57,48 +81,20 @@ const AppNav = () => { setShowCreateFromDSLModal(true) } - useEffect(() => { - if (appsData) { - const appItems = flatten((appsData.pages ?? []).map(appData => appData.data)) - const navItems = appItems.map((app) => { - const link = ((isCurrentWorkspaceEditor, app) => { - if (!isCurrentWorkspaceEditor) { - return `/app/${app.id}/overview` - } - else { - if (app.mode === AppModeEnum.WORKFLOW || app.mode === AppModeEnum.ADVANCED_CHAT) - return `/app/${app.id}/workflow` - else - return `/app/${app.id}/configuration` - } - })(isCurrentWorkspaceEditor, app) - return { - id: app.id, - icon_type: app.icon_type, - icon: app.icon, - icon_background: app.icon_background, - icon_url: app.icon_url, - name: app.name, - mode: app.mode, - link, - } - }) - setNavItems(navItems as any) - } - }, [appsData, isCurrentWorkspaceEditor, setNavItems]) + const navItems = useMemo(() => { + const appItems = appsData?.pages.flatMap(appData => appData.data) ?? [] - // update current app name - useEffect(() => { - if (appDetail) { - const newNavItems = produce(navItems, (draft: NavItem[]) => { - navItems.forEach((app, index) => { - if (app.id === appDetail.id) - draft[index]!.name = appDetail.name - }) - }) - setNavItems(newNavItems) - } - }, [appDetail, navItems]) + return appItems.map(app => ({ + id: app.id, + icon_type: app.icon_type, + icon: app.icon, + icon_background: app.icon_background, + icon_url: app.icon_url, + name: appDetail?.id === app.id ? appDetail.name : app.name, + mode: app.mode, + link: getAppLink(isCurrentWorkspaceEditor, app.id, app.mode), + })) + }, [appDetail?.id, appDetail?.name, appsData?.pages, isCurrentWorkspaceEditor]) return ( <> diff --git a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx index d67be38ab4..38b5324fa7 100644 --- a/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/app-selector/__tests__/index.spec.tsx @@ -15,6 +15,8 @@ import AppSelector from '../index' // ==================== Mock Setup ==================== +const mockAppListInfiniteOptions = vi.hoisted(() => vi.fn((options: unknown) => options)) + // Mock IntersectionObserver globally using class syntax let intersectionObserverCallback: IntersectionObserverCallback | null = null const mockIntersectionObserver = { @@ -163,19 +165,36 @@ const getAppDetailData = (appId: string) => { } vi.mock('@/service/use-apps', () => ({ - useInfiniteAppList: () => ({ - data: mockAppListData, - isLoading: mockIsLoading, - isFetchingNextPage: mockIsFetchingNextPage, - fetchNextPage: mockFetchNextPage, - hasNextPage: mockHasNextPage, - }), useAppDetail: (appId: string) => ({ data: getAppDetailData(appId), isFetching: mockAppDetailLoading, }), })) +vi.mock('@/service/client', () => ({ + consoleQuery: { + apps: { + list: { + infiniteOptions: (options: unknown) => mockAppListInfiniteOptions(options), + }, + }, + }, +})) + +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useInfiniteQuery: () => ({ + data: mockAppListData, + isLoading: mockIsLoading, + isFetchingNextPage: mockIsFetchingNextPage, + fetchNextPage: mockFetchNextPage, + hasNextPage: mockHasNextPage, + }), + } +}) + // Allow configurable mock data for useAppWorkflow let mockWorkflowData: Record | undefined | null let mockWorkflowLoading = false @@ -323,6 +342,11 @@ const renderWithQueryClient = (ui: React.ReactElement) => { ) } +type AppSelectorInfiniteOptions = { + input: (pageParam: number) => { query: Record } + getNextPageParam: (lastPage: { has_more: boolean, page: number }) => number | undefined +} + // Mock data factories const createMockApp = (overrides: Record = {}): App => ({ id: 'app-1', @@ -1539,6 +1563,22 @@ describe('AppSelector', () => { expect(screen.getByText('app.appSelector.placeholder'))!.toBeInTheDocument() }) + it('should configure paged app list query options', () => { + renderWithQueryClient() + + const options = mockAppListInfiniteOptions.mock.calls.at(-1)?.[0] as AppSelectorInfiniteOptions + + expect(options.input(4)).toEqual({ + query: { + page: 4, + limit: 20, + name: '', + }, + }) + expect(options.getNextPageParam({ has_more: true, page: 4 })).toBe(5) + expect(options.getNextPageParam({ has_more: false, page: 4 })).toBeUndefined() + }) + it('should show selected app info when value is provided', () => { renderWithQueryClient( = ({ const [isShow, setIsShow] = useState(false) const [searchText, setSearchText] = useState('') + const appListQuery = useMemo(() => ({ + page: 1, + limit: PAGE_SIZE, + name: searchText, + }), [searchText]) + const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage, - } = useInfiniteAppList({ - page: 1, - limit: PAGE_SIZE, - name: searchText, + } = useInfiniteQuery({ + ...consoleQuery.apps.list.infiniteOptions({ + input: pageParam => ({ + query: { + ...appListQuery, + page: Number(pageParam), + }, + }), + getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined, + initialPageParam: 1, + placeholderData: keepPreviousData, + }), }) const displayedApps = useMemo(() => { diff --git a/web/app/components/workflow/comment/comment-icon.spec.tsx b/web/app/components/workflow/comment/comment-icon.spec.tsx index aee8c64fa3..eeca5bb4e6 100644 --- a/web/app/components/workflow/comment/comment-icon.spec.tsx +++ b/web/app/components/workflow/comment/comment-icon.spec.tsx @@ -1,4 +1,4 @@ -import type { WorkflowCommentList } from '@/service/workflow-comment' +import type { WorkflowCommentList } from '@/contract/console/workflow-comment' import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { CommentIcon } from './comment-icon' diff --git a/web/app/components/workflow/comment/comment-icon.tsx b/web/app/components/workflow/comment/comment-icon.tsx index 7f005f3465..7270cd3ac9 100644 --- a/web/app/components/workflow/comment/comment-icon.tsx +++ b/web/app/components/workflow/comment/comment-icon.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC, PointerEvent as ReactPointerEvent } from 'react' -import type { WorkflowCommentList } from '@/service/workflow-comment' +import type { WorkflowCommentList } from '@/contract/console/workflow-comment' import { memo, useCallback, useMemo, useRef, useState } from 'react' import { useReactFlow, useViewport } from 'reactflow' import { UserAvatarList } from '@/app/components/base/user-avatar-list' diff --git a/web/app/components/workflow/comment/comment-preview.spec.tsx b/web/app/components/workflow/comment/comment-preview.spec.tsx index d411c67ecd..a83303cb0d 100644 --- a/web/app/components/workflow/comment/comment-preview.spec.tsx +++ b/web/app/components/workflow/comment/comment-preview.spec.tsx @@ -1,9 +1,9 @@ -import type { WorkflowCommentList } from '@/service/workflow-comment' +import type { WorkflowCommentList } from '@/contract/console/workflow-comment' import { fireEvent, render, screen } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import CommentPreview from './comment-preview' -type UserProfile = WorkflowCommentList['created_by_account'] +type UserProfile = NonNullable const mockSetHovering = vi.fn() let capturedUsers: UserProfile[] = [] diff --git a/web/app/components/workflow/comment/comment-preview.tsx b/web/app/components/workflow/comment/comment-preview.tsx index 5985ed848b..6aadb522a9 100644 --- a/web/app/components/workflow/comment/comment-preview.tsx +++ b/web/app/components/workflow/comment/comment-preview.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC } from 'react' -import type { WorkflowCommentList } from '@/service/workflow-comment' +import type { WorkflowCommentList } from '@/contract/console/workflow-comment' import { memo, useEffect, useMemo } from 'react' import { UserAvatarList } from '@/app/components/base/user-avatar-list' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' @@ -15,6 +15,7 @@ type CommentPreviewProps = { const CommentPreview: FC = ({ comment, onClick }) => { const { formatTimeFromNow } = useFormatTimeFromNow() const setCommentPreviewHovering = useStore(s => s.setCommentPreviewHovering) + const authorName = comment.created_by_account?.name ?? '' const participants = useMemo(() => { const list = comment.participants ?? [] const author = comment.created_by_account @@ -44,7 +45,7 @@ const CommentPreview: FC = ({ comment, onClick }) => {
-
{comment.created_by_account.name}
+
{authorName}
{formatTimeFromNow(comment.updated_at * 1000)}
diff --git a/web/app/components/workflow/comment/mention-input.spec.tsx b/web/app/components/workflow/comment/mention-input.spec.tsx index 49fc7b6e87..ce53880595 100644 --- a/web/app/components/workflow/comment/mention-input.spec.tsx +++ b/web/app/components/workflow/comment/mention-input.spec.tsx @@ -1,5 +1,5 @@ -import type { UserProfile } from '@/service/workflow-comment' -import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import type { UserProfile } from '@/contract/console/workflow-comment' +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { useState } from 'react' import { MentionInput } from './mention-input' @@ -30,8 +30,12 @@ vi.mock('@/next/navigation', () => ({ useParams: () => ({ appId: 'app-1' }), })) -vi.mock('@/service/workflow-comment', () => ({ - fetchMentionableUsers: (...args: unknown[]) => mockFetchMentionableUsers(...args), +vi.mock('@/service/client', () => ({ + consoleClient: { + workflowComments: { + mentionUsers: (...args: unknown[]) => mockFetchMentionableUsers(...args), + }, + }, })) vi.mock('../store', () => ({ @@ -80,7 +84,7 @@ describe('MentionInput', () => { vi.clearAllMocks() mentionStoreState.mentionableUsersCache = {} mentionStoreState.mentionableUsersLoading = {} - mockFetchMentionableUsers.mockResolvedValue(mentionUsers) + mockFetchMentionableUsers.mockResolvedValue({ users: mentionUsers }) }) it('loads mentionable users when cache is empty', async () => { @@ -93,7 +97,9 @@ describe('MentionInput', () => { ) await waitFor(() => { - expect(mockFetchMentionableUsers).toHaveBeenCalledWith('app-1') + expect(mockFetchMentionableUsers).toHaveBeenCalledWith({ + params: { appId: 'app-1' }, + }) }) expect(mockSetMentionableUsersLoading).toHaveBeenCalledWith('app-1', true) @@ -148,4 +154,35 @@ describe('MentionInput', () => { expect(onSubmit).toHaveBeenCalledWith('updated reply', []) }) }) + + it('focuses the textarea at the end when autoFocus is enabled', () => { + vi.useFakeTimers() + try { + mentionStoreState.mentionableUsersCache['app-1'] = mentionUsers + + const { unmount } = render( + , + ) + + const textarea = screen.getByPlaceholderText('workflow.comments.placeholder.add') as HTMLTextAreaElement + + act(() => { + vi.runOnlyPendingTimers() + }) + + expect(document.activeElement).toBe(textarea) + expect(textarea.selectionStart).toBe(5) + expect(textarea.selectionEnd).toBe(5) + + unmount() + } + finally { + vi.useRealTimers() + } + }) }) diff --git a/web/app/components/workflow/comment/mention-input.tsx b/web/app/components/workflow/comment/mention-input.tsx index b6a7caa055..59f3bf1249 100644 --- a/web/app/components/workflow/comment/mention-input.tsx +++ b/web/app/components/workflow/comment/mention-input.tsx @@ -1,7 +1,7 @@ 'use client' import type { ReactNode } from 'react' -import type { UserProfile } from '@/service/workflow-comment' +import type { UserProfile } from '@/contract/console/workflow-comment' import { Avatar } from '@langgenius/dify-ui/avatar' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' @@ -22,7 +22,7 @@ import { useTranslation } from 'react-i18next' import Textarea from 'react-textarea-autosize' import EnterKey from '@/app/components/base/icons/src/public/common/EnterKey' import { useParams } from '@/next/navigation' -import { fetchMentionableUsers } from '@/service/workflow-comment' +import { consoleClient } from '@/service/client' import { useStore, useWorkflowStore } from '../store' type MentionInputProps = { @@ -38,6 +38,8 @@ type MentionInputProps = { autoFocus?: boolean } +const EMPTY_USERS: UserProfile[] = [] + const MentionInputInner = forwardRef(({ value, onChange, @@ -66,7 +68,7 @@ const MentionInputInner = forwardRef(({ const mentionUsersFromStore = useStore(state => ( appId ? state.mentionableUsersCache[appId] : undefined )) - const mentionUsers = mentionUsersFromStore ?? [] + const mentionUsers = useMemo(() => mentionUsersFromStore ?? EMPTY_USERS, [mentionUsersFromStore]) const [showMentionDropdown, setShowMentionDropdown] = useState(false) const [mentionQuery, setMentionQuery] = useState('') @@ -163,8 +165,10 @@ const MentionInputInner = forwardRef(({ state.setMentionableUsersLoading(appId, true) try { - const users = await fetchMentionableUsers(appId) - workflowStore.getState().setMentionableUsersCache(appId, users) + const response = await consoleClient.workflowComments.mentionUsers({ + params: { appId }, + }) + workflowStore.getState().setMentionableUsersCache(appId, response.users) } catch (error) { console.error('Failed to load mentionable users:', error) @@ -495,14 +499,17 @@ const MentionInputInner = forwardRef(({ }, [value, resetMentionState]) useEffect(() => { - if (autoFocus && textareaRef.current) { - const textarea = textareaRef.current - setTimeout(() => { - textarea.focus() - const length = textarea.value.length - textarea.setSelectionRange(length, length) - }, 0) - } + if (!autoFocus || !textareaRef.current) + return + + const textarea = textareaRef.current + const timeout = window.setTimeout(() => { + textarea.focus() + const length = textarea.value.length + textarea.setSelectionRange(length, length) + }, 0) + + return () => window.clearTimeout(timeout) }, [autoFocus]) return ( diff --git a/web/app/components/workflow/comment/thread.spec.tsx b/web/app/components/workflow/comment/thread.spec.tsx index 58bb99cff0..2aa36142e9 100644 --- a/web/app/components/workflow/comment/thread.spec.tsx +++ b/web/app/components/workflow/comment/thread.spec.tsx @@ -1,4 +1,4 @@ -import type { WorkflowCommentDetail } from '@/service/workflow-comment' +import type { WorkflowCommentDetail } from '@/contract/console/workflow-comment' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { CommentThread } from './thread' diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index 34e0092372..071cc48462 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -1,7 +1,7 @@ 'use client' import type { FC, ReactNode } from 'react' -import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment' +import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/contract/console/workflow-comment' import { Avatar, AvatarFallback, AvatarImage, AvatarRoot } from '@langgenius/dify-ui/avatar' import { cn } from '@langgenius/dify-ui/cn' import { diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts index b2edfa5234..19eb1ad0e8 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow-comment.spec.ts @@ -1,4 +1,4 @@ -import type { WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment' +import type { WorkflowCommentDetail, WorkflowCommentList } from '@/contract/console/workflow-comment' import { act, waitFor } from '@testing-library/react' import { createTestQueryClient, seedSystemFeatures } from '@/__tests__/utils/mock-system-features' import { renderWorkflowHook } from '../../__tests__/workflow-test-env' @@ -52,16 +52,30 @@ vi.mock('@/context/app-context', () => ({ }), })) -vi.mock('@/service/workflow-comment', () => ({ - createWorkflowComment: (...args: unknown[]) => mockCreateWorkflowComment(...args), - createWorkflowCommentReply: (...args: unknown[]) => mockCreateWorkflowCommentReply(...args), - deleteWorkflowComment: (...args: unknown[]) => mockDeleteWorkflowComment(...args), - deleteWorkflowCommentReply: (...args: unknown[]) => mockDeleteWorkflowCommentReply(...args), - fetchWorkflowComment: (...args: unknown[]) => mockFetchWorkflowComment(...args), - fetchWorkflowComments: (...args: unknown[]) => mockFetchWorkflowComments(...args), - resolveWorkflowComment: (...args: unknown[]) => mockResolveWorkflowComment(...args), - updateWorkflowComment: (...args: unknown[]) => mockUpdateWorkflowComment(...args), - updateWorkflowCommentReply: (...args: unknown[]) => mockUpdateWorkflowCommentReply(...args), +vi.mock('@/service/client', () => ({ + consoleClient: { + systemFeatures: () => ({ + enable_collaboration_mode: globalFeatureState.enableCollaboration, + }), + workflowComments: { + create: (...args: unknown[]) => mockCreateWorkflowComment(...args), + delete: (...args: unknown[]) => mockDeleteWorkflowComment(...args), + detail: (...args: unknown[]) => mockFetchWorkflowComment(...args), + list: (...args: unknown[]) => mockFetchWorkflowComments(...args), + resolve: (...args: unknown[]) => mockResolveWorkflowComment(...args), + update: (...args: unknown[]) => mockUpdateWorkflowComment(...args), + replies: { + create: (...args: unknown[]) => mockCreateWorkflowCommentReply(...args), + delete: (...args: unknown[]) => mockDeleteWorkflowCommentReply(...args), + update: (...args: unknown[]) => mockUpdateWorkflowCommentReply(...args), + }, + }, + }, + consoleQuery: { + systemFeatures: { + queryKey: () => ['console', 'systemFeatures'], + }, + }, })) vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({ @@ -127,25 +141,27 @@ describe('useWorkflowComment', () => { commentsUpdateState.handler = undefined globalFeatureState.enableCollaboration = true - mockFetchWorkflowComments.mockResolvedValue([]) + mockFetchWorkflowComments.mockResolvedValue({ data: [] }) mockFetchWorkflowComment.mockResolvedValue(baseCommentDetail()) mockCreateWorkflowComment.mockResolvedValue({ id: 'comment-2', - created_at: '1700000000', + created_at: 1700000000, }) mockUpdateWorkflowComment.mockResolvedValue({}) }) it('loads comment list on mount when collaboration is enabled', async () => { const comment = baseComment() - mockFetchWorkflowComments.mockResolvedValue([comment]) + mockFetchWorkflowComments.mockResolvedValue({ data: [comment] }) const { store } = renderWorkflowHook(() => useWorkflowComment(), { queryClient: createSeededQueryClient(), }) await waitFor(() => { - expect(mockFetchWorkflowComments).toHaveBeenCalledWith('app-1') + expect(mockFetchWorkflowComments).toHaveBeenCalledWith({ + params: { appId: 'app-1' }, + }) }) expect(store.getState().comments).toEqual([comment]) @@ -186,11 +202,14 @@ describe('useWorkflowComment', () => { await result.current.handleCommentSubmit('new message', ['user-2']) }) - expect(mockCreateWorkflowComment).toHaveBeenCalledWith('app-1', { - position_x: 10, - position_y: 20, - content: 'new message', - mentioned_user_ids: ['user-2'], + expect(mockCreateWorkflowComment).toHaveBeenCalledWith({ + params: { appId: 'app-1' }, + body: { + position_x: 10, + position_y: 20, + content: 'new message', + mentioned_user_ids: ['user-2'], + }, }) expect(mockEmitCommentsUpdate).toHaveBeenCalledWith('app-1') @@ -214,6 +233,79 @@ describe('useWorkflowComment', () => { expect(store.getState().isCommentQuickAdd).toBe(false) }) + it('normalizes numeric string timestamps when creating a comment', async () => { + mockCreateWorkflowComment.mockResolvedValue({ + id: 'comment-string-time', + created_at: '1700001234', + }) + + const { result, store } = renderWorkflowHook(() => useWorkflowComment(), { + queryClient: createSeededQueryClient(), + initialStoreState: { + comments: [], + pendingComment: { pageX: 100, pageY: 200, elementX: 10, elementY: 20 }, + isCommentQuickAdd: true, + }, + }) + + await act(async () => { + await result.current.handleCommentSubmit('new message') + }) + + expect(store.getState().comments[0]).toMatchObject({ + id: 'comment-string-time', + created_at: 1700001234, + updated_at: 1700001234, + }) + }) + + it('normalizes ISO timestamps and keeps unresolved mentions as null', async () => { + const createdAt = '2024-01-02T03:04:05.000Z' + mockCreateWorkflowComment.mockResolvedValue({ + id: 'comment-date-time', + created_at: createdAt, + }) + + const { result, store } = renderWorkflowHook(() => useWorkflowComment(), { + queryClient: createSeededQueryClient(), + initialStoreState: { + comments: [], + pendingComment: { pageX: 100, pageY: 200, elementX: 10, elementY: 20 }, + isCommentQuickAdd: true, + mentionableUsersCache: { + 'app-1': [{ + id: 'user-2', + name: 'Bob', + email: 'bob@example.com', + avatar_url: 'bob.png', + }], + }, + }, + }) + + await act(async () => { + await result.current.handleCommentSubmit('new message', ['missing-user']) + }) + + const expectedCreatedAt = Math.floor(Date.parse(createdAt) / 1000) + expect(store.getState().comments[0]).toMatchObject({ + id: 'comment-date-time', + created_at: expectedCreatedAt, + updated_at: expectedCreatedAt, + participants: [{ + id: 'user-1', + name: 'Alice', + email: 'alice@example.com', + avatar_url: 'alice.png', + }], + }) + expect(store.getState().commentDetailCache['comment-date-time']?.mentions).toEqual([{ + mentioned_user_id: 'missing-user', + mentioned_user_account: null, + reply_id: null, + }]) + }) + it('rolls back optimistic position update when API update fails', async () => { const comment = baseComment() const commentDetail = baseCommentDetail() @@ -235,10 +327,13 @@ describe('useWorkflowComment', () => { await result.current.handleCommentPositionUpdate(comment.id, { x: 300, y: 400 }) }) - expect(mockUpdateWorkflowComment).toHaveBeenCalledWith('app-1', comment.id, { - content: 'hello', - position_x: 300, - position_y: 400, + expect(mockUpdateWorkflowComment).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: comment.id }, + body: { + content: 'hello', + position_x: 300, + position_y: 400, + }, }) expect(mockEmitCommentsUpdate).not.toHaveBeenCalled() expect(store.getState().comments[0]).toMatchObject({ @@ -257,8 +352,8 @@ describe('useWorkflowComment', () => { ...baseCommentDetail(), content: 'updated by another user', } - mockFetchWorkflowComments.mockResolvedValue([comment]) - mockFetchWorkflowComment.mockResolvedValue({ data: detail }) + mockFetchWorkflowComments.mockResolvedValue({ data: [comment] }) + mockFetchWorkflowComment.mockResolvedValue(detail) const { unmount } = renderWorkflowHook(() => useWorkflowComment(), { queryClient: createSeededQueryClient(), @@ -276,7 +371,9 @@ describe('useWorkflowComment', () => { }) await waitFor(() => { - expect(mockFetchWorkflowComment).toHaveBeenCalledWith('app-1', comment.id) + expect(mockFetchWorkflowComment).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: comment.id }, + }) }) expect(mockFetchWorkflowComments).toHaveBeenCalledTimes(2) @@ -362,7 +459,7 @@ describe('useWorkflowComment', () => { position_x: 33, position_y: 55, } - mockFetchWorkflowComments.mockResolvedValue([commentB]) + mockFetchWorkflowComments.mockResolvedValue({ data: [commentB] }) mockFetchWorkflowComment.mockResolvedValue({ ...baseCommentDetail(), id: commentB.id, @@ -383,7 +480,9 @@ describe('useWorkflowComment', () => { await result.current.handleCommentResolve(commentA.id) }) - expect(mockResolveWorkflowComment).toHaveBeenCalledWith('app-1', commentA.id) + expect(mockResolveWorkflowComment).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: commentA.id }, + }) await act(async () => { await result.current.handleCommentReply(commentA.id, ' new reply ', ['user-2']) @@ -391,24 +490,154 @@ describe('useWorkflowComment', () => { await result.current.handleCommentReplyDelete(commentA.id, 'reply-1') }) - expect(mockCreateWorkflowCommentReply).toHaveBeenCalledWith('app-1', commentA.id, { - content: 'new reply', - mentioned_user_ids: ['user-2'], + expect(mockCreateWorkflowCommentReply).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: commentA.id }, + body: { + content: 'new reply', + mentioned_user_ids: ['user-2'], + }, }) - expect(mockUpdateWorkflowCommentReply).toHaveBeenCalledWith('app-1', commentA.id, 'reply-1', { - content: 'edited reply', - mentioned_user_ids: ['user-2'], + expect(mockUpdateWorkflowCommentReply).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: commentA.id, replyId: 'reply-1' }, + body: { + content: 'edited reply', + mentioned_user_ids: ['user-2'], + }, + }) + expect(mockDeleteWorkflowCommentReply).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: commentA.id, replyId: 'reply-1' }, }) - expect(mockDeleteWorkflowCommentReply).toHaveBeenCalledWith('app-1', commentA.id, 'reply-1') await act(async () => { await result.current.handleCommentDelete(commentA.id) }) - expect(mockDeleteWorkflowComment).toHaveBeenCalledWith('app-1', commentA.id) + expect(mockDeleteWorkflowComment).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: commentA.id }, + }) await waitFor(() => { expect(store.getState().activeCommentId).toBe(commentB.id) }) expect(mockEmitCommentsUpdate).toHaveBeenCalled() }) + + it('does not update a reply when the content is blank', async () => { + const { result } = renderWorkflowHook(() => useWorkflowComment(), { + queryClient: createSeededQueryClient(), + }) + + await act(async () => { + await result.current.handleCommentReplyUpdate('comment-1', 'reply-1', ' ') + }) + + expect(mockUpdateWorkflowCommentReply).not.toHaveBeenCalled() + }) + + it('resets reply submit loading when creation fails', async () => { + mockCreateWorkflowCommentReply.mockRejectedValueOnce(new Error('create reply failed')) + + const { result, store } = renderWorkflowHook(() => useWorkflowComment(), { + queryClient: createSeededQueryClient(), + initialStoreState: { + replySubmitting: false, + }, + }) + + await act(async () => { + await result.current.handleCommentReply('comment-1', 'new reply') + }) + + expect(mockCreateWorkflowCommentReply).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: 'comment-1' }, + body: { + content: 'new reply', + mentioned_user_ids: [], + }, + }) + expect(store.getState().replySubmitting).toBe(false) + }) + + it('resets reply update loading when update fails', async () => { + mockUpdateWorkflowCommentReply.mockRejectedValueOnce(new Error('update failed')) + + const { result, store } = renderWorkflowHook(() => useWorkflowComment(), { + queryClient: createSeededQueryClient(), + initialStoreState: { + replyUpdating: false, + }, + }) + + await act(async () => { + await result.current.handleCommentReplyUpdate('comment-1', 'reply-1', 'updated reply') + }) + + expect(mockUpdateWorkflowCommentReply).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: 'comment-1', replyId: 'reply-1' }, + body: { + content: 'updated reply', + mentioned_user_ids: [], + }, + }) + expect(store.getState().replyUpdating).toBe(false) + }) + + it('resets reply delete loading when deletion fails', async () => { + mockDeleteWorkflowCommentReply.mockRejectedValueOnce(new Error('delete failed')) + + const { result, store } = renderWorkflowHook(() => useWorkflowComment(), { + queryClient: createSeededQueryClient(), + initialStoreState: { + activeCommentDetailLoading: false, + }, + }) + + await act(async () => { + await result.current.handleCommentReplyDelete('comment-1', 'reply-1') + }) + + expect(mockDeleteWorkflowCommentReply).toHaveBeenCalledWith({ + params: { appId: 'app-1', commentId: 'comment-1', replyId: 'reply-1' }, + }) + expect(store.getState().activeCommentDetailLoading).toBe(false) + }) + + it('ignores navigation when no active comment or active comment is absent from the list', () => { + const { result } = renderWorkflowHook(() => useWorkflowComment(), { + queryClient: createSeededQueryClient(), + initialStoreState: { + comments: [baseComment()], + }, + }) + + act(() => { + result.current.handleCommentNavigate('next') + }) + + expect(mockSetCenter).not.toHaveBeenCalled() + + act(() => { + result.current.handleCommentIconClick({ ...baseComment(), id: 'missing-comment' }) + }) + + mockSetCenter.mockClear() + + act(() => { + result.current.handleCommentNavigate('next') + }) + + expect(mockSetCenter).not.toHaveBeenCalled() + }) + + it('clears a pending comment when comment mode is left outside quick add', () => { + const { store } = renderWorkflowHook(() => useWorkflowComment(), { + queryClient: createSeededQueryClient(), + initialStoreState: { + controlMode: ControlMode.Pointer, + isCommentQuickAdd: false, + pendingComment: { pageX: 1, pageY: 2, elementX: 3, elementY: 4 }, + }, + }) + + expect(store.getState().pendingComment).toBeNull() + }) }) diff --git a/web/app/components/workflow/hooks/use-workflow-comment.ts b/web/app/components/workflow/hooks/use-workflow-comment.ts index cdd14ceef1..54be5325e5 100644 --- a/web/app/components/workflow/hooks/use-workflow-comment.ts +++ b/web/app/components/workflow/hooks/use-workflow-comment.ts @@ -1,24 +1,34 @@ -import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment' +import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/contract/console/workflow-comment' import { useSuspenseQuery } from '@tanstack/react-query' -import { useCallback, useEffect, useRef } from 'react' +import { useCallback, useEffect, useMemo, useRef } from 'react' import { useReactFlow } from 'reactflow' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { useAppContext } from '@/context/app-context' import { useParams } from '@/next/navigation' +import { consoleClient } from '@/service/client' import { systemFeaturesQueryOptions } from '@/service/system-features' -import { createWorkflowComment, createWorkflowCommentReply, deleteWorkflowComment, deleteWorkflowCommentReply, fetchWorkflowComment, fetchWorkflowComments, resolveWorkflowComment, updateWorkflowComment, updateWorkflowCommentReply } from '@/service/workflow-comment' import { useStore } from '../store' import { ControlMode } from '../types' const EMPTY_USERS: UserProfile[] = [] -type CommentDetailResponse = WorkflowCommentDetail | { data: WorkflowCommentDetail } -const getCommentDetail = (response: CommentDetailResponse): WorkflowCommentDetail => { - if ('data' in response) - return response.data - return response +const normalizeTimestamp = (value: number | string): number => { + if (typeof value === 'number') + return value + + const parsed = Number(value) + if (!Number.isNaN(parsed)) + return parsed + + return Math.floor(Date.parse(value) / 1000) } +const toCommentDetailPreview = (comment: WorkflowCommentList): WorkflowCommentDetail => ({ + ...comment, + replies: [], + mentions: [], +}) + export const useWorkflowComment = () => { const params = useParams() const appId = params.appId as string @@ -50,6 +60,10 @@ export const useWorkflowComment = () => { const mentionableUsers = useStore(state => ( appId ? state.mentionableUsersCache[appId] ?? EMPTY_USERS : EMPTY_USERS )) + const mentionableUserById = useMemo( + () => new Map(mentionableUsers.map(user => [user.id, user])), + [mentionableUsers], + ) const { userProfile } = useAppContext() const { data: isCollaborationEnabled } = useSuspenseQuery({ ...systemFeaturesQueryOptions(), @@ -70,8 +84,9 @@ export const useWorkflowComment = () => { if (!appId) return - const detailResponse = await fetchWorkflowComment(appId, commentId) as CommentDetailResponse - const detail = getCommentDetail(detailResponse) + const detail = await consoleClient.workflowComments.detail({ + params: { appId, commentId }, + }) commentDetailCacheRef.current = { ...commentDetailCacheRef.current, @@ -87,8 +102,10 @@ export const useWorkflowComment = () => { setCommentsLoading(true) try { - const commentsData = await fetchWorkflowComments(appId) - setComments(commentsData) + const response = await consoleClient.workflowComments.list({ + params: { appId }, + }) + setComments(response.data) } catch (error) { console.error('Failed to fetch comments:', error) @@ -133,17 +150,17 @@ export const useWorkflowComment = () => { y: pendingComment.pageY, }) - const newComment = await createWorkflowComment(appId, { - position_x: flowPosition.x, - position_y: flowPosition.y, - content, - mentioned_user_ids: mentionedUserIds, + const newComment = await consoleClient.workflowComments.create({ + params: { appId }, + body: { + position_x: flowPosition.x, + position_y: flowPosition.y, + content, + mentioned_user_ids: mentionedUserIds, + }, }) - const createdAt = Number(newComment.created_at) - const createdAtSeconds = Number.isNaN(createdAt) - ? Math.floor(Date.parse(newComment.created_at) / 1000) - : createdAt + const createdAtSeconds = normalizeTimestamp(newComment.created_at) const createdByAccount = { id: userProfile?.id ?? '', name: userProfile?.name ?? '', @@ -151,7 +168,7 @@ export const useWorkflowComment = () => { avatar_url: userProfile?.avatar_url || userProfile?.avatar || undefined, } const mentionedUsers = mentionedUserIds - .map(mentionedId => mentionableUsers.find(user => user.id === mentionedId)) + .map(mentionedId => mentionableUserById.get(mentionedId)) .filter((user): user is NonNullable => Boolean(user)) const uniqueParticipantsMap = new Map() if (createdByAccount.id) @@ -196,7 +213,7 @@ export const useWorkflowComment = () => { replies: [], mentions: mentionedUserIds.map(mentionedId => ({ mentioned_user_id: mentionedId, - mentioned_user_account: mentionableUsers.find(user => user.id === mentionedId) ?? null, + mentioned_user_account: mentionableUserById.get(mentionedId) ?? null, reply_id: null, })), } @@ -218,7 +235,7 @@ export const useWorkflowComment = () => { setPendingComment(null) setCommentQuickAdd(false) } - }, [appId, pendingComment, setPendingComment, setCommentQuickAdd, reactflow, comments, setComments, userProfile, setCommentDetailCache, mentionableUsers]) + }, [appId, pendingComment, setPendingComment, setCommentQuickAdd, reactflow, comments, setComments, userProfile, setCommentDetailCache, mentionableUserById]) const handleCommentCancel = useCallback(() => { setPendingComment(null) @@ -241,8 +258,8 @@ export const useWorkflowComment = () => { activeCommentIdRef.current = comment.id setActiveCommentId(comment.id) - const cachedDetail = commentDetailCacheRef.current[comment.id]! - setActiveComment(cachedDetail || comment) + const cachedDetail = commentDetailCacheRef.current[comment.id] + setActiveComment(cachedDetail ?? toCommentDetailPreview(comment)) const hasSelectedNode = reactflow.getNodes().some(node => node.data?.selected) const commentPanelWidth = controlMode === ControlMode.Comment ? 420 : 0 @@ -267,8 +284,9 @@ export const useWorkflowComment = () => { setActiveCommentLoading(!cachedDetail) try { - const detailResponse = await fetchWorkflowComment(appId, comment.id) as CommentDetailResponse - const detail = getCommentDetail(detailResponse) + const detail = await consoleClient.workflowComments.detail({ + params: { appId, commentId: comment.id }, + }) commentDetailCacheRef.current = { ...commentDetailCacheRef.current, @@ -304,7 +322,9 @@ export const useWorkflowComment = () => { setActiveCommentLoading(true) try { - await resolveWorkflowComment(appId, commentId) + await consoleClient.workflowComments.resolve({ + params: { appId, commentId }, + }) collaborationManager.emitCommentsUpdate(appId) @@ -325,7 +345,9 @@ export const useWorkflowComment = () => { setActiveCommentLoading(true) try { - await deleteWorkflowComment(appId, commentId) + await consoleClient.workflowComments.delete({ + params: { appId, commentId }, + }) collaborationManager.emitCommentsUpdate(appId) @@ -399,10 +421,13 @@ export const useWorkflowComment = () => { } try { - await updateWorkflowComment(appId, commentId, { - content: targetComment.content, - position_x: nextPosition.position_x, - position_y: nextPosition.position_y, + await consoleClient.workflowComments.update({ + params: { appId, commentId }, + body: { + content: targetComment.content, + position_x: nextPosition.position_x, + position_y: nextPosition.position_y, + }, }) collaborationManager.emitCommentsUpdate(appId) } @@ -443,11 +468,14 @@ export const useWorkflowComment = () => { return try { - await updateWorkflowComment(appId, commentId, { - content: trimmed, - position_x: positionX, - position_y: positionY, - mentioned_user_ids: mentionedUserIds, + await consoleClient.workflowComments.update({ + params: { appId, commentId }, + body: { + content: trimmed, + position_x: positionX, + position_y: positionY, + mentioned_user_ids: mentionedUserIds, + }, }) collaborationManager.emitCommentsUpdate(appId) @@ -469,7 +497,10 @@ export const useWorkflowComment = () => { setReplySubmitting(true) try { - await createWorkflowCommentReply(appId, commentId, { content: trimmed, mentioned_user_ids: mentionedUserIds }) + await consoleClient.workflowComments.replies.create({ + params: { appId, commentId }, + body: { content: trimmed, mentioned_user_ids: mentionedUserIds }, + }) collaborationManager.emitCommentsUpdate(appId) @@ -493,7 +524,10 @@ export const useWorkflowComment = () => { setReplyUpdating(true) try { - await updateWorkflowCommentReply(appId, commentId, replyId, { content: trimmed, mentioned_user_ids: mentionedUserIds }) + await consoleClient.workflowComments.replies.update({ + params: { appId, commentId, replyId }, + body: { content: trimmed, mentioned_user_ids: mentionedUserIds }, + }) collaborationManager.emitCommentsUpdate(appId) @@ -514,7 +548,9 @@ export const useWorkflowComment = () => { setActiveCommentLoading(true) try { - await deleteWorkflowCommentReply(appId, commentId, replyId) + await consoleClient.workflowComments.replies.delete({ + params: { appId, commentId, replyId }, + }) collaborationManager.emitCommentsUpdate(appId) diff --git a/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx b/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx index 990153d308..4ccf2b1061 100644 --- a/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx +++ b/web/app/components/workflow/nodes/tool/components/__tests__/input-var-list.spec.tsx @@ -116,23 +116,30 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () })) vi.mock('@/service/use-apps', () => ({ - useInfiniteAppList: () => ({ - data: { - pages: [{ - data: mockApps, - }], - }, - isLoading: false, - isFetchingNextPage: false, - fetchNextPage: mockFetchNextPage, - hasNextPage: false, - }), useAppDetail: (appId: string) => ({ data: mockApps.find(app => app.id === appId), isFetching: false, }), })) +vi.mock('@tanstack/react-query', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useInfiniteQuery: () => ({ + data: { + pages: [{ + data: mockApps, + }], + }, + isLoading: false, + isFetchingNextPage: false, + fetchNextPage: mockFetchNextPage, + hasNextPage: false, + }), + } +}) + vi.mock('@/service/use-workflow', () => ({ useAppWorkflow: () => ({ data: undefined, diff --git a/web/app/components/workflow/panel/comments-panel/__tests__/index.spec.tsx b/web/app/components/workflow/panel/comments-panel/__tests__/index.spec.tsx index 49ae2a11a8..fd3f570f02 100644 --- a/web/app/components/workflow/panel/comments-panel/__tests__/index.spec.tsx +++ b/web/app/components/workflow/panel/comments-panel/__tests__/index.spec.tsx @@ -1,14 +1,12 @@ -import type { WorkflowCommentList } from '@/service/workflow-comment' +import type { WorkflowCommentList } from '@/contract/console/workflow-comment' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import CommentsPanel from '../index' const mockHandleCommentIconClick = vi.hoisted(() => vi.fn()) -const mockLoadComments = vi.hoisted(() => vi.fn()) +const mockHandleCommentResolve = vi.hoisted(() => vi.fn()) const mockSetActiveCommentId = vi.hoisted(() => vi.fn()) const mockSetControlMode = vi.hoisted(() => vi.fn()) const mockSetShowResolvedComments = vi.hoisted(() => vi.fn()) -const mockResolveWorkflowComment = vi.hoisted(() => vi.fn()) -const mockEmitCommentsUpdate = vi.hoisted(() => vi.fn()) const commentFixtures: WorkflowCommentList[] = [ { @@ -90,21 +88,11 @@ vi.mock('@/app/components/workflow/hooks/use-workflow-comment', () => ({ useWorkflowComment: () => ({ comments: commentFixtures, loading: false, - loadComments: (...args: unknown[]) => mockLoadComments(...args), handleCommentIconClick: (...args: unknown[]) => mockHandleCommentIconClick(...args), + handleCommentResolve: (...args: unknown[]) => mockHandleCommentResolve(...args), }), })) -vi.mock('@/service/workflow-comment', () => ({ - resolveWorkflowComment: (...args: unknown[]) => mockResolveWorkflowComment(...args), -})) - -vi.mock('@/app/components/workflow/collaboration/core/collaboration-manager', () => ({ - collaborationManager: { - emitCommentsUpdate: (...args: unknown[]) => mockEmitCommentsUpdate(...args), - }, -})) - vi.mock('@/app/components/base/user-avatar-list', () => ({ UserAvatarList: () =>
, })) @@ -122,8 +110,7 @@ describe('CommentsPanel', () => { vi.clearAllMocks() storeState.activeCommentId = null storeState.showResolvedComments = true - mockResolveWorkflowComment.mockResolvedValue({}) - mockLoadComments.mockResolvedValue(undefined) + mockHandleCommentResolve.mockResolvedValue(undefined) }) it('filters comments and selects a thread', () => { @@ -149,9 +136,7 @@ describe('CommentsPanel', () => { fireEvent.click(resolveIcons[0]!) await waitFor(() => { - expect(mockResolveWorkflowComment).toHaveBeenCalledWith('app-1', 'c-1') - expect(mockEmitCommentsUpdate).toHaveBeenCalledWith('app-1') - expect(mockLoadComments).toHaveBeenCalled() + expect(mockHandleCommentResolve).toHaveBeenCalledWith('c-1') expect(mockSetActiveCommentId).toHaveBeenCalledWith('c-1') }) }) diff --git a/web/app/components/workflow/panel/comments-panel/index.tsx b/web/app/components/workflow/panel/comments-panel/index.tsx index 05480abb2b..47f7c015ba 100644 --- a/web/app/components/workflow/panel/comments-panel/index.tsx +++ b/web/app/components/workflow/panel/comments-panel/index.tsx @@ -1,4 +1,4 @@ -import type { WorkflowCommentList } from '@/service/workflow-comment' +import type { WorkflowCommentList } from '@/contract/console/workflow-comment' import { cn } from '@langgenius/dify-ui/cn' import { Switch } from '@langgenius/dify-ui/switch' import { RiCheckboxCircleFill, RiCheckboxCircleLine, RiCheckLine, RiCloseLine, RiFilter3Line } from '@remixicon/react' @@ -6,14 +6,11 @@ import { memo, useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Divider from '@/app/components/base/divider' import { UserAvatarList } from '@/app/components/base/user-avatar-list' -import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { useWorkflowComment } from '@/app/components/workflow/hooks/use-workflow-comment' import { useStore } from '@/app/components/workflow/store' import { ControlMode } from '@/app/components/workflow/types' import { useAppContext } from '@/context/app-context' import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now' -import { useParams } from '@/next/navigation' -import { resolveWorkflowComment } from '@/service/workflow-comment' const CommentsPanel = () => { const { t } = useTranslation() @@ -22,9 +19,7 @@ const CommentsPanel = () => { const setControlMode = useStore(s => s.setControlMode) const showResolvedComments = useStore(s => s.showResolvedComments) const setShowResolvedComments = useStore(s => s.setShowResolvedComments) - const { comments, loading, loadComments, handleCommentIconClick } = useWorkflowComment() - const params = useParams() - const appId = params.appId as string + const { comments, loading, handleCommentIconClick, handleCommentResolve } = useWorkflowComment() const { formatTimeFromNow } = useFormatTimeFromNow() const [showOnlyMine, setShowOnlyMine] = useState(false) @@ -48,20 +43,14 @@ const CommentsPanel = () => { const handleResolve = useCallback(async (comment: WorkflowCommentList) => { if (comment.resolved) return - if (!appId) - return try { - await resolveWorkflowComment(appId, comment.id) - - collaborationManager.emitCommentsUpdate(appId) - - await loadComments() + await handleCommentResolve(comment.id) setActiveCommentId(comment.id) } catch (e) { console.error('Resolve comment failed', e) } - }, [appId, loadComments, setActiveCommentId]) + }, [handleCommentResolve, setActiveCommentId]) const hasActiveFilter = showOnlyMine || !showResolvedComments @@ -172,7 +161,7 @@ const CommentsPanel = () => { {/* Header row: creator + time */}
-
{c.created_by_account.name}
+
{c.created_by_account?.name ?? ''}
{formatTimeFromNow(c.updated_at * 1000)}
diff --git a/web/app/components/workflow/store/workflow/comment-slice.ts b/web/app/components/workflow/store/workflow/comment-slice.ts index cc7605c285..71d439fced 100644 --- a/web/app/components/workflow/store/workflow/comment-slice.ts +++ b/web/app/components/workflow/store/workflow/comment-slice.ts @@ -1,5 +1,5 @@ import type { StateCreator } from 'zustand' -import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/service/workflow-comment' +import type { UserProfile, WorkflowCommentDetail, WorkflowCommentList } from '@/contract/console/workflow-comment' export type CommentSliceShape = { comments: WorkflowCommentList[] diff --git a/web/contract/console/apps.ts b/web/contract/console/apps.ts index 2f5f16c25e..bd9e8ed06b 100644 --- a/web/contract/console/apps.ts +++ b/web/contract/console/apps.ts @@ -1,7 +1,27 @@ -import type { WorkflowOnlineUsersResponse } from '@/models/app' +import type { AppListResponse, WorkflowOnlineUsersResponse } from '@/models/app' +import type { AppModeEnum } from '@/types/app' import { type } from '@orpc/contract' import { base } from '../base' +export type AppListQuery = { + page?: number + limit?: number + name?: string + mode?: AppModeEnum + tag_ids?: string[] + is_created_by_me?: boolean +} + +export const appListContract = base + .route({ + path: '/apps', + method: 'GET', + }) + .input(type<{ + query?: AppListQuery + }>()) + .output(type()) + export const appDeleteContract = base .route({ path: '/apps/{appId}', diff --git a/web/contract/console/workflow-comment.ts b/web/contract/console/workflow-comment.ts index a4c55a46e0..216487f8c8 100644 --- a/web/contract/console/workflow-comment.ts +++ b/web/contract/console/workflow-comment.ts @@ -15,13 +15,13 @@ export type WorkflowCommentList = { position_y: number content: string created_by: string - created_by_account: UserProfile + created_by_account: UserProfile | null created_at: number updated_at: number resolved: boolean - resolved_by?: string - resolved_by_account?: UserProfile - resolved_at?: number + resolved_by?: string | null + resolved_by_account?: UserProfile | null + resolved_at?: number | null mention_count: number reply_count: number participants: UserProfile[] @@ -47,59 +47,59 @@ export type WorkflowCommentDetail = { position_y: number content: string created_by: string - created_by_account: UserProfile + created_by_account: UserProfile | null created_at: number updated_at: number resolved: boolean - resolved_by?: string - resolved_by_account?: UserProfile - resolved_at?: number + resolved_by?: string | null + resolved_by_account?: UserProfile | null + resolved_at?: number | null replies: WorkflowCommentDetailReply[] mentions: WorkflowCommentDetailMention[] } -export type WorkflowCommentCreateRes = { +type WorkflowCommentCreateRes = { id: string - created_at: string + created_at: number } -export type WorkflowCommentUpdateRes = { +type WorkflowCommentUpdateRes = { id: string - updated_at: string + updated_at: number } -export type WorkflowCommentResolveRes = { +type WorkflowCommentResolveRes = { id: string resolved: boolean resolved_by: string resolved_at: number } -export type WorkflowCommentReplyCreateRes = { +type WorkflowCommentReplyCreateRes = { id: string - created_at: string + created_at: number } -export type WorkflowCommentReplyUpdateRes = { +type WorkflowCommentReplyUpdateRes = { id: string - updated_at: string + updated_at: number } -export type CreateCommentParams = { +type CreateCommentParams = { position_x: number position_y: number content: string mentioned_user_ids?: string[] } -export type UpdateCommentParams = { +type UpdateCommentParams = { content: string position_x?: number position_y?: number mentioned_user_ids?: string[] } -export type CreateReplyParams = { +type CreateReplyParams = { content: string mentioned_user_ids?: string[] } diff --git a/web/contract/router.ts b/web/contract/router.ts index d45d3c000a..c1b4e1fa08 100644 --- a/web/contract/router.ts +++ b/web/contract/router.ts @@ -1,7 +1,7 @@ import type { InferContractRouterInputs } from '@orpc/contract' import { contract as enterpriseContract } from '@dify/contracts/enterprise/orpc.gen' import { accountAvatarContract } from './console/account' -import { appDeleteContract, workflowOnlineUsersContract } from './console/apps' +import { appDeleteContract, appListContract, workflowOnlineUsersContract } from './console/apps' import { bindPartnerStackContract, invoicesContract } from './console/billing' import { exploreAppDetailContract, @@ -61,6 +61,7 @@ export const consoleRouterContract = { }, systemFeatures: systemFeaturesContract, apps: { + list: appListContract, deleteApp: appDeleteContract, workflowOnlineUsers: workflowOnlineUsersContract, }, diff --git a/web/service/apps.ts b/web/service/apps.ts index 221e83cf39..d8e5e2136b 100644 --- a/web/service/apps.ts +++ b/web/service/apps.ts @@ -1,40 +1,13 @@ import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type' -import type { AppDetailResponse, AppListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, WebhookTriggerResponse, WorkflowOnlineUser } from '@/models/app' +import type { AppDetailResponse, AppListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, WebhookTriggerResponse } from '@/models/app' import type { CommonResponse } from '@/models/common' import type { AppIconType, AppModeEnum, ModelConfig } from '@/types/app' import { del, get, patch, post, put } from './base' -import { consoleClient } from './client' export const fetchAppList = ({ url, params }: { url: string, params?: Record }): Promise => { return get(url, { params }) } -export const fetchWorkflowOnlineUsers = async ({ appIds }: { appIds: string[] }): Promise> => { - if (!appIds.length) - return {} - - const response = await consoleClient.apps.workflowOnlineUsers({ - body: { app_ids: appIds }, - }) - - if (!response?.data) - return {} - - if (Array.isArray(response.data)) { - return response.data.reduce>((acc, item) => { - if (item?.app_id) - acc[item.app_id] = item.users || [] - return acc - }, {}) - } - - return Object.entries(response.data).reduce>((acc, [appId, users]) => { - if (appId) - acc[appId] = users || [] - return acc - }, {}) -} - export const fetchAppDetail = ({ url, id }: { url: string, id: string }): Promise => { return get(`${url}/${id}`) } diff --git a/web/service/use-apps.ts b/web/service/use-apps.ts index f28df7bd4b..b09aa18f94 100644 --- a/web/service/use-apps.ts +++ b/web/service/use-apps.ts @@ -4,7 +4,6 @@ import type { AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, - AppListResponse, AppStatisticsResponse, AppTokenCostsResponse, AppVoicesListResponse, @@ -12,66 +11,20 @@ import type { } from '@/models/app' import type { App } from '@/types/app' import { - keepPreviousData, - useInfiniteQuery, useMutation, useQuery, useQueryClient, } from '@tanstack/react-query' import { consoleClient, consoleQuery } from '@/service/client' -import { AppModeEnum } from '@/types/app' import { get, post } from './base' const NAME_SPACE = 'apps' -type AppListParams = { - page?: number - limit?: number - name?: string - mode?: AppModeEnum | 'all' - tag_ids?: string[] - is_created_by_me?: boolean -} - type DateRangeParams = { start?: string end?: string } -// Allowed app modes for filtering; defined at module scope to avoid re-creating on every call -const allowedModes = new Set([ - 'all', - AppModeEnum.WORKFLOW, - AppModeEnum.ADVANCED_CHAT, - AppModeEnum.CHAT, - AppModeEnum.AGENT_CHAT, - AppModeEnum.COMPLETION, -]) - -const normalizeAppListParams = (params: AppListParams) => { - const { - page = 1, - limit = 30, - name = '', - mode, - tag_ids, - is_created_by_me, - } = params - - const safeMode = allowedModes.has((mode as any)) ? mode : undefined - - return { - page, - limit, - name, - ...(safeMode && safeMode !== 'all' ? { mode: safeMode } : {}), - ...(tag_ids?.length ? { tag_ids } : {}), - ...(is_created_by_me ? { is_created_by_me } : {}), - } -} - -const appListKey = (params: AppListParams) => [NAME_SPACE, 'list', params] - const useAppFullListKey = [NAME_SPACE, 'full-list'] export const useGenerateRuleTemplate = (type: GeneratorType, disabled?: boolean) => { @@ -95,32 +48,11 @@ export const useAppDetail = (appID: string) => { }) } -export const useAppList = (params: AppListParams, options?: { enabled?: boolean }) => { - const normalizedParams = normalizeAppListParams(params) - return useQuery({ - queryKey: appListKey(normalizedParams), - queryFn: () => get('/apps', { params: normalizedParams }), - ...options, - }) -} - -export const useInfiniteAppList = (params: AppListParams, options?: { enabled?: boolean }) => { - const normalizedParams = normalizeAppListParams(params) - return useInfiniteQuery({ - queryKey: appListKey(normalizedParams), - queryFn: ({ pageParam = normalizedParams.page }) => get('/apps', { params: { ...normalizedParams, page: pageParam } }), - getNextPageParam: lastPage => lastPage.has_more ? lastPage.page + 1 : undefined, - initialPageParam: normalizedParams.page, - placeholderData: keepPreviousData, - ...options, - }) -} - export const useInvalidateAppList = () => { const queryClient = useQueryClient() return () => { queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'list'], + queryKey: consoleQuery.apps.list.key(), }) } } @@ -138,7 +70,7 @@ export const useDeleteAppMutation = () => { onSuccess: async () => { await Promise.all([ queryClient.invalidateQueries({ - queryKey: [NAME_SPACE, 'list'], + queryKey: consoleQuery.apps.list.key(), }), queryClient.invalidateQueries({ queryKey: useAppFullListKey, diff --git a/web/service/workflow-comment.ts b/web/service/workflow-comment.ts deleted file mode 100644 index a8debbfd15..0000000000 --- a/web/service/workflow-comment.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { - CreateCommentParams as ContractCreateCommentParams, - CreateReplyParams as ContractCreateReplyParams, - UpdateCommentParams as ContractUpdateCommentParams, - UserProfile as ContractUserProfile, - WorkflowCommentCreateRes as ContractWorkflowCommentCreateRes, - WorkflowCommentDetail as ContractWorkflowCommentDetail, - WorkflowCommentDetailReply as ContractWorkflowCommentDetailReply, - WorkflowCommentList as ContractWorkflowCommentList, - WorkflowCommentReplyCreateRes as ContractWorkflowCommentReplyCreateRes, - WorkflowCommentReplyUpdateRes as ContractWorkflowCommentReplyUpdateRes, - WorkflowCommentResolveRes as ContractWorkflowCommentResolveRes, - WorkflowCommentUpdateRes as ContractWorkflowCommentUpdateRes, -} from '@/contract/console/workflow-comment' -import type { CommonResponse } from '@/models/common' -import { consoleClient } from './client' - -type CreateCommentParams = ContractCreateCommentParams -type CreateReplyParams = ContractCreateReplyParams -type UpdateCommentParams = ContractUpdateCommentParams -export type UserProfile = ContractUserProfile -type WorkflowCommentCreateRes = ContractWorkflowCommentCreateRes -export type WorkflowCommentDetail = ContractWorkflowCommentDetail -export type WorkflowCommentDetailReply = ContractWorkflowCommentDetailReply -export type WorkflowCommentList = ContractWorkflowCommentList -type WorkflowCommentReplyCreateRes = ContractWorkflowCommentReplyCreateRes -type WorkflowCommentReplyUpdateRes = ContractWorkflowCommentReplyUpdateRes -type WorkflowCommentResolveRes = ContractWorkflowCommentResolveRes -type WorkflowCommentUpdateRes = ContractWorkflowCommentUpdateRes - -export const fetchWorkflowComments = async (appId: string): Promise => { - const response = await consoleClient.workflowComments.list({ - params: { appId }, - }) - return response.data -} - -export const createWorkflowComment = async (appId: string, params: CreateCommentParams): Promise => { - return consoleClient.workflowComments.create({ - params: { appId }, - body: params, - }) -} - -export const fetchWorkflowComment = async (appId: string, commentId: string): Promise => { - return consoleClient.workflowComments.detail({ - params: { appId, commentId }, - }) -} - -export const updateWorkflowComment = async (appId: string, commentId: string, params: UpdateCommentParams): Promise => { - return consoleClient.workflowComments.update({ - params: { appId, commentId }, - body: params, - }) -} - -export const deleteWorkflowComment = async (appId: string, commentId: string): Promise => { - return consoleClient.workflowComments.delete({ - params: { appId, commentId }, - }) -} - -export const resolveWorkflowComment = async (appId: string, commentId: string): Promise => { - return consoleClient.workflowComments.resolve({ - params: { appId, commentId }, - }) -} - -export const createWorkflowCommentReply = async (appId: string, commentId: string, params: CreateReplyParams): Promise => { - return consoleClient.workflowComments.replies.create({ - params: { appId, commentId }, - body: params, - }) -} - -export const updateWorkflowCommentReply = async (appId: string, commentId: string, replyId: string, params: CreateReplyParams): Promise => { - return consoleClient.workflowComments.replies.update({ - params: { - appId, - commentId, - replyId, - }, - body: params, - }) -} - -export const deleteWorkflowCommentReply = async (appId: string, commentId: string, replyId: string): Promise => { - return consoleClient.workflowComments.replies.delete({ - params: { - appId, - commentId, - replyId, - }, - }) -} - -export const fetchMentionableUsers = async (appId: string) => { - const response = await consoleClient.workflowComments.mentionUsers({ - params: { appId }, - }) - return response.users -} From 4df7c00859d069f09de1c29e31ec7038d60623c6 Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 6 May 2026 09:47:32 +0800 Subject: [PATCH 002/202] fix: fix test_sharded_channel failed (#35814) --- .../broadcast_channel/redis/test_sharded_channel.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api/tests/test_containers_integration_tests/libs/broadcast_channel/redis/test_sharded_channel.py b/api/tests/test_containers_integration_tests/libs/broadcast_channel/redis/test_sharded_channel.py index 43915a204d..84c1d0ca41 100644 --- a/api/tests/test_containers_integration_tests/libs/broadcast_channel/redis/test_sharded_channel.py +++ b/api/tests/test_containers_integration_tests/libs/broadcast_channel/redis/test_sharded_channel.py @@ -8,6 +8,7 @@ Covers real Redis 7+ sharded pub/sub interactions including: - Resource cleanup accounting via PUBSUB SHARDNUMSUB """ +import socket import threading import time import uuid @@ -356,10 +357,17 @@ class TestShardedRedisBroadcastChannelClusterIntegration: def _get_test_topic_name(cls) -> str: return f"test_sharded_cluster_topic_{uuid.uuid4()}" + @staticmethod + def _resolve_announced_ip(host: str) -> str: + """Resolve the container host name to a literal IP accepted by Redis cluster config.""" + return socket.getaddrinfo(host, None, type=socket.SOCK_STREAM)[0][4][0] + @staticmethod def _ensure_single_node_cluster(host: str, port: int) -> None: + """Bootstrap a single-node cluster using a literal IP for Redis node advertisement.""" client = redis.Redis(host=host, port=port, decode_responses=False) - client.config_set("cluster-announce-ip", host) + announced_ip = TestShardedRedisBroadcastChannelClusterIntegration._resolve_announced_ip(host) + client.config_set("cluster-announce-ip", announced_ip) client.config_set("cluster-announce-port", port) slots = client.execute_command("CLUSTER", "SLOTS") if not slots: From 5c68f12bb81603911b894bbde07f41d0b1d2ff8d Mon Sep 17 00:00:00 2001 From: wangxiaolei Date: Wed, 6 May 2026 10:40:19 +0800 Subject: [PATCH 003/202] fix: fix Working outside of application context (#35819) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- api/core/provider_manager.py | 18 +++++++++--------- api/models/provider.py | 12 +++++++----- .../unit_tests/core/test_provider_manager.py | 16 ++++++---------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index 8969825be4..b290ae456e 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -9,9 +9,9 @@ from typing import TYPE_CHECKING, Any from pydantic import TypeAdapter from sqlalchemy import select from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session from configs import dify_config +from core.db.session_factory import session_factory from core.entities.model_entities import DefaultModelEntity, DefaultModelProviderEntity from core.entities.provider_configuration import ProviderConfiguration, ProviderConfigurations, ProviderModelBundle from core.entities.provider_entities import ( @@ -445,7 +445,7 @@ class ProviderManager: @staticmethod def _get_all_providers(tenant_id: str) -> dict[str, list[Provider]]: provider_name_to_provider_records_dict = defaultdict(list) - with Session(db.engine, expire_on_commit=False) as session: + with session_factory.create_session() as session: stmt = select(Provider).where(Provider.tenant_id == tenant_id, Provider.is_valid == True) providers = session.scalars(stmt) for provider in providers: @@ -462,7 +462,7 @@ class ProviderManager: :return: """ provider_name_to_provider_model_records_dict = defaultdict(list) - with Session(db.engine, expire_on_commit=False) as session: + with session_factory.create_session() as session: stmt = select(ProviderModel).where(ProviderModel.tenant_id == tenant_id, ProviderModel.is_valid == True) provider_models = session.scalars(stmt) for provider_model in provider_models: @@ -478,7 +478,7 @@ class ProviderManager: :return: """ provider_name_to_preferred_provider_type_records_dict = {} - with Session(db.engine, expire_on_commit=False) as session: + with session_factory.create_session() as session: stmt = select(TenantPreferredModelProvider).where(TenantPreferredModelProvider.tenant_id == tenant_id) preferred_provider_types = session.scalars(stmt) provider_name_to_preferred_provider_type_records_dict = { @@ -496,7 +496,7 @@ class ProviderManager: :return: """ provider_name_to_provider_model_settings_dict = defaultdict(list) - with Session(db.engine, expire_on_commit=False) as session: + with session_factory.create_session() as session: stmt = select(ProviderModelSetting).where(ProviderModelSetting.tenant_id == tenant_id) provider_model_settings = session.scalars(stmt) for provider_model_setting in provider_model_settings: @@ -514,7 +514,7 @@ class ProviderManager: :return: """ provider_name_to_provider_model_credentials_dict = defaultdict(list) - with Session(db.engine, expire_on_commit=False) as session: + with session_factory.create_session() as session: stmt = select(ProviderModelCredential).where(ProviderModelCredential.tenant_id == tenant_id) provider_model_credentials = session.scalars(stmt) for provider_model_credential in provider_model_credentials: @@ -544,7 +544,7 @@ class ProviderManager: return {} provider_name_to_provider_load_balancing_model_configs_dict = defaultdict(list) - with Session(db.engine, expire_on_commit=False) as session: + with session_factory.create_session() as session: stmt = select(LoadBalancingModelConfig).where(LoadBalancingModelConfig.tenant_id == tenant_id) provider_load_balancing_configs = session.scalars(stmt) for provider_load_balancing_config in provider_load_balancing_configs: @@ -578,7 +578,7 @@ class ProviderManager: :param provider_name: provider name :return: """ - with Session(db.engine, expire_on_commit=False) as session: + with session_factory.create_session() as session: stmt = ( select(ProviderCredential) .where( @@ -608,7 +608,7 @@ class ProviderManager: :param model_type: model type :return: """ - with Session(db.engine, expire_on_commit=False) as session: + with session_factory.create_session() as session: stmt = ( select(ProviderModelCredential) .where( diff --git a/api/models/provider.py b/api/models/provider.py index 2bb67d605b..8dc3ce4ff6 100644 --- a/api/models/provider.py +++ b/api/models/provider.py @@ -9,11 +9,11 @@ import sqlalchemy as sa from sqlalchemy import DateTime, String, func, select, text from sqlalchemy.orm import Mapped, mapped_column +from core.db.session_factory import session_factory from graphon.model_runtime.entities.model_entities import ModelType from libs.uuid_utils import uuidv7 from .base import TypeBase -from .engine import db from .enums import CredentialSourceType, PaymentStatus, ProviderQuotaType from .types import EnumText, LongText, StringUUID @@ -82,7 +82,8 @@ class Provider(TypeBase): @cached_property def credential(self): if self.credential_id: - return db.session.scalar(select(ProviderCredential).where(ProviderCredential.id == self.credential_id)) + with session_factory.create_session() as session: + return session.scalar(select(ProviderCredential).where(ProviderCredential.id == self.credential_id)) @property def credential_name(self): @@ -145,9 +146,10 @@ class ProviderModel(TypeBase): @cached_property def credential(self): if self.credential_id: - return db.session.scalar( - select(ProviderModelCredential).where(ProviderModelCredential.id == self.credential_id) - ) + with session_factory.create_session() as session: + return session.scalar( + select(ProviderModelCredential).where(ProviderModelCredential.id == self.credential_id) + ) @property def credential_name(self): diff --git a/api/tests/unit_tests/core/test_provider_manager.py b/api/tests/unit_tests/core/test_provider_manager.py index a5a542c94f..02f12fb3b4 100644 --- a/api/tests/unit_tests/core/test_provider_manager.py +++ b/api/tests/unit_tests/core/test_provider_manager.py @@ -570,8 +570,7 @@ def test_get_all_providers_normalizes_provider_names_with_model_provider_id() -> session.scalars.return_value = [openai_provider, gemini_provider] with ( - patch("core.provider_manager.db", SimpleNamespace(engine=object())), - patch("core.provider_manager.Session", return_value=_build_session_context(session)), + patch("core.provider_manager.session_factory.create_session", return_value=_build_session_context(session)), ): result = ProviderManager._get_all_providers("tenant-id") @@ -595,8 +594,7 @@ def test_provider_grouping_helpers_group_records_by_provider_name(method_name: s session.scalars.return_value = [openai_primary, openai_secondary, anthropic_record] with ( - patch("core.provider_manager.db", SimpleNamespace(engine=object())), - patch("core.provider_manager.Session", return_value=_build_session_context(session)), + patch("core.provider_manager.session_factory.create_session", return_value=_build_session_context(session)), ): result = getattr(ProviderManager, method_name)("tenant-id") @@ -611,8 +609,7 @@ def test_get_all_preferred_model_providers_returns_mapping_by_provider_name() -> session.scalars.return_value = [openai_preference, anthropic_preference] with ( - patch("core.provider_manager.db", SimpleNamespace(engine=object())), - patch("core.provider_manager.Session", return_value=_build_session_context(session)), + patch("core.provider_manager.session_factory.create_session", return_value=_build_session_context(session)), ): result = ProviderManager._get_all_preferred_model_providers("tenant-id") @@ -626,13 +623,13 @@ def test_get_all_provider_load_balancing_configs_returns_empty_when_cached_flag_ with ( patch("core.provider_manager.redis_client.get", return_value=b"False"), patch("core.provider_manager.FeatureService.get_features") as mock_get_features, - patch("core.provider_manager.Session") as mock_session_cls, + patch("core.provider_manager.session_factory.create_session") as mock_create_session, ): result = ProviderManager._get_all_provider_load_balancing_configs("tenant-id") assert result == {} mock_get_features.assert_not_called() - mock_session_cls.assert_not_called() + mock_create_session.assert_not_called() def test_get_all_provider_load_balancing_configs_populates_cache_and_groups_configs() -> None: @@ -642,14 +639,13 @@ def test_get_all_provider_load_balancing_configs_populates_cache_and_groups_conf session.scalars.return_value = [openai_config, anthropic_config] with ( - patch("core.provider_manager.db", SimpleNamespace(engine=object())), patch("core.provider_manager.redis_client.get", return_value=None), patch("core.provider_manager.redis_client.setex") as mock_setex, patch( "core.provider_manager.FeatureService.get_features", return_value=SimpleNamespace(model_load_balancing_enabled=True), ), - patch("core.provider_manager.Session", return_value=_build_session_context(session)), + patch("core.provider_manager.session_factory.create_session", return_value=_build_session_context(session)), ): result = ProviderManager._get_all_provider_load_balancing_configs("tenant-id") From b83f296634badce22efe929fa76d1283c6ff4252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9D=9E=E6=B3=95=E6=93=8D=E4=BD=9C?= Date: Wed, 6 May 2026 10:56:10 +0800 Subject: [PATCH 004/202] fix: restore workflow versions via backend API (#35817) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- eslint-suppressions.json | 2 +- .../__tests__/workflow-edge-events.spec.tsx | 32 ++- ...laboration-manager.logs-and-events.spec.ts | 18 +- ...n-manager.socket-and-subscriptions.spec.ts | 8 - .../core/collaboration-manager.ts | 20 -- .../collaboration/types/collaboration.ts | 20 -- .../__tests__/header-in-restoring.spec.tsx | 10 +- .../header/__tests__/header-layouts.spec.tsx | 76 ++++- .../workflow/header/header-in-restoring.tsx | 121 +++++--- .../__tests__/use-leader-restore.spec.ts | 262 ------------------ web/app/components/workflow/hooks/index.ts | 1 - .../workflow/hooks/use-leader-restore.ts | 164 ----------- web/app/components/workflow/index.tsx | 14 +- 13 files changed, 192 insertions(+), 556 deletions(-) delete mode 100644 web/app/components/workflow/hooks/__tests__/use-leader-restore.spec.ts delete mode 100644 web/app/components/workflow/hooks/use-leader-restore.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 3c86fb2b7c..258584c7f3 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -3642,7 +3642,7 @@ }, "web/app/components/workflow/hooks/index.ts": { "no-barrel-files/no-barrel-files": { - "count": 26 + "count": 25 } }, "web/app/components/workflow/hooks/use-checklist.ts": { diff --git a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx index 28be928a82..817c397d5d 100644 --- a/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx +++ b/web/app/components/workflow/__tests__/workflow-edge-events.spec.tsx @@ -27,6 +27,12 @@ const reactFlowBridge = vi.hoisted(() => ({ const collaborationBridge = vi.hoisted(() => ({ graphImportHandler: null as null | ((payload: { nodes: Node[], edges: Edge[] }) => void), historyActionHandler: null as null | ((payload: unknown) => void), + restoreIntentHandler: null as null | ((payload: { + versionId: string + versionName?: string + initiatorUserId: string + initiatorName: string + }) => void), })) const toastInfoMock = vi.hoisted(() => vi.fn()) @@ -180,6 +186,10 @@ vi.mock('../collaboration/core/collaboration-manager', () => ({ collaborationBridge.historyActionHandler = handler return vi.fn() }, + onRestoreIntent: (handler: typeof collaborationBridge.restoreIntentHandler) => { + collaborationBridge.restoreIntentHandler = handler + return vi.fn() + }, }, })) @@ -401,7 +411,6 @@ vi.mock('../hooks', () => ({ useWorkflowRefreshDraft: () => ({ handleRefreshWorkflowDraft: vi.fn(), }), - useLeaderRestoreListener: vi.fn(), })) vi.mock('../hooks/use-workflow-search', () => ({ @@ -479,6 +488,7 @@ describe('Workflow edge event wiring', () => { reactFlowBridge.store = null collaborationBridge.graphImportHandler = null collaborationBridge.historyActionHandler = null + collaborationBridge.restoreIntentHandler = null workflowCommentState.comments = [] workflowCommentState.pendingComment = null workflowCommentState.activeComment = null @@ -626,6 +636,26 @@ describe('Workflow edge event wiring', () => { }) }) + it('should show restore intent toast when another collaborator restores a workflow version', async () => { + renderSubject() + + act(() => { + collaborationBridge.restoreIntentHandler?.({ + versionId: 'version-1', + versionName: 'Version One', + initiatorUserId: 'user-1', + initiatorName: 'Alice', + }) + }) + + await waitFor(() => { + expect(toastInfoMock).toHaveBeenCalledWith( + 'workflow.versionHistory.action.restoreInProgress:{"userName":"Alice","versionName":"Version One"}', + { timeout: 3000 }, + ) + }) + }) + it('should render comment overlays and execute comment actions in comment mode', async () => { workflowCommentState.comments = [ { id: 'comment-1', resolved: false }, diff --git a/web/app/components/workflow/collaboration/core/__tests__/collaboration-manager.logs-and-events.spec.ts b/web/app/components/workflow/collaboration/core/__tests__/collaboration-manager.logs-and-events.spec.ts index 89888bf1cb..e56aa1d82c 100644 --- a/web/app/components/workflow/collaboration/core/__tests__/collaboration-manager.logs-and-events.spec.ts +++ b/web/app/components/workflow/collaboration/core/__tests__/collaboration-manager.logs-and-events.spec.ts @@ -1,5 +1,5 @@ import type { LoroMap } from 'loro-crdt' -import type { OnlineUser, RestoreRequestData } from '../../types/collaboration' +import type { OnlineUser } from '../../types/collaboration' import type { NoteNodeType } from '@/app/components/workflow/note-node/types' import type { Edge, Node } from '@/app/components/workflow/types' import { LoroDoc } from 'loro-crdt' @@ -67,18 +67,6 @@ const createEdge = (id: string, source: string, target: string): Edge => ({ }, }) -const createRestoreRequestData = (): RestoreRequestData => ({ - versionId: 'version-1', - versionName: 'Version One', - initiatorUserId: 'user-1', - initiatorName: 'Alice', - graphData: { - nodes: [createNode('n-restore')], - edges: [], - viewport: { x: 1, y: 2, zoom: 0.5 }, - }, -}) - const setupManagerWithDoc = () => { const manager = new CollaborationManager() const doc = new LoroDoc() @@ -275,7 +263,6 @@ describe('CollaborationManager logs and event helpers', () => { manager.emitCommentsUpdate('app-1') manager.emitHistoryAction('undo') - manager.emitRestoreRequest(createRestoreRequestData()) manager.emitRestoreIntent({ versionId: 'version-1', versionName: 'Version One', @@ -289,13 +276,11 @@ describe('CollaborationManager logs and event helpers', () => { manager.emitCommentsUpdate('app-1') manager.emitHistoryAction('undo') - manager.emitRestoreRequest(createRestoreRequestData()) expect(sendSpy).not.toHaveBeenCalled() isConnectedSpy.mockReturnValue(true) manager.emitCommentsUpdate('app-1') manager.emitHistoryAction('redo') - manager.emitRestoreRequest(createRestoreRequestData()) manager.emitRestoreIntent({ versionId: 'version-2', initiatorUserId: 'u-2', @@ -309,7 +294,6 @@ describe('CollaborationManager logs and event helpers', () => { expect(eventTypes).toEqual([ 'comments_update', 'workflow_history_action', - 'workflow_restore_request', 'workflow_restore_intent', 'workflow_restore_complete', ]) diff --git a/web/app/components/workflow/collaboration/core/__tests__/collaboration-manager.socket-and-subscriptions.spec.ts b/web/app/components/workflow/collaboration/core/__tests__/collaboration-manager.socket-and-subscriptions.spec.ts index 6768e5b4e2..63487432dc 100644 --- a/web/app/components/workflow/collaboration/core/__tests__/collaboration-manager.socket-and-subscriptions.spec.ts +++ b/web/app/components/workflow/collaboration/core/__tests__/collaboration-manager.socket-and-subscriptions.spec.ts @@ -203,7 +203,6 @@ describe('CollaborationManager socket and subscription behavior', () => { const mcpHandler = vi.fn() const workflowUpdateHandler = vi.fn() const commentsHandler = vi.fn() - const restoreRequestHandler = vi.fn() const restoreIntentHandler = vi.fn() const restoreCompleteHandler = vi.fn() const historyHandler = vi.fn() @@ -218,7 +217,6 @@ describe('CollaborationManager socket and subscription behavior', () => { manager.onMcpServerUpdate(mcpHandler) manager.onWorkflowUpdate(workflowUpdateHandler) manager.onCommentsUpdate(commentsHandler) - manager.onRestoreRequest(restoreRequestHandler) manager.onRestoreIntent(restoreIntentHandler) manager.onRestoreComplete(restoreCompleteHandler) manager.onHistoryAction(historyHandler) @@ -296,11 +294,6 @@ describe('CollaborationManager socket and subscription behavior', () => { type: 'graph_resync_request', data: {}, } satisfies CollaborationUpdate) - socket.trigger('collaboration_update', { - ...baseUpdate, - type: 'workflow_restore_request', - data: { versionId: 'v1', initiatorUserId: 'u-1', initiatorName: 'Alice', graphData: { nodes: [], edges: [] } } as unknown as Record, - } satisfies CollaborationUpdate) socket.trigger('collaboration_update', { ...baseUpdate, type: 'workflow_restore_intent', @@ -335,7 +328,6 @@ describe('CollaborationManager socket and subscription behavior', () => { expect(latestPresence).toMatchObject({ 'n-1': { 'socket-events': { userId: 'u-1' } } }) expect(syncRequestHandler).toHaveBeenCalledTimes(1) expect(broadcastSpy).toHaveBeenCalledTimes(1) - expect(restoreRequestHandler).toHaveBeenCalledTimes(1) expect(restoreIntentHandler).toHaveBeenCalledWith({ versionId: 'v1', initiatorUserId: 'u-1', initiatorName: 'Alice' } satisfies RestoreIntentData) expect(restoreCompleteHandler).toHaveBeenCalledWith({ versionId: 'v1', success: true } satisfies RestoreCompleteData) expect(historyHandler).toHaveBeenCalledWith({ action: 'undo', userId: 'u-1' }) diff --git a/web/app/components/workflow/collaboration/core/collaboration-manager.ts b/web/app/components/workflow/collaboration/core/collaboration-manager.ts index 5000886222..a80e493da9 100644 --- a/web/app/components/workflow/collaboration/core/collaboration-manager.ts +++ b/web/app/components/workflow/collaboration/core/collaboration-manager.ts @@ -16,7 +16,6 @@ import type { OnlineUser, RestoreCompleteData, RestoreIntentData, - RestoreRequestData, } from '../types/collaboration' import { cloneDeep } from 'es-toolkit/object' import { isEqual } from 'es-toolkit/predicate' @@ -770,17 +769,6 @@ export class CollaborationManager { return this.eventEmitter.on('historyAction', callback) } - emitRestoreRequest(data: RestoreRequestData): void { - if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) - return - - this.sendCollaborationEvent({ - type: 'workflow_restore_request', - data: data as unknown as Record, - timestamp: Date.now(), - }) - } - emitRestoreIntent(data: RestoreIntentData): void { if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return @@ -803,10 +791,6 @@ export class CollaborationManager { }) } - onRestoreRequest(callback: (data: RestoreRequestData) => void): () => void { - return this.eventEmitter.on('restoreRequest', callback) - } - onRestoreIntent(callback: (data: RestoreIntentData) => void): () => void { return this.eventEmitter.on('restoreIntent', callback) } @@ -1476,10 +1460,6 @@ export class CollaborationManager { if (this.isLeader) this.broadcastCurrentGraph() } - else if (update.type === 'workflow_restore_request') { - if (this.isLeader) - this.eventEmitter.emit('restoreRequest', update.data as RestoreRequestData) - } else if (update.type === 'workflow_restore_intent') { this.eventEmitter.emit('restoreIntent', update.data as RestoreIntentData) } diff --git a/web/app/components/workflow/collaboration/types/collaboration.ts b/web/app/components/workflow/collaboration/types/collaboration.ts index 3a5b71e2d1..7c083bca18 100644 --- a/web/app/components/workflow/collaboration/types/collaboration.ts +++ b/web/app/components/workflow/collaboration/types/collaboration.ts @@ -1,7 +1,3 @@ -import type { Viewport } from 'reactflow' -import type { ConversationVariable, Edge, EnvironmentVariable, Node } from '../../types' -import type { Features } from '@/app/components/base/features/types' - export type OnlineUser = { user_id: string username: string @@ -51,7 +47,6 @@ type CollaborationEventType | 'node_panel_presence' | 'app_publish_update' | 'graph_resync_request' - | 'workflow_restore_request' | 'workflow_restore_intent' | 'workflow_restore_complete' | 'workflow_history_action' @@ -63,21 +58,6 @@ export type CollaborationUpdate = { timestamp: number } -export type RestoreRequestData = { - versionId: string - versionName?: string - initiatorUserId: string - initiatorName: string - graphData: { - nodes: Node[] - edges: Edge[] - viewport?: Viewport - } - features?: Features - environmentVariables?: EnvironmentVariable[] - conversationVariables?: ConversationVariable[] -} - export type RestoreIntentData = { versionId: string versionName?: string diff --git a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx index 60d55a931c..87e1db69bf 100644 --- a/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-in-restoring.spec.tsx @@ -7,9 +7,9 @@ import HeaderInRestoring from '../header-in-restoring' const mockRestoreWorkflow = vi.fn() const mockInvalidAllLastRun = vi.fn() +const mockResetWorkflowVersionHistory = vi.fn() const mockHandleLoadBackupDraft = vi.fn() const mockHandleRefreshWorkflowDraft = vi.fn() -const mockRequestRestore = vi.fn() vi.mock('@/hooks/use-theme', () => ({ default: () => ({ @@ -31,6 +31,7 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ vi.mock('@/service/use-workflow', () => ({ useInvalidAllLastRun: () => mockInvalidAllLastRun, + useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory, useRestoreWorkflow: () => ({ mutateAsync: mockRestoreWorkflow, }), @@ -43,9 +44,6 @@ vi.mock('../../hooks', () => ({ useWorkflowRefreshDraft: () => ({ handleRefreshWorkflowDraft: mockHandleRefreshWorkflowDraft, }), - useLeaderRestore: () => ({ - requestRestore: mockRequestRestore, - }), })) const createVersion = (overrides: Partial = {}): VersionHistory => ({ @@ -92,7 +90,7 @@ describe('HeaderInRestoring', () => { expect(screen.getByRole('button', { name: 'workflow.common.restore' })).toBeDisabled() }) - it('should enable restore when version and flow config are both ready', () => { + it('should enable restore when version and flow id are both ready', () => { renderWorkflowComponent(, { initialStoreState: { currentVersion: createVersion(), @@ -100,7 +98,7 @@ describe('HeaderInRestoring', () => { hooksStoreProps: { configsMap: { flowId: 'app-1', - flowType: FlowType.appFlow, + flowType: undefined as never, fileSettings: {} as never, }, }, diff --git a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx index 1ddf013f8d..19833b4fa7 100644 --- a/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx +++ b/web/app/components/workflow/header/__tests__/header-layouts.spec.tsx @@ -14,7 +14,11 @@ const mockHandleNodeSelect = vi.fn() const mockHandleRefreshWorkflowDraft = vi.fn() const mockCloseAllInputFieldPanels = vi.fn() const mockInvalidAllLastRun = vi.fn() -const mockRequestRestore = vi.fn() +const mockRestoreWorkflow = vi.fn() +const mockResetWorkflowVersionHistory = vi.fn() +const mockEmitRestoreIntent = vi.fn() +const mockEmitRestoreComplete = vi.fn() +const mockEmitWorkflowUpdate = vi.fn() const mockNotify = vi.fn() const mockRunAndHistory = vi.fn() const mockViewHistory = vi.fn() @@ -33,9 +37,6 @@ vi.mock('../../hooks', () => ({ handleBackupDraft: mockHandleBackupDraft, handleLoadBackupDraft: mockHandleLoadBackupDraft, }), - useLeaderRestore: () => ({ - requestRestore: mockRequestRestore, - }), useNodesSyncDraft: () => ({ handleSyncWorkflowDraft: vi.fn(), }), @@ -58,6 +59,18 @@ vi.mock('@/hooks/use-theme', () => ({ vi.mock('@/service/use-workflow', () => ({ useInvalidAllLastRun: () => mockInvalidAllLastRun, + useResetWorkflowVersionHistory: () => mockResetWorkflowVersionHistory, + useRestoreWorkflow: () => ({ + mutateAsync: mockRestoreWorkflow, + }), +})) + +vi.mock('../../collaboration/core/collaboration-manager', () => ({ + collaborationManager: { + emitRestoreIntent: mockEmitRestoreIntent, + emitRestoreComplete: mockEmitRestoreComplete, + emitWorkflowUpdate: mockEmitWorkflowUpdate, + }, })) vi.mock('@langgenius/dify-ui/toast', () => ({ @@ -166,13 +179,7 @@ describe('Header layout components', () => { mockNodesReadOnly = false mockTheme = 'light' mockUseNodes.mockReturnValue([]) - mockRequestRestore.mockImplementation((_payload: unknown, callbacks?: { - onSuccess?: () => void - onSettled?: () => void - }) => { - callbacks?.onSuccess?.() - callbacks?.onSettled?.() - }) + mockRestoreWorkflow.mockResolvedValue({}) }) describe('HeaderInNormal', () => { @@ -277,7 +284,7 @@ describe('Header layout components', () => { fireEvent.click(screen.getByRole('button', { name: 'workflow.common.restore' })) await waitFor(() => { - expect(mockRequestRestore).toHaveBeenCalledTimes(1) + expect(mockRestoreWorkflow).toHaveBeenCalledWith('/apps/flow-1/workflows/version-1/restore') expect(store.getState().showWorkflowVersionHistoryPanel).toBe(false) expect(store.getState().isRestoring).toBe(false) expect(store.getState().backupDraft).toBeUndefined() @@ -289,8 +296,53 @@ describe('Header layout components', () => { message: 'workflow.versionHistory.action.restoreSuccess', }) }) + expect(mockEmitRestoreIntent).toHaveBeenCalledWith({ + versionId: currentVersion.id, + versionName: currentVersion.marked_name, + initiatorUserId: '', + initiatorName: '', + }) + expect(mockEmitRestoreComplete).toHaveBeenCalledWith({ + versionId: currentVersion.id, + success: true, + }) + expect(mockEmitWorkflowUpdate).toHaveBeenCalledWith('flow-1') + expect(mockResetWorkflowVersionHistory).toHaveBeenCalledTimes(1) expect(onRestoreSettled).toHaveBeenCalledTimes(1) }) + + it('should restore rag pipeline versions without emitting collaboration events', async () => { + const currentVersion = createCurrentVersion() + + renderWorkflowComponent( + , + { + initialStoreState: { + isRestoring: true, + showWorkflowVersionHistoryPanel: true, + backupDraft: createBackupDraft(), + currentVersion, + }, + hooksStoreProps: { + configsMap: { + flowType: FlowType.ragPipeline, + flowId: 'pipeline-1', + fileSettings: {}, + }, + }, + }, + ) + + fireEvent.click(screen.getByRole('button', { name: 'workflow.common.restore' })) + + await waitFor(() => { + expect(mockRestoreWorkflow).toHaveBeenCalledWith('/rag/pipelines/pipeline-1/workflows/version-1/restore') + expect(mockHandleRefreshWorkflowDraft).toHaveBeenCalledTimes(1) + }) + expect(mockEmitRestoreIntent).not.toHaveBeenCalled() + expect(mockEmitRestoreComplete).not.toHaveBeenCalled() + expect(mockEmitWorkflowUpdate).not.toHaveBeenCalled() + }) }) describe('HeaderInHistory', () => { diff --git a/web/app/components/workflow/header/header-in-restoring.tsx b/web/app/components/workflow/header/header-in-restoring.tsx index f07d28ff13..8ce061eb2a 100644 --- a/web/app/components/workflow/header/header-in-restoring.tsx +++ b/web/app/components/workflow/header/header-in-restoring.tsx @@ -6,12 +6,11 @@ import { useCallback, } from 'react' import { useTranslation } from 'react-i18next' -import { useFeaturesStore } from '@/app/components/base/features/hooks' import { useSelector as useAppContextSelector } from '@/context/app-context' import useTheme from '@/hooks/use-theme' -import { useInvalidAllLastRun } from '@/service/use-workflow' +import { useInvalidAllLastRun, useResetWorkflowVersionHistory, useRestoreWorkflow } from '@/service/use-workflow' +import { FlowType } from '@/types/common' import { - useLeaderRestore, useWorkflowRefreshDraft, useWorkflowRun, } from '../hooks' @@ -35,7 +34,6 @@ const HeaderInRestoring = ({ const { theme } = useTheme() const workflowStore = useWorkflowStore() const userProfile = useAppContextSelector(s => s.userProfile) - const featuresStore = useFeaturesStore() const configsMap = useHooksStore(s => s.configsMap) const invalidAllLastRun = useInvalidAllLastRun(configsMap?.flowType, configsMap?.flowId) const { @@ -47,9 +45,11 @@ const HeaderInRestoring = ({ const { handleLoadBackupDraft, } = useWorkflowRun() - const { requestRestore } = useLeaderRestore() const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft() + const { mutateAsync: restoreWorkflow } = useRestoreWorkflow() + const resetWorkflowVersionHistory = useResetWorkflowVersionHistory() const canRestore = !!currentVersion?.id && !!configsMap?.flowId && currentVersion.version !== WorkflowVersion.Draft + const canEmitCollaborationEvents = configsMap?.flowType === FlowType.appFlow const handleCancelRestore = useCallback(() => { handleLoadBackupDraft() @@ -57,47 +57,86 @@ const HeaderInRestoring = ({ setShowWorkflowVersionHistoryPanel(false) }, [workflowStore, handleLoadBackupDraft, setShowWorkflowVersionHistoryPanel]) - const handleRestore = useCallback(() => { + const restoreVersionUrl = useCallback((versionId: string) => { + if (!configsMap?.flowId) + return '' + if (configsMap.flowType === FlowType.ragPipeline) + return `/rag/pipelines/${configsMap.flowId}/workflows/${versionId}/restore` + return `/apps/${configsMap.flowId}/workflows/${versionId}/restore` + }, [configsMap?.flowId, configsMap?.flowType]) + + const emitRestoreIntent = useCallback(async () => { + if (!currentVersion || !canEmitCollaborationEvents) + return + try { + const { collaborationManager } = await import('../collaboration/core/collaboration-manager') + collaborationManager.emitRestoreIntent({ + versionId: currentVersion.id, + versionName: currentVersion.marked_name, + initiatorUserId: userProfile.id, + initiatorName: userProfile.name, + }) + } + catch (error) { + console.error('Failed to emit restore intent:', error) + } + }, [canEmitCollaborationEvents, currentVersion, userProfile.id, userProfile.name]) + + const emitRestoreComplete = useCallback(async (success: boolean, errorMessage?: string) => { + if (!currentVersion || !canEmitCollaborationEvents) + return + try { + const { collaborationManager } = await import('../collaboration/core/collaboration-manager') + collaborationManager.emitRestoreComplete({ + versionId: currentVersion.id, + success, + ...(errorMessage ? { error: errorMessage } : {}), + }) + } + catch (error) { + console.error('Failed to emit restore complete:', error) + } + }, [canEmitCollaborationEvents, currentVersion]) + + const emitWorkflowUpdate = useCallback(async () => { + if (!configsMap?.flowId || !canEmitCollaborationEvents) + return + try { + const { collaborationManager } = await import('../collaboration/core/collaboration-manager') + collaborationManager.emitWorkflowUpdate(configsMap.flowId) + } + catch (error) { + console.error('Failed to emit workflow update:', error) + } + }, [canEmitCollaborationEvents, configsMap?.flowId]) + + const handleRestore = useCallback(async () => { if (!canRestore || !currentVersion) return setShowWorkflowVersionHistoryPanel(false) - workflowStore.setState({ isRestoring: false }) - workflowStore.setState({ backupDraft: undefined }) + await emitRestoreIntent() - const { graph } = currentVersion - const features = featuresStore?.getState().features - const environmentVariables = currentVersion.environment_variables || [] - const conversationVariables = currentVersion.conversation_variables || [] - - requestRestore({ - versionId: currentVersion.id, - versionName: currentVersion.marked_name, - initiatorUserId: userProfile.id, - initiatorName: userProfile.name, - graphData: { - nodes: graph.nodes, - edges: graph.edges, - viewport: graph.viewport, - }, - features, - environmentVariables, - conversationVariables, - }, { - onSuccess: () => { - handleRefreshWorkflowDraft() - toast.success(t('versionHistory.action.restoreSuccess', { ns: 'workflow' })) - deleteAllInspectVars() - invalidAllLastRun() - }, - onError: () => { - toast.error(t('versionHistory.action.restoreFailure', { ns: 'workflow' })) - }, - onSettled: () => { - onRestoreSettled?.() - }, - }) - }, [canRestore, currentVersion, setShowWorkflowVersionHistoryPanel, workflowStore, featuresStore, requestRestore, userProfile, handleRefreshWorkflowDraft, deleteAllInspectVars, invalidAllLastRun, t, onRestoreSettled]) + try { + await restoreWorkflow(restoreVersionUrl(currentVersion.id)) + workflowStore.setState({ isRestoring: false }) + workflowStore.setState({ backupDraft: undefined }) + handleRefreshWorkflowDraft() + toast.success(t('versionHistory.action.restoreSuccess', { ns: 'workflow' })) + deleteAllInspectVars() + invalidAllLastRun() + await emitRestoreComplete(true) + await emitWorkflowUpdate() + } + catch { + toast.error(t('versionHistory.action.restoreFailure', { ns: 'workflow' })) + await emitRestoreComplete(false, 'restore failed') + } + finally { + resetWorkflowVersionHistory() + onRestoreSettled?.() + } + }, [canRestore, currentVersion, setShowWorkflowVersionHistoryPanel, emitRestoreIntent, restoreWorkflow, restoreVersionUrl, workflowStore, handleRefreshWorkflowDraft, t, deleteAllInspectVars, invalidAllLastRun, emitRestoreComplete, emitWorkflowUpdate, resetWorkflowVersionHistory, onRestoreSettled]) return ( <> diff --git a/web/app/components/workflow/hooks/__tests__/use-leader-restore.spec.ts b/web/app/components/workflow/hooks/__tests__/use-leader-restore.spec.ts deleted file mode 100644 index b0fb43f768..0000000000 --- a/web/app/components/workflow/hooks/__tests__/use-leader-restore.spec.ts +++ /dev/null @@ -1,262 +0,0 @@ -import type { RestoreIntentData, RestoreRequestData } from '../../collaboration/types/collaboration' -import type { SyncDraftCallback } from '../../hooks-store/store' -import type { Edge, Node } from '../../types' -import { act, renderHook } from '@testing-library/react' -import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features' -import { ChatVarType } from '../../panel/chat-variable-panel/type' -import { useLeaderRestore, useLeaderRestoreListener } from '../use-leader-restore' - -const mockSetViewport = vi.hoisted(() => vi.fn()) -const mockSetFeatures = vi.hoisted(() => vi.fn()) -const mockSetEnvironmentVariables = vi.hoisted(() => vi.fn()) -const mockSetConversationVariables = vi.hoisted(() => vi.fn()) -const mockDoSyncWorkflowDraft = vi.hoisted(() => vi.fn()) -const mockToastInfo = vi.hoisted(() => vi.fn()) - -const mockEmitRestoreIntent = vi.hoisted(() => vi.fn()) -const mockEmitRestoreComplete = vi.hoisted(() => vi.fn()) -const mockEmitWorkflowUpdate = vi.hoisted(() => vi.fn()) -const mockEmitRestoreRequest = vi.hoisted(() => vi.fn()) -const mockIsConnected = vi.hoisted(() => vi.fn()) -const mockGetIsLeader = vi.hoisted(() => vi.fn()) -const mockSetNodes = vi.hoisted(() => vi.fn()) -const mockSetEdges = vi.hoisted(() => vi.fn()) -const mockRefreshGraphSynchronously = vi.hoisted(() => vi.fn()) -const mockGetNodes = vi.hoisted(() => vi.fn(() => [{ id: 'old-node' } as unknown as Node])) -const mockGetEdges = vi.hoisted(() => vi.fn(() => [{ id: 'old-edge' } as unknown as Edge])) - -let restoreCompleteCallback: ((data: { versionId: string, success: boolean }) => void) | null = null -let restoreRequestCallback: ((data: RestoreRequestData) => void) | null = null -let restoreIntentCallback: ((data: RestoreIntentData) => void) | null = null - -const unsubscribeRestoreComplete = vi.hoisted(() => vi.fn()) -const unsubscribeRestoreRequest = vi.hoisted(() => vi.fn()) -const unsubscribeRestoreIntent = vi.hoisted(() => vi.fn()) - -let isCollaborationEnabled = true - -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (key: string, options?: { ns?: string, userName?: string, versionName?: string }) => { - const ns = options?.ns ? `${options.ns}.` : '' - const extra = options?.userName ? `:${options.userName}:${options.versionName}` : '' - return `${ns}${key}${extra}` - }, - }), -})) - -vi.mock('reactflow', () => ({ - useReactFlow: () => ({ - setViewport: mockSetViewport, - }), -})) - -vi.mock('@/app/components/app/store', () => ({ - useStore: { - getState: () => ({ - appDetail: { id: 'app-1' }, - }), - }, -})) - -vi.mock('@/app/components/base/features/hooks', () => ({ - useFeaturesStore: () => ({ - getState: () => ({ - setFeatures: mockSetFeatures, - }), - }), -})) - -vi.mock('@langgenius/dify-ui/toast', () => ({ - toast: { - info: (...args: unknown[]) => mockToastInfo(...args), - }, -})) - -vi.mock('../../store', () => ({ - useWorkflowStore: () => ({ - getState: () => ({ - setEnvironmentVariables: mockSetEnvironmentVariables, - setConversationVariables: mockSetConversationVariables, - }), - }), -})) - -vi.mock('../use-nodes-sync-draft', () => ({ - useNodesSyncDraft: () => ({ - doSyncWorkflowDraft: (...args: unknown[]) => mockDoSyncWorkflowDraft(...args), - }), -})) - -vi.mock('../../collaboration/core/collaboration-manager', () => ({ - collaborationManager: { - emitRestoreIntent: (...args: unknown[]) => mockEmitRestoreIntent(...args), - emitRestoreComplete: (...args: unknown[]) => mockEmitRestoreComplete(...args), - emitWorkflowUpdate: (...args: unknown[]) => mockEmitWorkflowUpdate(...args), - emitRestoreRequest: (...args: unknown[]) => mockEmitRestoreRequest(...args), - isConnected: (...args: unknown[]) => mockIsConnected(...args), - getIsLeader: (...args: unknown[]) => mockGetIsLeader(...args), - setNodes: (...args: unknown[]) => mockSetNodes(...args), - setEdges: (...args: unknown[]) => mockSetEdges(...args), - refreshGraphSynchronously: (...args: unknown[]) => mockRefreshGraphSynchronously(...args), - getNodes: () => mockGetNodes(), - getEdges: () => mockGetEdges(), - onRestoreComplete: (callback: (data: { versionId: string, success: boolean }) => void) => { - restoreCompleteCallback = callback - return unsubscribeRestoreComplete - }, - onRestoreRequest: (callback: (data: RestoreRequestData) => void) => { - restoreRequestCallback = callback - return unsubscribeRestoreRequest - }, - onRestoreIntent: (callback: (data: RestoreIntentData) => void) => { - restoreIntentCallback = callback - return unsubscribeRestoreIntent - }, - }, -})) - -describe('useLeaderRestore', () => { - const restoreData: RestoreRequestData = { - versionId: 'v-1', - versionName: 'Version One', - initiatorUserId: 'u-1', - initiatorName: 'Alice', - features: { moreLikeThis: { enabled: true } }, - environmentVariables: [{ id: 'env-1', name: 'A', value: '1', value_type: ChatVarType.String, description: '' }], - conversationVariables: [{ id: 'conv-1', name: 'B', value: '2', value_type: ChatVarType.String, description: '' }], - graphData: { - nodes: [{ id: 'new-node' } as unknown as Node], - edges: [{ id: 'new-edge' } as unknown as Edge], - viewport: { x: 1, y: 2, zoom: 0.5 }, - }, - } - - beforeEach(() => { - vi.clearAllMocks() - restoreCompleteCallback = null - restoreRequestCallback = null - restoreIntentCallback = null - isCollaborationEnabled = true - mockIsConnected.mockReturnValue(true) - mockGetIsLeader.mockReturnValue(false) - mockDoSyncWorkflowDraft.mockImplementation((_sync: boolean, callbacks?: SyncDraftCallback) => { - callbacks?.onSuccess?.() - callbacks?.onSettled?.() - }) - }) - - it('performs restore locally when collaboration is disabled', async () => { - isCollaborationEnabled = false - const onSuccess = vi.fn() - const onSettled = vi.fn() - - const { result } = renderHookWithSystemFeatures(() => useLeaderRestore(), { - systemFeatures: { enable_collaboration_mode: isCollaborationEnabled }, - }) - - await act(async () => { - result.current.requestRestore(restoreData, { onSuccess, onSettled }) - }) - - expect(mockEmitRestoreIntent).toHaveBeenCalledWith(expect.objectContaining({ - versionId: 'v-1', - initiatorName: 'Alice', - })) - expect(mockSetFeatures).toHaveBeenCalledWith({ moreLikeThis: { enabled: true } }) - expect(mockSetEnvironmentVariables).toHaveBeenCalled() - expect(mockSetConversationVariables).toHaveBeenCalled() - expect(mockSetNodes).toHaveBeenCalledWith([{ id: 'old-node' }], [{ id: 'new-node' }], 'leader-restore:apply-graph') - expect(mockSetEdges).toHaveBeenCalledWith([{ id: 'old-edge' }], [{ id: 'new-edge' }]) - expect(mockRefreshGraphSynchronously).toHaveBeenCalled() - expect(mockSetViewport).toHaveBeenCalledWith({ x: 1, y: 2, zoom: 0.5 }) - expect(mockEmitRestoreComplete).toHaveBeenCalledWith({ versionId: 'v-1', success: true }) - expect(mockEmitWorkflowUpdate).toHaveBeenCalledWith('app-1') - expect(onSuccess).toHaveBeenCalled() - expect(onSettled).toHaveBeenCalled() - }) - - it('emits restore request and resolves callbacks from restore-complete events', async () => { - isCollaborationEnabled = true - mockIsConnected.mockReturnValue(true) - mockGetIsLeader.mockReturnValue(false) - const onSuccess = vi.fn() - const onError = vi.fn() - const onSettled = vi.fn() - - const { result } = renderHookWithSystemFeatures(() => useLeaderRestore(), { - systemFeatures: { enable_collaboration_mode: isCollaborationEnabled }, - }) - - act(() => { - result.current.requestRestore(restoreData, { onSuccess, onError, onSettled }) - }) - - expect(mockEmitRestoreRequest).toHaveBeenCalledWith(restoreData) - expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() - - act(() => { - restoreCompleteCallback?.({ versionId: 'v-1', success: true }) - }) - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onSettled).toHaveBeenCalledTimes(1) - - act(() => { - result.current.requestRestore(restoreData, { onSuccess, onError, onSettled }) - restoreCompleteCallback?.({ versionId: 'v-1', success: false }) - }) - expect(onError).toHaveBeenCalledTimes(1) - expect(onSettled).toHaveBeenCalledTimes(2) - }) -}) - -describe('useLeaderRestoreListener', () => { - beforeEach(() => { - vi.clearAllMocks() - restoreRequestCallback = null - restoreIntentCallback = null - mockDoSyncWorkflowDraft.mockImplementation((_sync: boolean, callbacks?: SyncDraftCallback) => { - callbacks?.onSuccess?.() - callbacks?.onSettled?.() - }) - }) - - it('shows restore notifications for request and intent events', () => { - const { unmount } = renderHook(() => useLeaderRestoreListener()) - - act(() => { - restoreRequestCallback?.({ - ...{ - versionId: 'v-2', - versionName: 'Version Two', - initiatorUserId: 'u-2', - initiatorName: 'Bob', - graphData: { nodes: [], edges: [] }, - }, - }) - }) - - expect(mockToastInfo).toHaveBeenCalledWith( - 'workflow.versionHistory.action.restoreInProgress:Bob:Version Two', - { timeout: 3000 }, - ) - expect(mockEmitRestoreIntent).toHaveBeenCalled() - - act(() => { - restoreIntentCallback?.({ - versionId: 'v-3', - versionName: 'Version Three', - initiatorUserId: 'u-3', - initiatorName: 'Carol', - }) - }) - expect(mockToastInfo).toHaveBeenCalledWith( - 'workflow.versionHistory.action.restoreInProgress:Carol:Version Three', - { timeout: 3000 }, - ) - - unmount() - expect(unsubscribeRestoreRequest).toHaveBeenCalled() - expect(unsubscribeRestoreIntent).toHaveBeenCalled() - }) -}) diff --git a/web/app/components/workflow/hooks/index.ts b/web/app/components/workflow/hooks/index.ts index e0e24ef994..8dc5def829 100644 --- a/web/app/components/workflow/hooks/index.ts +++ b/web/app/components/workflow/hooks/index.ts @@ -4,7 +4,6 @@ export * from './use-checklist' export * from './use-DSL' export * from './use-edges-interactions' export * from './use-inspect-vars-crud' -export * from './use-leader-restore' export * from './use-node-data-update' export * from './use-nodes-interactions' export * from './use-nodes-meta-data' diff --git a/web/app/components/workflow/hooks/use-leader-restore.ts b/web/app/components/workflow/hooks/use-leader-restore.ts deleted file mode 100644 index 9083767f40..0000000000 --- a/web/app/components/workflow/hooks/use-leader-restore.ts +++ /dev/null @@ -1,164 +0,0 @@ -import type { RestoreCompleteData, RestoreIntentData, RestoreRequestData } from '../collaboration/types/collaboration' -import type { SyncCallback } from './use-nodes-sync-draft' -import { toast } from '@langgenius/dify-ui/toast' -import { useSuspenseQuery } from '@tanstack/react-query' -import { useCallback, useEffect, useRef } from 'react' -import { useTranslation } from 'react-i18next' -import { useReactFlow } from 'reactflow' -import { useStore as useAppStore } from '@/app/components/app/store' -import { useFeaturesStore } from '@/app/components/base/features/hooks' -import { systemFeaturesQueryOptions } from '@/service/system-features' -import { collaborationManager } from '../collaboration/core/collaboration-manager' -import { useWorkflowStore } from '../store' -import { useNodesSyncDraft } from './use-nodes-sync-draft' - -type RestoreCallbacks = SyncCallback - -const usePerformRestore = () => { - const { doSyncWorkflowDraft } = useNodesSyncDraft() - const appDetail = useAppStore.getState().appDetail - const featuresStore = useFeaturesStore() - const workflowStore = useWorkflowStore() - const reactflow = useReactFlow() - - return useCallback((data: RestoreRequestData, callbacks?: RestoreCallbacks) => { - collaborationManager.emitRestoreIntent({ - versionId: data.versionId, - versionName: data.versionName, - initiatorUserId: data.initiatorUserId, - initiatorName: data.initiatorName, - }) - - if (data.features && featuresStore) { - const { setFeatures } = featuresStore.getState() - setFeatures(data.features) - } - - if (data.environmentVariables) { - workflowStore.getState().setEnvironmentVariables(data.environmentVariables) - } - - if (data.conversationVariables) { - workflowStore.getState().setConversationVariables(data.conversationVariables) - } - - const { nodes, edges, viewport } = data.graphData - const currentNodes = collaborationManager.getNodes() - const currentEdges = collaborationManager.getEdges() - - collaborationManager.setNodes(currentNodes, nodes, 'leader-restore:apply-graph') - collaborationManager.setEdges(currentEdges, edges) - collaborationManager.refreshGraphSynchronously() - - if (viewport) - reactflow.setViewport(viewport) - - doSyncWorkflowDraft(false, { - onSuccess: () => { - collaborationManager.emitRestoreComplete({ - versionId: data.versionId, - success: true, - }) - - if (appDetail) - collaborationManager.emitWorkflowUpdate(appDetail.id) - - callbacks?.onSuccess?.() - }, - onError: () => { - collaborationManager.emitRestoreComplete({ - versionId: data.versionId, - success: false, - error: 'Failed to sync restore to server', - }) - callbacks?.onError?.() - }, - onSettled: () => { - callbacks?.onSettled?.() - }, - }) - }, [appDetail, doSyncWorkflowDraft, featuresStore, reactflow, workflowStore]) -} - -export const useLeaderRestoreListener = () => { - const { t } = useTranslation() - const performRestore = usePerformRestore() - - useEffect(() => { - const unsubscribe = collaborationManager.onRestoreRequest((data: RestoreRequestData) => { - toast.info(t('versionHistory.action.restoreInProgress', { - ns: 'workflow', - userName: data.initiatorName, - versionName: data.versionName || data.versionId, - }), { timeout: 3000 }) - performRestore(data) - }) - - return unsubscribe - }, [performRestore, t]) - - useEffect(() => { - const unsubscribe = collaborationManager.onRestoreIntent((data: RestoreIntentData) => { - toast.info(t('versionHistory.action.restoreInProgress', { - ns: 'workflow', - userName: data.initiatorName, - versionName: data.versionName || data.versionId, - }), { timeout: 3000 }) - }) - - return unsubscribe - }, [t]) -} - -export const useLeaderRestore = () => { - const performRestore = usePerformRestore() - const pendingCallbacksRef = useRef<{ - versionId: string - callbacks: RestoreCallbacks | null - } | null>(null) - const { data: isCollaborationEnabled } = useSuspenseQuery({ - ...systemFeaturesQueryOptions(), - select: s => s.enable_collaboration_mode, - }) - - const requestRestore = useCallback((data: RestoreRequestData, callbacks?: RestoreCallbacks) => { - if (!isCollaborationEnabled || !collaborationManager.isConnected() || collaborationManager.getIsLeader()) { - performRestore(data, callbacks) - return - } - - pendingCallbacksRef.current = { - versionId: data.versionId, - callbacks: callbacks || null, - } - collaborationManager.emitRestoreRequest(data) - }, [isCollaborationEnabled, performRestore]) - - useEffect(() => { - const unsubscribe = collaborationManager.onRestoreComplete((data: RestoreCompleteData) => { - const pending = pendingCallbacksRef.current - if (!pending || pending.versionId !== data.versionId) - return - - const callbacks = pending.callbacks - if (!callbacks) { - pendingCallbacksRef.current = null - return - } - - if (data.success) - callbacks.onSuccess?.() - else - callbacks.onError?.() - - callbacks.onSettled?.() - pendingCallbacksRef.current = null - }) - - return unsubscribe - }, []) - - return { - requestRestore, - } -} diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index 8598904cb7..b10e168c25 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -81,7 +81,6 @@ import EdgeContextmenu from './edge-contextmenu' import HelpLine from './help-line' import { useEdgesInteractions, - useLeaderRestoreListener, useNodesInteractions, useNodesReadOnly, useNodesSyncDraft, @@ -277,6 +276,17 @@ export const Workflow: FC = memo(({ toast.info(t('collaboration.historyAction.generic', { ns: 'workflow' })) }) }, [t]) + + useEffect(() => { + return collaborationManager.onRestoreIntent((data) => { + toast.info(t('versionHistory.action.restoreInProgress', { + ns: 'workflow', + userName: data.initiatorName, + versionName: data.versionName || data.versionId, + }), { timeout: 3000 }) + }) + }, [t]) + const { handleSyncWorkflowDraft, syncWorkflowDraftWhenPageClose, @@ -533,8 +543,6 @@ export const Workflow: FC = memo(({ // Initialize workflow node search functionality useWorkflowSearch() - useLeaderRestoreListener() - // Set up scroll to node event listener using the utility function useEffect(() => { return setupScrollToNodeListener(nodes, reactflow) From 70eb98d6c5f405d57e5f2fdec20f70e59f928bf5 Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 5 May 2026 23:03:22 -0400 Subject: [PATCH 005/202] fix(file_factory): drop doubled dot when standardizing datasource file extension (#35808) Co-authored-by: Beandon13 --- api/factories/file_factory/builders.py | 2 +- .../unit_tests/factories/test_file_factory.py | 92 +++++++++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/api/factories/file_factory/builders.py b/api/factories/file_factory/builders.py index 1d2ad4d445..4fb976f0e7 100644 --- a/api/factories/file_factory/builders.py +++ b/api/factories/file_factory/builders.py @@ -298,7 +298,7 @@ def _build_from_datasource_file( raise ValueError(f"DatasourceFile {mapping.get('datasource_file_id')} not found") extension = "." + datasource_file.key.split(".")[-1] if "." in datasource_file.key else ".bin" - detected_file_type = standardize_file_type(extension="." + extension, mime_type=datasource_file.mime_type) + detected_file_type = standardize_file_type(extension=extension, mime_type=datasource_file.mime_type) file_type = _resolve_file_type( detected_file_type=detected_file_type, specified_type=mapping.get("type"), diff --git a/api/tests/unit_tests/factories/test_file_factory.py b/api/tests/unit_tests/factories/test_file_factory.py index c2835c4124..293be925ae 100644 --- a/api/tests/unit_tests/factories/test_file_factory.py +++ b/api/tests/unit_tests/factories/test_file_factory.py @@ -1,8 +1,11 @@ import re +from unittest.mock import MagicMock import pytest +from factories.file_factory import builders from factories.file_factory.remote import extract_filename, get_remote_file_info +from graphon.file import FileTransferMethod class _FakeResponse: @@ -291,3 +294,92 @@ class TestExtractFilename: """Test bare path (not full URL) with query string.""" result = extract_filename("/path/to/file.txt?extra=params", None) assert result == "file.txt" + + +class TestBuildFromDatasourceFile: + """Tests for _build_from_datasource_file extension handling.""" + + @staticmethod + def _patch_session(monkeypatch: pytest.MonkeyPatch, datasource_file): + """Stub session_factory.create_session() so it returns the given UploadFile-shaped record.""" + session = MagicMock() + session.scalar.return_value = datasource_file + ctx = MagicMock() + ctx.__enter__ = MagicMock(return_value=session) + ctx.__exit__ = MagicMock(return_value=False) + monkeypatch.setattr(builders.session_factory, "create_session", lambda: ctx) + + def _make_datasource_file(self, *, key: str, mime_type: str = "text/csv"): + f = MagicMock() + f.id = "file-id" + f.key = key + f.name = key.split("/")[-1] + f.mime_type = mime_type + f.size = 123 + f.source_url = f"https://example.com/{key}" + return f + + def test_extension_passed_without_doubled_dot(self, monkeypatch: pytest.MonkeyPatch): + """Regression: standardize_file_type must receive the extension exactly once-prefixed. + + Previously the call was ``standardize_file_type(extension="." + extension, ...)`` while + ``extension`` already had a leading dot, producing ``"..csv"``. The mitigating + ``lstrip(".")`` inside ``standardize_file_type`` masked the bug from end users, but the + argument shape itself was wrong and showed up in any caller that didn't strip dots. + """ + captured: dict = {} + + def fake_standardize(*, extension: str = "", mime_type: str = ""): + from graphon.file import FileType + + captured["extension"] = extension + captured["mime_type"] = mime_type + return FileType.DOCUMENT + + monkeypatch.setattr(builders, "standardize_file_type", fake_standardize) + + datasource_file = self._make_datasource_file(key="folder/data.csv", mime_type="text/csv") + self._patch_session(monkeypatch, datasource_file) + + access_controller = MagicMock() + access_controller.apply_upload_file_filters = lambda stmt: stmt + + file = builders._build_from_datasource_file( + mapping={"datasource_file_id": "file-id", "transfer_method": "datasource_file"}, + tenant_id="tenant-id", + transfer_method=FileTransferMethod.DATASOURCE_FILE, + access_controller=access_controller, + ) + + assert captured["extension"] == ".csv", ( + f"standardize_file_type received {captured['extension']!r}; expected single-dot '.csv'" + ) + assert captured["mime_type"] == "text/csv" + assert file.extension == ".csv" + + def test_extension_falls_back_to_bin_when_key_has_no_dot(self, monkeypatch: pytest.MonkeyPatch): + captured: dict = {} + + def fake_standardize(*, extension: str = "", mime_type: str = ""): + from graphon.file import FileType + + captured["extension"] = extension + return FileType.CUSTOM + + monkeypatch.setattr(builders, "standardize_file_type", fake_standardize) + + datasource_file = self._make_datasource_file(key="dotless-key", mime_type="application/octet-stream") + self._patch_session(monkeypatch, datasource_file) + + access_controller = MagicMock() + access_controller.apply_upload_file_filters = lambda stmt: stmt + + file = builders._build_from_datasource_file( + mapping={"datasource_file_id": "file-id", "transfer_method": "datasource_file"}, + tenant_id="tenant-id", + transfer_method=FileTransferMethod.DATASOURCE_FILE, + access_controller=access_controller, + ) + + assert captured["extension"] == ".bin" + assert file.extension == ".bin" From b2dacf0718a04ee9d18c485efe777b6ee725a281 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 6 May 2026 12:28:11 +0800 Subject: [PATCH 006/202] chore(tailwind-css): migrate to css first (#35754) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- eslint-suppressions.json | 8 - packages/dify-ui/.storybook/storybook.css | 5 +- packages/dify-ui/AGENTS.md | 4 +- packages/dify-ui/README.md | 20 +- packages/dify-ui/package.json | 4 - packages/dify-ui/src/placement.ts | 22 +- packages/dify-ui/src/plugins/icons.ts | 10 + packages/dify-ui/src/popover/index.tsx | 1 + packages/dify-ui/src/styles/styles.css | 86 ++ packages/dify-ui/src/tailwind-preset.ts | 87 -- packages/dify-ui/src/themes/dark.css | 64 +- packages/dify-ui/src/themes/light.css | 52 +- .../src/themes/tailwind-theme-var-define.ts | 816 ------------------ packages/dify-ui/src/themes/theme.css | 772 +++++++++++++++++ packages/dify-ui/tailwind.config.ts | 23 - packages/dify-ui/tsconfig.json | 2 +- web/.storybook/preview.tsx | 1 - web/.storybook/storybook.css | 13 + web/__tests__/real-browser-flicker.test.tsx | 1 - .../model-provider-page/model-modal/Form.tsx | 78 +- .../model-modal/__tests__/Form.spec.tsx | 4 + .../__tests__/workflow-edge-events.spec.tsx | 1 - web/app/components/workflow/index.tsx | 2 +- .../__tests__/details.spec.tsx | 5 + .../context-menu-content.tsx | 4 +- .../node-actions-menu/dropdown-content.tsx | 4 +- .../workflow/node-actions-menu/shared.tsx | 1 + .../components/class-list.tsx | 8 +- web/app/layout.tsx | 1 - web/app/styles/globals.css | 89 +- web/app/styles/monaco-sticky-fix.css | 12 +- web/app/styles/plugins/icons.ts | 21 + .../styles/plugins/typography-config.js} | 0 web/app/styles/plugins/typography.ts | 17 + web/app/styles/tailwind-core.css | 141 +++ web/tailwind-common-config.ts | 91 -- web/tailwind.config.ts | 18 - web/themes/manual-dark.css | 7 + 38 files changed, 1190 insertions(+), 1305 deletions(-) create mode 100644 packages/dify-ui/src/plugins/icons.ts delete mode 100644 packages/dify-ui/src/tailwind-preset.ts delete mode 100644 packages/dify-ui/src/themes/tailwind-theme-var-define.ts create mode 100644 packages/dify-ui/src/themes/theme.css delete mode 100644 packages/dify-ui/tailwind.config.ts create mode 100644 web/app/styles/plugins/icons.ts rename web/{typography.js => app/styles/plugins/typography-config.js} (100%) create mode 100644 web/app/styles/plugins/typography.ts create mode 100644 web/app/styles/tailwind-core.css delete mode 100644 web/tailwind-common-config.ts delete mode 100644 web/tailwind.config.ts diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 258584c7f3..fd915d809d 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -2734,14 +2734,6 @@ "count": 3 } }, - "web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 6 - } - }, "web/app/components/header/account-setting/model-provider-page/model-modal/Input.tsx": { "unicorn/prefer-number-properties": { "count": 2 diff --git a/packages/dify-ui/.storybook/storybook.css b/packages/dify-ui/.storybook/storybook.css index e9796fd046..ca76cd2968 100644 --- a/packages/dify-ui/.storybook/storybook.css +++ b/packages/dify-ui/.storybook/storybook.css @@ -1,6 +1,9 @@ @import 'tailwindcss'; -@config '../tailwind.config.ts'; +@plugin '../src/plugins/icons.ts'; + +@source '../src'; +@source '../.storybook'; @import '../src/styles/styles.css'; diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md index 4a7fe2f22a..d8a59b7a0b 100644 --- a/packages/dify-ui/AGENTS.md +++ b/packages/dify-ui/AGENTS.md @@ -1,6 +1,6 @@ # @langgenius/dify-ui -Shared design tokens, the `cn()` utility, a Tailwind CSS preset, and headless primitive components consumed by `web/`. +Shared design tokens, the `cn()` utility, CSS-first Tailwind styles, and headless primitive components consumed by `web/`. ## Component Authoring Rules @@ -51,7 +51,7 @@ The Figma design system uses `--radius/*` tokens whose scale is **offset by one ### Rules -- **Do not** add custom `borderRadius` values to `tailwind-preset.ts`. We use Tailwind v4 defaults and arbitrary values (`rounded-[Npx]`) for sizes without a standard equivalent. +- **Do not** add custom `borderRadius` theme values. We use Tailwind v4 defaults and arbitrary values (`rounded-[Npx]`) for sizes without a standard equivalent. - **Do not** use `radius-*` as CSS class names. The old `@utility radius-*` definitions have been removed. - When the Figma MCP returns `rounded-[var(--radius/sm, 6px)]`, convert it to the standard Tailwind class from the table above (e.g. `rounded-md`). - For values without a standard Tailwind equivalent (10px, 20px, 28px), use arbitrary values like `rounded-[10px]`. diff --git a/packages/dify-ui/README.md b/packages/dify-ui/README.md index cd9485c400..cd24a0c078 100644 --- a/packages/dify-ui/README.md +++ b/packages/dify-ui/README.md @@ -1,6 +1,6 @@ # @langgenius/dify-ui -Shared UI primitives, design tokens, Tailwind preset, and the `cn()` utility consumed by Dify's `web/` app. +Shared UI primitives, design tokens, CSS-first Tailwind styles, and the `cn()` utility consumed by Dify's `web/` app. The primitives are thin, opinionated wrappers around [Base UI] headless components, styled with `cva` + `cn` and Dify design tokens. @@ -46,8 +46,22 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported Utilities: - `./cn` — `clsx` + `tailwind-merge` wrapper. Use this for conditional class composition. -- `./tailwind-preset` — Tailwind v4 preset with Dify tokens. Apps extend it from their own `tailwind.config.ts`. -- `./styles.css` — the one CSS entry that ships the design tokens, theme variables, and base reset. Import it once from the app root. +- `./styles.css` — the one CSS entry that ships the design tokens, theme variables, and project utilities/components. Import it once from the app root. + +## Tailwind CSS v4 integration + +This package uses Tailwind CSS v4's CSS-first configuration model. Consumers should import Tailwind from their own root stylesheet, then import this package's CSS entry: + +```css +@import 'tailwindcss'; +@import '@langgenius/dify-ui/styles.css'; +``` + +If a consumer uses Dify UI source files through the workspace, add an explicit source so Tailwind can detect utility classes: + +```css +@source '../packages/dify-ui/src'; +``` ## Overlay & portal contract diff --git a/packages/dify-ui/package.json b/packages/dify-ui/package.json index 483db46986..73c6c0bd22 100644 --- a/packages/dify-ui/package.json +++ b/packages/dify-ui/package.json @@ -5,10 +5,6 @@ "private": true, "exports": { "./styles.css": "./src/styles/styles.css", - "./tailwind-preset": { - "types": "./src/tailwind-preset.ts", - "import": "./src/tailwind-preset.ts" - }, "./cn": { "types": "./src/cn.ts", "import": "./src/cn.ts" diff --git a/packages/dify-ui/src/placement.ts b/packages/dify-ui/src/placement.ts index 95f233fd56..bf5534c92d 100644 --- a/packages/dify-ui/src/placement.ts +++ b/packages/dify-ui/src/placement.ts @@ -19,11 +19,21 @@ export type Placement | 'left-start' | 'left-end' -export function parsePlacement(placement: Placement): { side: Side, align: Align } { - const [side, align] = placement.split('-') as [Side, Align | undefined] +const PLACEMENT_PARTS = { + 'top': { side: 'top', align: 'center' }, + 'top-start': { side: 'top', align: 'start' }, + 'top-end': { side: 'top', align: 'end' }, + 'right': { side: 'right', align: 'center' }, + 'right-start': { side: 'right', align: 'start' }, + 'right-end': { side: 'right', align: 'end' }, + 'bottom': { side: 'bottom', align: 'center' }, + 'bottom-start': { side: 'bottom', align: 'start' }, + 'bottom-end': { side: 'bottom', align: 'end' }, + 'left': { side: 'left', align: 'center' }, + 'left-start': { side: 'left', align: 'start' }, + 'left-end': { side: 'left', align: 'end' }, +} satisfies Record - return { - side, - align: align ?? 'center', - } +export function parsePlacement(placement: Placement): { side: Side, align: Align } { + return PLACEMENT_PARTS[placement] } diff --git a/packages/dify-ui/src/plugins/icons.ts b/packages/dify-ui/src/plugins/icons.ts new file mode 100644 index 0000000000..e2d678c78f --- /dev/null +++ b/packages/dify-ui/src/plugins/icons.ts @@ -0,0 +1,10 @@ +import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons' + +export default iconsPlugin({ + collections: getIconCollections(['ri']), + extraProperties: { + width: '1rem', + height: '1rem', + display: 'block', + }, +}) diff --git a/packages/dify-ui/src/popover/index.tsx b/packages/dify-ui/src/popover/index.tsx index f6fcd5ed43..3fc9f98f9a 100644 --- a/packages/dify-ui/src/popover/index.tsx +++ b/packages/dify-ui/src/popover/index.tsx @@ -13,6 +13,7 @@ export const PopoverTrigger = BasePopover.Trigger export const PopoverClose = BasePopover.Close export const PopoverTitle = BasePopover.Title export const PopoverDescription = BasePopover.Description +export const createPopoverHandle = BasePopover.createHandle type PopoverContentProps = { children: ReactNode diff --git a/packages/dify-ui/src/styles/styles.css b/packages/dify-ui/src/styles/styles.css index fb410b2d5f..e2403d308a 100644 --- a/packages/dify-ui/src/styles/styles.css +++ b/packages/dify-ui/src/styles/styles.css @@ -1,4 +1,90 @@ +/* + * @langgenius/dify-ui — Tailwind CSS v4 preset (CSS-first). + * + * This is the single CSS entry point for consumers: + * @import '@langgenius/dify-ui/styles.css'; + * + * Consumers must also `@import 'tailwindcss';` (or selected layers) in their + * own root stylesheet so that the engine generates utilities. This file only + * contributes design tokens, runtime CSS variables, and project utilities. + */ + +/* ---------- Palette overrides ----------------------------------------- */ +/* Override Tailwind v4's oklch defaults with the Dify brand palette so that + * `bg-gray-500`, `text-primary-600`, etc. resolve to the design system. */ +@theme { + /* gray */ + --color-gray-25: #fcfcfd; + --color-gray-50: #f9fafb; + --color-gray-100: #f2f4f7; + --color-gray-200: #eaecf0; + --color-gray-300: #d0d5dd; + --color-gray-400: #98a2b3; + --color-gray-500: #667085; + --color-gray-600: #344054; + --color-gray-700: #475467; + --color-gray-800: #1d2939; + --color-gray-900: #101828; + + /* primary */ + --color-primary-25: #f5f8ff; + --color-primary-50: #eff4ff; + --color-primary-100: #d1e0ff; + --color-primary-200: #b2ccff; + --color-primary-300: #84adff; + --color-primary-400: #528bff; + --color-primary-500: #2970ff; + --color-primary-600: #155eef; + --color-primary-700: #004eeb; + --color-primary-800: #0040c1; + --color-primary-900: #00359e; + + /* blue / green / yellow / purple — narrow overrides used by legacy markup */ + --color-blue-500: #e1effe; + --color-green-50: #f3faf7; + --color-green-100: #def7ec; + --color-green-800: #03543f; + --color-yellow-100: #fdf6b2; + --color-yellow-800: #723b13; + --color-purple-50: #f6f5ff; + --color-purple-200: #dcd7fe; + + /* indigo */ + --color-indigo-25: #f5f8ff; + --color-indigo-50: #eef4ff; + --color-indigo-100: #e0eaff; + --color-indigo-300: #a4bcfd; + --color-indigo-400: #8098f9; + --color-indigo-600: #444ce7; + --color-indigo-800: #2d31a6; + + /* shadows */ + --shadow-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05); + --shadow-sm: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10); + --shadow-sm-no-bottom: 0px -1px 2px 0px rgba(16, 24, 40, 0.06), 0px -1px 3px 0px rgba(16, 24, 40, 0.10); + --shadow-md: 0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10); + --shadow-lg: 0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08); + --shadow-xl: 0px 8px 8px -4px rgba(16, 24, 40, 0.03), 0px 20px 24px -4px rgba(16, 24, 40, 0.08); + --shadow-2xl: 0px 24px 48px -12px rgba(16, 24, 40, 0.18); + --shadow-3xl: 0px 32px 64px -12px rgba(16, 24, 40, 0.14); + --shadow-status-indicator-green-shadow: 0px 2px 6px 0px var(--color-components-badge-status-light-success-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer); + --shadow-status-indicator-warning-shadow: 0px 2px 6px 0px var(--color-components-badge-status-light-warning-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer); + --shadow-status-indicator-red-shadow: 0px 2px 6px 0px var(--color-components-badge-status-light-error-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer); + --shadow-status-indicator-blue-shadow: 0px 2px 6px 0px var(--color-components-badge-status-light-normal-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer); + --shadow-status-indicator-gray-shadow: 0px 1px 2px 0px var(--color-components-badge-status-light-disabled-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer); + + /* font sizes */ + --text-2xs: 0.625rem; +} + +/* ---------- Semantic design tokens (`@theme inline`) ------------------ */ +@import '../themes/theme.css'; + +/* ---------- Runtime variable values ----------------------------------- */ +/* Real values for the semantic tokens above, scoped per `html[data-theme]`. */ @import '../themes/light.css' layer(base); @import '../themes/dark.css' layer(base); + +/* ---------- Project utilities & components ---------------------------- */ @import './utilities.css'; @import './components.css'; diff --git a/packages/dify-ui/src/tailwind-preset.ts b/packages/dify-ui/src/tailwind-preset.ts deleted file mode 100644 index 2dbf4781b0..0000000000 --- a/packages/dify-ui/src/tailwind-preset.ts +++ /dev/null @@ -1,87 +0,0 @@ -import tailwindThemeVarDefine from './themes/tailwind-theme-var-define' - -const difyUIPreset = { - theme: { - extend: { - colors: { - gray: { - 25: '#fcfcfd', - 50: '#f9fafb', - 100: '#f2f4f7', - 200: '#eaecf0', - 300: '#d0d5dd', - 400: '#98a2b3', - 500: '#667085', - 600: '#344054', - 700: '#475467', - 800: '#1d2939', - 900: '#101828', - }, - primary: { - 25: '#f5f8ff', - 50: '#eff4ff', - 100: '#d1e0ff', - 200: '#b2ccff', - 300: '#84adff', - 400: '#528bff', - 500: '#2970ff', - 600: '#155eef', - 700: '#004eeb', - 800: '#0040c1', - 900: '#00359e', - }, - blue: { - 500: '#E1EFFE', - }, - green: { - 50: '#F3FAF7', - 100: '#DEF7EC', - 800: '#03543F', - }, - yellow: { - 100: '#FDF6B2', - 800: '#723B13', - }, - purple: { - 50: '#F6F5FF', - 200: '#DCD7FE', - }, - indigo: { - 25: '#F5F8FF', - 50: '#EEF4FF', - 100: '#E0EAFF', - 300: '#A4BCFD', - 400: '#8098F9', - 600: '#444CE7', - 800: '#2D31A6', - }, - ...tailwindThemeVarDefine, - }, - boxShadow: { - 'xs': '0px 1px 2px 0px rgba(16, 24, 40, 0.05)', - 'sm': '0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10)', - 'sm-no-bottom': '0px -1px 2px 0px rgba(16, 24, 40, 0.06), 0px -1px 3px 0px rgba(16, 24, 40, 0.10)', - 'md': '0px 2px 4px -2px rgba(16, 24, 40, 0.06), 0px 4px 8px -2px rgba(16, 24, 40, 0.10)', - 'lg': '0px 4px 6px -2px rgba(16, 24, 40, 0.03), 0px 12px 16px -4px rgba(16, 24, 40, 0.08)', - 'xl': '0px 8px 8px -4px rgba(16, 24, 40, 0.03), 0px 20px 24px -4px rgba(16, 24, 40, 0.08)', - '2xl': '0px 24px 48px -12px rgba(16, 24, 40, 0.18)', - '3xl': '0px 32px 64px -12px rgba(16, 24, 40, 0.14)', - 'status-indicator-green-shadow': '0px 2px 6px 0px var(--color-components-badge-status-light-success-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', - 'status-indicator-warning-shadow': '0px 2px 6px 0px var(--color-components-badge-status-light-warning-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', - 'status-indicator-red-shadow': '0px 2px 6px 0px var(--color-components-badge-status-light-error-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', - 'status-indicator-blue-shadow': '0px 2px 6px 0px var(--color-components-badge-status-light-normal-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', - 'status-indicator-gray-shadow': '0px 1px 2px 0px var(--color-components-badge-status-light-disabled-halo), 0px 0px 0px 1px var(--color-components-badge-status-light-border-outer)', - }, - opacity: { - 2: '0.02', - 8: '0.08', - }, - fontSize: { - '2xs': '0.625rem', - }, - }, - }, - plugins: [], -} - -export default difyUIPreset diff --git a/packages/dify-ui/src/themes/dark.css b/packages/dify-ui/src/themes/dark.css index 59c9811412..4c005983ef 100644 --- a/packages/dify-ui/src/themes/dark.css +++ b/packages/dify-ui/src/themes/dark.css @@ -6,18 +6,6 @@ html[data-theme="dark"] { --color-components-input-bg-active: rgb(255 255 255 / 0.05); --color-components-input-border-active: #747481; --color-components-input-border-destructive: #f97066; - - /* Sticky header / Monaco editor sticky scroll colors (dark mode) */ - /* Use solid panel background to ensure visibility when elements become sticky */ - --color-components-sticky-header-bg: var(--color-components-panel-bg); - --color-components-sticky-header-bg-hover: var(--color-components-panel-on-panel-item-bg-hover); - --color-components-sticky-header-border: var(--color-components-panel-border); - - /* Override Monaco/VSCode CSS variables for sticky scroll so the sticky header is opaque */ - --vscode-editorStickyScroll-background: var(--color-components-sticky-header-bg); - --vscode-editorStickyScrollHover-background: var(--color-components-sticky-header-bg-hover); - --vscode-editorStickyScroll-border: var(--color-components-sticky-header-border); - --vscode-editorStickyScroll-shadow: rgba(0, 0, 0, 0.6); --color-components-input-text-filled: #f4f4f5; --color-components-input-bg-destructive: rgb(255 255 255 / 0.01); --color-components-input-bg-disabled: rgb(255 255 255 / 0.03); @@ -101,17 +89,6 @@ html[data-theme="dark"] { --color-components-button-indigo-bg-hover: #6172f3; --color-components-button-indigo-bg-disabled: rgb(255 255 255 / 0.03); - --color-components-button-debug-text: rgb(255 255 255 / 0.95); - --color-components-button-debug-text-disabled: rgb(255 255 255 / 0.2); - --color-components-button-debug-bg: #ff4405; - --color-components-button-debug-bg-hover: #ff692e; - --color-components-button-debug-bg-disabled: rgb(255 68 5 / 0.08); - --color-components-button-debug-border: rgb(255 255 255 / 0.12); - --color-components-button-debug-border-hover: rgb(255 255 255 / 0.2); - --color-components-button-debug-border-disabled: rgb(255 255 255 / 0.08); - - --color-components-button-button-seam: rgb(0 0 0 / 0.15); - --color-components-checkbox-icon: rgb(255 255 255 / 0.95); --color-components-checkbox-icon-disabled: rgb(255 255 255 / 0.2); --color-components-checkbox-bg: #296dff; @@ -172,7 +149,6 @@ html[data-theme="dark"] { --color-components-panel-on-panel-item-bg-destructive-hover-transparent: rgb(255 251 250 / 0); --color-components-panel-bg-transparent: rgb(34 34 37 / 0); - --color-components-panel-bg-blur-burn: rgb(31 31 35 / 0.9); --color-components-main-nav-nav-button-text: rgb(200 206 218 / 0.6); --color-components-main-nav-nav-button-text-active: #f4f4f5; @@ -183,26 +159,6 @@ html[data-theme="dark"] { --color-components-main-nav-nav-user-border: rgb(255 255 255 / 0.05); - --color-components-main-nav-text: #a8a8b3; - --color-components-main-nav-text-active: #ffffff; - --color-components-main-nav-glass-edge-highlight-first: rgb(196 207 255 / 0.15); - --color-components-main-nav-glass-edge-highlight-middle: rgb(72 108 255 / 0); - --color-components-main-nav-glass-edge-highlight-end: rgb(196 207 255 / 0.05); - - --color-components-main-nav-glass-edge-reflection-first: rgb(92 124 255 / 0); - --color-components-main-nav-glass-edge-reflection-middle: rgb(210 219 255 / 0.8); - --color-components-main-nav-glass-edge-reflection-end: rgb(92 124 255 / 0); - - --color-components-main-nav-glass-surface-first: rgb(196 207 255 / 0.08); - --color-components-main-nav-glass-surface-middle-1: rgb(210 219 255 / 0.12); - --color-components-main-nav-glass-surface-middle-2: rgb(210 219 255 / 0.1); - --color-components-main-nav-glass-surface-end: rgb(196 207 255 / 0.08); - - --color-components-main-nav-glass-inner-glow: rgb(210 219 255 / 0.05); - --color-components-main-nav-glass-shadow-reflection: rgb(210 219 255 / 0.04); - --color-components-main-nav-glass-shadow-reflection-glow: rgb(255 255 255 / 0.02); - --color-components-main-nav-glass-text-glow: rgb(245 246 255 / 0.27); - --color-components-slider-knob: #f4f4f5; --color-components-slider-knob-hover: #fefefe; --color-components-slider-knob-disabled: rgb(255 255 255 / 0.2); @@ -401,8 +357,6 @@ html[data-theme="dark"] { --color-components-icon-bg-orange-solid: #f79009; --color-components-icon-bg-orange-soft: rgb(247 144 9 / 0.2); - --color-components-marketplace-header-bg: rgb(31 31 35 / 0.9); - --color-text-primary: #fbfbfc; --color-text-secondary: #d9d9de; --color-text-tertiary: rgb(200 206 218 / 0.6); @@ -464,7 +418,6 @@ html[data-theme="dark"] { --color-background-overlay-backdrop: rgb(24 24 27 / 0.95); --color-background-body-transparent: rgb(29 29 32 / 0); --color-background-section-burn-inverted: #27272b; - --color-background-default-hover-alpha-0: rgb(39 39 43 / 0); --color-shadow-shadow-1: rgb(0 0 0 / 0.05); --color-shadow-shadow-3: rgb(0 0 0 / 0.1); @@ -548,18 +501,6 @@ html[data-theme="dark"] { --color-workflow-workflow-progress-bg-1: rgb(24 24 27 / 0.25); --color-workflow-workflow-progress-bg-2: rgb(24 24 27 / 0.04); - --color-workflow-debug-run-status-bg: rgb(230 46 5 / 0.4); - --color-workflow-debug-breakpoint: #ff692e; - --color-workflow-debug-text: #ff9c66; - --color-workflow-debug-text-disabled: rgb(255 68 5 / 0.2); - --color-workflow-debug-run-status-bg-alt: rgb(255 46 0 / 0.5); - - --color-workflow-test-run-run-status-bg: rgb(21 90 239 / 0.5); - --color-workflow-test-run-text: #d1e0ff; - --color-workflow-test-run-run-status-bg-alt: rgb(45 90 190 / 0.9); - --color-workflow-test-run-paused-bg: rgb(247 144 9 / 0.3); - --color-workflow-test-run-paused-text: #fdb022; - --color-divider-subtle: rgb(200 206 218 / 0.08); --color-divider-regular: rgb(200 206 218 / 0.14); --color-divider-deep: rgb(200 206 218 / 0.2); @@ -604,7 +545,6 @@ html[data-theme="dark"] { --color-effects-highlight-lightmode-off: rgb(200 206 218 / 0.08); --color-effects-image-frame: #ffffff; --color-effects-icon-border: rgb(255 255 255 / 0.15); - --color-effects-highlight-subtle: rgb(200 206 218 / 0.04); --color-util-colors-orange-dark-orange-dark-50: #57130a; --color-util-colors-orange-dark-orange-dark-100: #771a0d; @@ -821,7 +761,5 @@ html[data-theme="dark"] { --color-dify-logo-blue: #e8e8e8; --color-dify-logo-black: #e8e8e8; - --color-dify-logo-outline-1: #ffffff; - --color-dify-logo-outline-2: #e8e8e8; -} \ No newline at end of file +} diff --git a/packages/dify-ui/src/themes/light.css b/packages/dify-ui/src/themes/light.css index 3901de4d8b..16e5e898ee 100644 --- a/packages/dify-ui/src/themes/light.css +++ b/packages/dify-ui/src/themes/light.css @@ -89,17 +89,6 @@ html[data-theme="light"] { --color-components-button-indigo-bg-hover: #3538cd; --color-components-button-indigo-bg-disabled: rgb(97 114 243 / 0.14); - --color-components-button-debug-text: #ffffff; - --color-components-button-debug-text-disabled: rgb(255 255 255 / 0.6); - --color-components-button-debug-bg: #ff4405; - --color-components-button-debug-bg-hover: #e62e05; - --color-components-button-debug-bg-disabled: rgb(255 68 5 / 0.2); - --color-components-button-debug-border: rgb(16 24 40 / 0.04); - --color-components-button-debug-border-hover: rgb(16 24 40 / 0.08); - --color-components-button-debug-border-disabled: rgb(255 255 255 / 0); - - --color-components-button-button-seam: rgb(0 0 0 / 0.03); - --color-components-checkbox-icon: #ffffff; --color-components-checkbox-icon-disabled: rgb(255 255 255 / 0.5); --color-components-checkbox-bg: #155aef; @@ -160,7 +149,6 @@ html[data-theme="light"] { --color-components-panel-on-panel-item-bg-destructive-hover-transparent: rgb(254 243 242 / 0); --color-components-panel-bg-transparent: rgb(255 255 255 / 0); - --color-components-panel-bg-blur-burn: rgb(255 255 255 / 0.9); --color-components-main-nav-nav-button-text: #495464; --color-components-main-nav-nav-button-text-active: #155aef; @@ -171,26 +159,6 @@ html[data-theme="light"] { --color-components-main-nav-nav-user-border: #ffffff; - --color-components-main-nav-text: #495464; - --color-components-main-nav-text-active: #0033ff; - --color-components-main-nav-glass-edge-highlight-first: rgb(255 255 255 / 0.98); - --color-components-main-nav-glass-edge-highlight-middle: rgb(255 255 255 / 0); - --color-components-main-nav-glass-edge-highlight-end: rgb(255 255 255 / 0.42); - - --color-components-main-nav-glass-edge-reflection-first: rgb(0 51 255 / 0); - --color-components-main-nav-glass-edge-reflection-middle: rgb(0 51 255 / 0.6); - --color-components-main-nav-glass-edge-reflection-end: rgb(0 51 255 / 0); - - --color-components-main-nav-glass-surface-first: rgb(0 51 255 / 0.08); - --color-components-main-nav-glass-surface-middle-1: rgb(0 51 255 / 0.12); - --color-components-main-nav-glass-surface-middle-2: rgb(0 51 255 / 0.1); - --color-components-main-nav-glass-surface-end: rgb(0 51 255 / 0.08); - - --color-components-main-nav-glass-inner-glow: rgb(255 255 255 / 0.3); - --color-components-main-nav-glass-shadow-reflection: rgb(0 51 255 / 0.06); - --color-components-main-nav-glass-shadow-reflection-glow: rgb(255 255 255 / 0); - --color-components-main-nav-glass-text-glow: rgb(49 70 255 / 0.18); - --color-components-slider-knob: #ffffff; --color-components-slider-knob-hover: #ffffff; --color-components-slider-knob-disabled: rgb(255 255 255 / 0.95); @@ -389,8 +357,6 @@ html[data-theme="light"] { --color-components-icon-bg-orange-solid: #f79009; --color-components-icon-bg-orange-soft: #fffaeb; - --color-components-marketplace-header-bg: rgb(255 255 255 / 0.98); - --color-text-primary: #101828; --color-text-secondary: #354052; --color-text-tertiary: #676f83; @@ -452,7 +418,6 @@ html[data-theme="light"] { --color-background-overlay-backdrop: rgb(242 244 247 / 0.95); --color-background-body-transparent: rgb(242 244 247 / 0); --color-background-section-burn-inverted: #f2f4f7; - --color-background-default-hover-alpha-0: rgb(249 250 251 / 0); --color-shadow-shadow-1: rgb(9 9 11 / 0.03); --color-shadow-shadow-3: rgb(9 9 11 / 0.05); @@ -536,18 +501,6 @@ html[data-theme="light"] { --color-workflow-workflow-progress-bg-1: rgb(200 206 218 / 0.2); --color-workflow-workflow-progress-bg-2: rgb(200 206 218 / 0.04); - --color-workflow-debug-run-status-bg: rgb(255 68 5 / 0.08); - --color-workflow-debug-breakpoint: #e62e05; - --color-workflow-debug-text: #e62e05; - --color-workflow-debug-text-disabled: rgb(255 68 5 / 0.2); - --color-workflow-debug-run-status-bg-alt: rgb(255 68 5 / 0.14); - - --color-workflow-test-run-run-status-bg: rgb(21 90 239 / 0.08); - --color-workflow-test-run-text: #004aeb; - --color-workflow-test-run-run-status-bg-alt: rgb(21 90 239 / 0.14); - --color-workflow-test-run-paused-bg: rgb(247 144 9 / 0.14); - --color-workflow-test-run-paused-text: #dc6803; - --color-divider-subtle: rgb(16 24 40 / 0.04); --color-divider-regular: rgb(16 24 40 / 0.08); --color-divider-deep: rgb(16 24 40 / 0.14); @@ -592,7 +545,6 @@ html[data-theme="light"] { --color-effects-highlight-lightmode-off: rgb(255 255 255 / 0); --color-effects-image-frame: #ffffff; --color-effects-icon-border: rgb(16 24 40 / 0.08); - --color-effects-highlight-subtle: rgb(255 255 255 / 0.5); --color-util-colors-orange-dark-orange-dark-50: #fff4ed; --color-util-colors-orange-dark-orange-dark-100: #ffe6d5; @@ -809,7 +761,5 @@ html[data-theme="light"] { --color-dify-logo-blue: #0033ff; --color-dify-logo-black: #000000; - --color-dify-logo-outline-1: rgb(0 0 0 / 0); - --color-dify-logo-outline-2: rgb(0 0 0 / 0); -} \ No newline at end of file +} diff --git a/packages/dify-ui/src/themes/tailwind-theme-var-define.ts b/packages/dify-ui/src/themes/tailwind-theme-var-define.ts deleted file mode 100644 index 2ea617284f..0000000000 --- a/packages/dify-ui/src/themes/tailwind-theme-var-define.ts +++ /dev/null @@ -1,816 +0,0 @@ -/* Attention: Generate by code. Don't update by hand!!! */ -const vars = { - 'components-input-bg-normal': 'var(--color-components-input-bg-normal)', - 'components-input-text-placeholder': 'var(--color-components-input-text-placeholder)', - 'components-input-bg-hover': 'var(--color-components-input-bg-hover)', - 'components-input-bg-active': 'var(--color-components-input-bg-active)', - 'components-input-border-active': 'var(--color-components-input-border-active)', - 'components-input-border-destructive': 'var(--color-components-input-border-destructive)', - 'components-input-text-filled': 'var(--color-components-input-text-filled)', - 'components-input-bg-destructive': 'var(--color-components-input-bg-destructive)', - 'components-input-bg-disabled': 'var(--color-components-input-bg-disabled)', - 'components-input-text-disabled': 'var(--color-components-input-text-disabled)', - 'components-input-text-filled-disabled': 'var(--color-components-input-text-filled-disabled)', - 'components-input-border-hover': 'var(--color-components-input-border-hover)', - 'components-input-border-active-prompt-1': 'var(--color-components-input-border-active-prompt-1)', - 'components-input-border-active-prompt-2': 'var(--color-components-input-border-active-prompt-2)', - - 'components-kbd-bg-gray': 'var(--color-components-kbd-bg-gray)', - 'components-kbd-bg-white': 'var(--color-components-kbd-bg-white)', - - 'components-tooltip-bg': 'var(--color-components-tooltip-bg)', - - 'components-button-primary-text': 'var(--color-components-button-primary-text)', - 'components-button-primary-bg': 'var(--color-components-button-primary-bg)', - 'components-button-primary-border': 'var(--color-components-button-primary-border)', - 'components-button-primary-bg-hover': 'var(--color-components-button-primary-bg-hover)', - 'components-button-primary-border-hover': 'var(--color-components-button-primary-border-hover)', - 'components-button-primary-bg-disabled': 'var(--color-components-button-primary-bg-disabled)', - 'components-button-primary-border-disabled': 'var(--color-components-button-primary-border-disabled)', - 'components-button-primary-text-disabled': 'var(--color-components-button-primary-text-disabled)', - - 'components-button-secondary-text': 'var(--color-components-button-secondary-text)', - 'components-button-secondary-text-disabled': 'var(--color-components-button-secondary-text-disabled)', - 'components-button-secondary-bg': 'var(--color-components-button-secondary-bg)', - 'components-button-secondary-bg-hover': 'var(--color-components-button-secondary-bg-hover)', - 'components-button-secondary-bg-disabled': 'var(--color-components-button-secondary-bg-disabled)', - 'components-button-secondary-border': 'var(--color-components-button-secondary-border)', - 'components-button-secondary-border-hover': 'var(--color-components-button-secondary-border-hover)', - 'components-button-secondary-border-disabled': 'var(--color-components-button-secondary-border-disabled)', - - 'components-button-tertiary-text': 'var(--color-components-button-tertiary-text)', - 'components-button-tertiary-text-disabled': 'var(--color-components-button-tertiary-text-disabled)', - 'components-button-tertiary-bg': 'var(--color-components-button-tertiary-bg)', - 'components-button-tertiary-bg-hover': 'var(--color-components-button-tertiary-bg-hover)', - 'components-button-tertiary-bg-disabled': 'var(--color-components-button-tertiary-bg-disabled)', - - 'components-button-ghost-text': 'var(--color-components-button-ghost-text)', - 'components-button-ghost-text-disabled': 'var(--color-components-button-ghost-text-disabled)', - 'components-button-ghost-bg-hover': 'var(--color-components-button-ghost-bg-hover)', - - 'components-button-destructive-primary-text': 'var(--color-components-button-destructive-primary-text)', - 'components-button-destructive-primary-text-disabled': 'var(--color-components-button-destructive-primary-text-disabled)', - 'components-button-destructive-primary-bg': 'var(--color-components-button-destructive-primary-bg)', - 'components-button-destructive-primary-bg-hover': 'var(--color-components-button-destructive-primary-bg-hover)', - 'components-button-destructive-primary-bg-disabled': 'var(--color-components-button-destructive-primary-bg-disabled)', - 'components-button-destructive-primary-border': 'var(--color-components-button-destructive-primary-border)', - 'components-button-destructive-primary-border-hover': 'var(--color-components-button-destructive-primary-border-hover)', - 'components-button-destructive-primary-border-disabled': 'var(--color-components-button-destructive-primary-border-disabled)', - - 'components-button-destructive-secondary-text': 'var(--color-components-button-destructive-secondary-text)', - 'components-button-destructive-secondary-text-disabled': 'var(--color-components-button-destructive-secondary-text-disabled)', - 'components-button-destructive-secondary-bg': 'var(--color-components-button-destructive-secondary-bg)', - 'components-button-destructive-secondary-bg-hover': 'var(--color-components-button-destructive-secondary-bg-hover)', - 'components-button-destructive-secondary-bg-disabled': 'var(--color-components-button-destructive-secondary-bg-disabled)', - 'components-button-destructive-secondary-border': 'var(--color-components-button-destructive-secondary-border)', - 'components-button-destructive-secondary-border-hover': 'var(--color-components-button-destructive-secondary-border-hover)', - 'components-button-destructive-secondary-border-disabled': 'var(--color-components-button-destructive-secondary-border-disabled)', - - 'components-button-destructive-tertiary-text': 'var(--color-components-button-destructive-tertiary-text)', - 'components-button-destructive-tertiary-text-disabled': 'var(--color-components-button-destructive-tertiary-text-disabled)', - 'components-button-destructive-tertiary-bg': 'var(--color-components-button-destructive-tertiary-bg)', - 'components-button-destructive-tertiary-bg-hover': 'var(--color-components-button-destructive-tertiary-bg-hover)', - 'components-button-destructive-tertiary-bg-disabled': 'var(--color-components-button-destructive-tertiary-bg-disabled)', - - 'components-button-destructive-ghost-text': 'var(--color-components-button-destructive-ghost-text)', - 'components-button-destructive-ghost-text-disabled': 'var(--color-components-button-destructive-ghost-text-disabled)', - 'components-button-destructive-ghost-bg-hover': 'var(--color-components-button-destructive-ghost-bg-hover)', - - 'components-button-secondary-accent-text': 'var(--color-components-button-secondary-accent-text)', - 'components-button-secondary-accent-text-disabled': 'var(--color-components-button-secondary-accent-text-disabled)', - 'components-button-secondary-accent-bg': 'var(--color-components-button-secondary-accent-bg)', - 'components-button-secondary-accent-bg-hover': 'var(--color-components-button-secondary-accent-bg-hover)', - 'components-button-secondary-accent-bg-disabled': 'var(--color-components-button-secondary-accent-bg-disabled)', - 'components-button-secondary-accent-border': 'var(--color-components-button-secondary-accent-border)', - 'components-button-secondary-accent-border-hover': 'var(--color-components-button-secondary-accent-border-hover)', - 'components-button-secondary-accent-border-disabled': 'var(--color-components-button-secondary-accent-border-disabled)', - - 'components-button-indigo-bg': 'var(--color-components-button-indigo-bg)', - 'components-button-indigo-bg-hover': 'var(--color-components-button-indigo-bg-hover)', - 'components-button-indigo-bg-disabled': 'var(--color-components-button-indigo-bg-disabled)', - - 'components-button-debug-text': 'var(--color-components-button-debug-text)', - 'components-button-debug-text-disabled': 'var(--color-components-button-debug-text-disabled)', - 'components-button-debug-bg': 'var(--color-components-button-debug-bg)', - 'components-button-debug-bg-hover': 'var(--color-components-button-debug-bg-hover)', - 'components-button-debug-bg-disabled': 'var(--color-components-button-debug-bg-disabled)', - 'components-button-debug-border': 'var(--color-components-button-debug-border)', - 'components-button-debug-border-hover': 'var(--color-components-button-debug-border-hover)', - 'components-button-debug-border-disabled': 'var(--color-components-button-debug-border-disabled)', - - 'components-button-button-seam': 'var(--color-components-button-button-seam)', - - 'components-checkbox-icon': 'var(--color-components-checkbox-icon)', - 'components-checkbox-icon-disabled': 'var(--color-components-checkbox-icon-disabled)', - 'components-checkbox-bg': 'var(--color-components-checkbox-bg)', - 'components-checkbox-bg-hover': 'var(--color-components-checkbox-bg-hover)', - 'components-checkbox-bg-disabled': 'var(--color-components-checkbox-bg-disabled)', - 'components-checkbox-border': 'var(--color-components-checkbox-border)', - 'components-checkbox-border-hover': 'var(--color-components-checkbox-border-hover)', - 'components-checkbox-border-disabled': 'var(--color-components-checkbox-border-disabled)', - 'components-checkbox-bg-unchecked': 'var(--color-components-checkbox-bg-unchecked)', - 'components-checkbox-bg-unchecked-hover': 'var(--color-components-checkbox-bg-unchecked-hover)', - 'components-checkbox-bg-disabled-checked': 'var(--color-components-checkbox-bg-disabled-checked)', - - 'components-radio-border-checked': 'var(--color-components-radio-border-checked)', - 'components-radio-border-checked-hover': 'var(--color-components-radio-border-checked-hover)', - 'components-radio-border-checked-disabled': 'var(--color-components-radio-border-checked-disabled)', - 'components-radio-bg-disabled': 'var(--color-components-radio-bg-disabled)', - 'components-radio-border': 'var(--color-components-radio-border)', - 'components-radio-border-hover': 'var(--color-components-radio-border-hover)', - 'components-radio-border-disabled': 'var(--color-components-radio-border-disabled)', - 'components-radio-bg': 'var(--color-components-radio-bg)', - 'components-radio-bg-hover': 'var(--color-components-radio-bg-hover)', - - 'components-toggle-knob': 'var(--color-components-toggle-knob)', - 'components-toggle-knob-disabled': 'var(--color-components-toggle-knob-disabled)', - 'components-toggle-bg': 'var(--color-components-toggle-bg)', - 'components-toggle-bg-hover': 'var(--color-components-toggle-bg-hover)', - 'components-toggle-bg-disabled': 'var(--color-components-toggle-bg-disabled)', - 'components-toggle-bg-unchecked': 'var(--color-components-toggle-bg-unchecked)', - 'components-toggle-bg-unchecked-hover': 'var(--color-components-toggle-bg-unchecked-hover)', - 'components-toggle-bg-unchecked-disabled': 'var(--color-components-toggle-bg-unchecked-disabled)', - 'components-toggle-knob-hover': 'var(--color-components-toggle-knob-hover)', - - 'components-card-bg': 'var(--color-components-card-bg)', - 'components-card-border': 'var(--color-components-card-border)', - 'components-card-bg-alt': 'var(--color-components-card-bg-alt)', - 'components-card-bg-transparent': 'var(--color-components-card-bg-transparent)', - 'components-card-bg-alt-transparent': 'var(--color-components-card-bg-alt-transparent)', - - 'components-menu-item-text': 'var(--color-components-menu-item-text)', - 'components-menu-item-text-active': 'var(--color-components-menu-item-text-active)', - 'components-menu-item-text-hover': 'var(--color-components-menu-item-text-hover)', - 'components-menu-item-text-active-accent': 'var(--color-components-menu-item-text-active-accent)', - 'components-menu-item-bg-active': 'var(--color-components-menu-item-bg-active)', - 'components-menu-item-bg-hover': 'var(--color-components-menu-item-bg-hover)', - - 'components-panel-bg': 'var(--color-components-panel-bg)', - 'components-panel-bg-blur': 'var(--color-components-panel-bg-blur)', - 'components-panel-border': 'var(--color-components-panel-border)', - 'components-panel-border-subtle': 'var(--color-components-panel-border-subtle)', - 'components-panel-gradient-2': 'var(--color-components-panel-gradient-2)', - 'components-panel-gradient-1': 'var(--color-components-panel-gradient-1)', - 'components-panel-bg-alt': 'var(--color-components-panel-bg-alt)', - 'components-panel-on-panel-item-bg': 'var(--color-components-panel-on-panel-item-bg)', - 'components-panel-on-panel-item-bg-hover': 'var(--color-components-panel-on-panel-item-bg-hover)', - 'components-panel-on-panel-item-bg-alt': 'var(--color-components-panel-on-panel-item-bg-alt)', - 'components-panel-on-panel-item-bg-transparent': 'var(--color-components-panel-on-panel-item-bg-transparent)', - 'components-panel-on-panel-item-bg-hover-transparent': 'var(--color-components-panel-on-panel-item-bg-hover-transparent)', - 'components-panel-on-panel-item-bg-destructive-hover-transparent': 'var(--color-components-panel-on-panel-item-bg-destructive-hover-transparent)', - - 'components-panel-bg-transparent': 'var(--color-components-panel-bg-transparent)', - 'components-panel-bg-blur-burn': 'var(--color-components-panel-bg-blur-burn)', - - 'components-main-nav-nav-button-text': 'var(--color-components-main-nav-nav-button-text)', - 'components-main-nav-nav-button-text-active': 'var(--color-components-main-nav-nav-button-text-active)', - 'components-main-nav-nav-button-bg': 'var(--color-components-main-nav-nav-button-bg)', - 'components-main-nav-nav-button-bg-active': 'var(--color-components-main-nav-nav-button-bg-active)', - 'components-main-nav-nav-button-border': 'var(--color-components-main-nav-nav-button-border)', - 'components-main-nav-nav-button-bg-hover': 'var(--color-components-main-nav-nav-button-bg-hover)', - - 'components-main-nav-nav-user-border': 'var(--color-components-main-nav-nav-user-border)', - - 'components-main-nav-text': 'var(--color-components-main-nav-text)', - 'components-main-nav-text-active': 'var(--color-components-main-nav-text-active)', - 'components-main-nav-glass-edge-highlight-first': 'var(--color-components-main-nav-glass-edge-highlight-first)', - 'components-main-nav-glass-edge-highlight-middle': 'var(--color-components-main-nav-glass-edge-highlight-middle)', - 'components-main-nav-glass-edge-highlight-end': 'var(--color-components-main-nav-glass-edge-highlight-end)', - - 'components-main-nav-glass-edge-reflection-first': 'var(--color-components-main-nav-glass-edge-reflection-first)', - 'components-main-nav-glass-edge-reflection-middle': 'var(--color-components-main-nav-glass-edge-reflection-middle)', - 'components-main-nav-glass-edge-reflection-end': 'var(--color-components-main-nav-glass-edge-reflection-end)', - - 'components-main-nav-glass-surface-first': 'var(--color-components-main-nav-glass-surface-first)', - 'components-main-nav-glass-surface-middle-1': 'var(--color-components-main-nav-glass-surface-middle-1)', - 'components-main-nav-glass-surface-middle-2': 'var(--color-components-main-nav-glass-surface-middle-2)', - 'components-main-nav-glass-surface-end': 'var(--color-components-main-nav-glass-surface-end)', - - 'components-main-nav-glass-inner-glow': 'var(--color-components-main-nav-glass-inner-glow)', - 'components-main-nav-glass-shadow-reflection': 'var(--color-components-main-nav-glass-shadow-reflection)', - 'components-main-nav-glass-shadow-reflection-glow': 'var(--color-components-main-nav-glass-shadow-reflection-glow)', - 'components-main-nav-glass-text-glow': 'var(--color-components-main-nav-glass-text-glow)', - - 'components-slider-knob': 'var(--color-components-slider-knob)', - 'components-slider-knob-hover': 'var(--color-components-slider-knob-hover)', - 'components-slider-knob-disabled': 'var(--color-components-slider-knob-disabled)', - 'components-slider-range': 'var(--color-components-slider-range)', - 'components-slider-track': 'var(--color-components-slider-track)', - 'components-slider-knob-border-hover': 'var(--color-components-slider-knob-border-hover)', - 'components-slider-knob-border': 'var(--color-components-slider-knob-border)', - - 'components-segmented-control-item-active-bg': 'var(--color-components-segmented-control-item-active-bg)', - 'components-segmented-control-item-active-border': 'var(--color-components-segmented-control-item-active-border)', - 'components-segmented-control-bg-normal': 'var(--color-components-segmented-control-bg-normal)', - 'components-segmented-control-item-active-accent-bg': 'var(--color-components-segmented-control-item-active-accent-bg)', - 'components-segmented-control-item-active-accent-border': 'var(--color-components-segmented-control-item-active-accent-border)', - - 'components-option-card-option-bg': 'var(--color-components-option-card-option-bg)', - 'components-option-card-option-selected-bg': 'var(--color-components-option-card-option-selected-bg)', - 'components-option-card-option-selected-border': 'var(--color-components-option-card-option-selected-border)', - 'components-option-card-option-border': 'var(--color-components-option-card-option-border)', - 'components-option-card-option-bg-hover': 'var(--color-components-option-card-option-bg-hover)', - 'components-option-card-option-border-hover': 'var(--color-components-option-card-option-border-hover)', - - 'components-tab-active': 'var(--color-components-tab-active)', - - 'components-badge-white-to-dark': 'var(--color-components-badge-white-to-dark)', - 'components-badge-status-light-success-bg': 'var(--color-components-badge-status-light-success-bg)', - 'components-badge-status-light-success-border-inner': 'var(--color-components-badge-status-light-success-border-inner)', - 'components-badge-status-light-success-halo': 'var(--color-components-badge-status-light-success-halo)', - - 'components-badge-status-light-border-outer': 'var(--color-components-badge-status-light-border-outer)', - 'components-badge-status-light-high-light': 'var(--color-components-badge-status-light-high-light)', - 'components-badge-status-light-warning-bg': 'var(--color-components-badge-status-light-warning-bg)', - 'components-badge-status-light-warning-border-inner': 'var(--color-components-badge-status-light-warning-border-inner)', - 'components-badge-status-light-warning-halo': 'var(--color-components-badge-status-light-warning-halo)', - - 'components-badge-status-light-error-bg': 'var(--color-components-badge-status-light-error-bg)', - 'components-badge-status-light-error-border-inner': 'var(--color-components-badge-status-light-error-border-inner)', - 'components-badge-status-light-error-halo': 'var(--color-components-badge-status-light-error-halo)', - - 'components-badge-status-light-normal-bg': 'var(--color-components-badge-status-light-normal-bg)', - 'components-badge-status-light-normal-border-inner': 'var(--color-components-badge-status-light-normal-border-inner)', - 'components-badge-status-light-normal-halo': 'var(--color-components-badge-status-light-normal-halo)', - - 'components-badge-status-light-disabled-bg': 'var(--color-components-badge-status-light-disabled-bg)', - 'components-badge-status-light-disabled-border-inner': 'var(--color-components-badge-status-light-disabled-border-inner)', - 'components-badge-status-light-disabled-halo': 'var(--color-components-badge-status-light-disabled-halo)', - - 'components-badge-bg-green-soft': 'var(--color-components-badge-bg-green-soft)', - 'components-badge-bg-orange-soft': 'var(--color-components-badge-bg-orange-soft)', - 'components-badge-bg-red-soft': 'var(--color-components-badge-bg-red-soft)', - 'components-badge-bg-blue-light-soft': 'var(--color-components-badge-bg-blue-light-soft)', - 'components-badge-bg-gray-soft': 'var(--color-components-badge-bg-gray-soft)', - 'components-badge-bg-dimm': 'var(--color-components-badge-bg-dimm)', - - 'components-chart-line': 'var(--color-components-chart-line)', - 'components-chart-area-1': 'var(--color-components-chart-area-1)', - 'components-chart-area-2': 'var(--color-components-chart-area-2)', - 'components-chart-current-1': 'var(--color-components-chart-current-1)', - 'components-chart-current-2': 'var(--color-components-chart-current-2)', - 'components-chart-bg': 'var(--color-components-chart-bg)', - - 'components-actionbar-bg': 'var(--color-components-actionbar-bg)', - 'components-actionbar-border': 'var(--color-components-actionbar-border)', - 'components-actionbar-bg-accent': 'var(--color-components-actionbar-bg-accent)', - 'components-actionbar-border-accent': 'var(--color-components-actionbar-border-accent)', - - 'components-dropzone-bg-alt': 'var(--color-components-dropzone-bg-alt)', - 'components-dropzone-bg': 'var(--color-components-dropzone-bg)', - 'components-dropzone-bg-accent': 'var(--color-components-dropzone-bg-accent)', - 'components-dropzone-border': 'var(--color-components-dropzone-border)', - 'components-dropzone-border-alt': 'var(--color-components-dropzone-border-alt)', - 'components-dropzone-border-accent': 'var(--color-components-dropzone-border-accent)', - - 'components-progress-brand-progress': 'var(--color-components-progress-brand-progress)', - 'components-progress-brand-border': 'var(--color-components-progress-brand-border)', - 'components-progress-brand-bg': 'var(--color-components-progress-brand-bg)', - - 'components-progress-white-progress': 'var(--color-components-progress-white-progress)', - 'components-progress-white-border': 'var(--color-components-progress-white-border)', - 'components-progress-white-bg': 'var(--color-components-progress-white-bg)', - - 'components-progress-gray-progress': 'var(--color-components-progress-gray-progress)', - 'components-progress-gray-border': 'var(--color-components-progress-gray-border)', - 'components-progress-gray-bg': 'var(--color-components-progress-gray-bg)', - - 'components-progress-warning-progress': 'var(--color-components-progress-warning-progress)', - 'components-progress-warning-border': 'var(--color-components-progress-warning-border)', - 'components-progress-warning-bg': 'var(--color-components-progress-warning-bg)', - - 'components-progress-error-progress': 'var(--color-components-progress-error-progress)', - 'components-progress-error-border': 'var(--color-components-progress-error-border)', - 'components-progress-error-bg': 'var(--color-components-progress-error-bg)', - - 'components-chat-input-audio-bg': 'var(--color-components-chat-input-audio-bg)', - 'components-chat-input-audio-wave-default': 'var(--color-components-chat-input-audio-wave-default)', - 'components-chat-input-bg-mask-1': 'var(--color-components-chat-input-bg-mask-1)', - 'components-chat-input-bg-mask-2': 'var(--color-components-chat-input-bg-mask-2)', - 'components-chat-input-border': 'var(--color-components-chat-input-border)', - 'components-chat-input-audio-wave-active': 'var(--color-components-chat-input-audio-wave-active)', - 'components-chat-input-audio-bg-alt': 'var(--color-components-chat-input-audio-bg-alt)', - - 'components-avatar-shape-fill-stop-0': 'var(--color-components-avatar-shape-fill-stop-0)', - 'components-avatar-shape-fill-stop-100': 'var(--color-components-avatar-shape-fill-stop-100)', - - 'components-avatar-bg-mask-stop-0': 'var(--color-components-avatar-bg-mask-stop-0)', - 'components-avatar-bg-mask-stop-100': 'var(--color-components-avatar-bg-mask-stop-100)', - - 'components-avatar-default-avatar-bg': 'var(--color-components-avatar-default-avatar-bg)', - 'components-avatar-mask-darkmode-dimmed': 'var(--color-components-avatar-mask-darkmode-dimmed)', - - 'components-label-gray': 'var(--color-components-label-gray)', - - 'components-premium-badge-blue-bg-stop-0': 'var(--color-components-premium-badge-blue-bg-stop-0)', - 'components-premium-badge-blue-bg-stop-100': 'var(--color-components-premium-badge-blue-bg-stop-100)', - 'components-premium-badge-blue-stroke-stop-0': 'var(--color-components-premium-badge-blue-stroke-stop-0)', - 'components-premium-badge-blue-stroke-stop-100': 'var(--color-components-premium-badge-blue-stroke-stop-100)', - 'components-premium-badge-blue-text-stop-0': 'var(--color-components-premium-badge-blue-text-stop-0)', - 'components-premium-badge-blue-text-stop-100': 'var(--color-components-premium-badge-blue-text-stop-100)', - 'components-premium-badge-blue-glow': 'var(--color-components-premium-badge-blue-glow)', - 'components-premium-badge-blue-bg-stop-0-hover': 'var(--color-components-premium-badge-blue-bg-stop-0-hover)', - 'components-premium-badge-blue-bg-stop-100-hover': 'var(--color-components-premium-badge-blue-bg-stop-100-hover)', - 'components-premium-badge-blue-glow-hover': 'var(--color-components-premium-badge-blue-glow-hover)', - 'components-premium-badge-blue-stroke-stop-0-hover': 'var(--color-components-premium-badge-blue-stroke-stop-0-hover)', - 'components-premium-badge-blue-stroke-stop-100-hover': 'var(--color-components-premium-badge-blue-stroke-stop-100-hover)', - - 'components-premium-badge-highlight-stop-0': 'var(--color-components-premium-badge-highlight-stop-0)', - 'components-premium-badge-highlight-stop-100': 'var(--color-components-premium-badge-highlight-stop-100)', - 'components-premium-badge-indigo-bg-stop-0': 'var(--color-components-premium-badge-indigo-bg-stop-0)', - 'components-premium-badge-indigo-bg-stop-100': 'var(--color-components-premium-badge-indigo-bg-stop-100)', - 'components-premium-badge-indigo-stroke-stop-0': 'var(--color-components-premium-badge-indigo-stroke-stop-0)', - 'components-premium-badge-indigo-stroke-stop-100': 'var(--color-components-premium-badge-indigo-stroke-stop-100)', - 'components-premium-badge-indigo-text-stop-0': 'var(--color-components-premium-badge-indigo-text-stop-0)', - 'components-premium-badge-indigo-text-stop-100': 'var(--color-components-premium-badge-indigo-text-stop-100)', - 'components-premium-badge-indigo-glow': 'var(--color-components-premium-badge-indigo-glow)', - 'components-premium-badge-indigo-glow-hover': 'var(--color-components-premium-badge-indigo-glow-hover)', - 'components-premium-badge-indigo-bg-stop-0-hover': 'var(--color-components-premium-badge-indigo-bg-stop-0-hover)', - 'components-premium-badge-indigo-bg-stop-100-hover': 'var(--color-components-premium-badge-indigo-bg-stop-100-hover)', - 'components-premium-badge-indigo-stroke-stop-0-hover': 'var(--color-components-premium-badge-indigo-stroke-stop-0-hover)', - 'components-premium-badge-indigo-stroke-stop-100-hover': 'var(--color-components-premium-badge-indigo-stroke-stop-100-hover)', - - 'components-premium-badge-grey-bg-stop-0': 'var(--color-components-premium-badge-grey-bg-stop-0)', - 'components-premium-badge-grey-bg-stop-100': 'var(--color-components-premium-badge-grey-bg-stop-100)', - 'components-premium-badge-grey-stroke-stop-0': 'var(--color-components-premium-badge-grey-stroke-stop-0)', - 'components-premium-badge-grey-stroke-stop-100': 'var(--color-components-premium-badge-grey-stroke-stop-100)', - 'components-premium-badge-grey-text-stop-0': 'var(--color-components-premium-badge-grey-text-stop-0)', - 'components-premium-badge-grey-text-stop-100': 'var(--color-components-premium-badge-grey-text-stop-100)', - 'components-premium-badge-grey-glow': 'var(--color-components-premium-badge-grey-glow)', - 'components-premium-badge-grey-glow-hover': 'var(--color-components-premium-badge-grey-glow-hover)', - 'components-premium-badge-grey-bg-stop-0-hover': 'var(--color-components-premium-badge-grey-bg-stop-0-hover)', - 'components-premium-badge-grey-bg-stop-100-hover': 'var(--color-components-premium-badge-grey-bg-stop-100-hover)', - 'components-premium-badge-grey-stroke-stop-0-hover': 'var(--color-components-premium-badge-grey-stroke-stop-0-hover)', - 'components-premium-badge-grey-stroke-stop-100-hover': 'var(--color-components-premium-badge-grey-stroke-stop-100-hover)', - - 'components-premium-badge-orange-bg-stop-0': 'var(--color-components-premium-badge-orange-bg-stop-0)', - 'components-premium-badge-orange-bg-stop-100': 'var(--color-components-premium-badge-orange-bg-stop-100)', - 'components-premium-badge-orange-stroke-stop-0': 'var(--color-components-premium-badge-orange-stroke-stop-0)', - 'components-premium-badge-orange-stroke-stop-100': 'var(--color-components-premium-badge-orange-stroke-stop-100)', - 'components-premium-badge-orange-text-stop-0': 'var(--color-components-premium-badge-orange-text-stop-0)', - 'components-premium-badge-orange-text-stop-100': 'var(--color-components-premium-badge-orange-text-stop-100)', - 'components-premium-badge-orange-glow': 'var(--color-components-premium-badge-orange-glow)', - 'components-premium-badge-orange-glow-hover': 'var(--color-components-premium-badge-orange-glow-hover)', - 'components-premium-badge-orange-bg-stop-0-hover': 'var(--color-components-premium-badge-orange-bg-stop-0-hover)', - 'components-premium-badge-orange-bg-stop-100-hover': 'var(--color-components-premium-badge-orange-bg-stop-100-hover)', - 'components-premium-badge-orange-stroke-stop-0-hover': 'var(--color-components-premium-badge-orange-stroke-stop-0-hover)', - 'components-premium-badge-orange-stroke-stop-100-hover': 'var(--color-components-premium-badge-orange-stroke-stop-100-hover)', - - 'components-progress-bar-bg': 'var(--color-components-progress-bar-bg)', - 'components-progress-bar-progress': 'var(--color-components-progress-bar-progress)', - 'components-progress-bar-border': 'var(--color-components-progress-bar-border)', - 'components-progress-bar-progress-solid': 'var(--color-components-progress-bar-progress-solid)', - 'components-progress-bar-progress-highlight': 'var(--color-components-progress-bar-progress-highlight)', - - 'components-icon-bg-red-solid': 'var(--color-components-icon-bg-red-solid)', - 'components-icon-bg-rose-solid': 'var(--color-components-icon-bg-rose-solid)', - 'components-icon-bg-pink-solid': 'var(--color-components-icon-bg-pink-solid)', - 'components-icon-bg-orange-dark-solid': 'var(--color-components-icon-bg-orange-dark-solid)', - 'components-icon-bg-yellow-solid': 'var(--color-components-icon-bg-yellow-solid)', - 'components-icon-bg-green-solid': 'var(--color-components-icon-bg-green-solid)', - 'components-icon-bg-teal-solid': 'var(--color-components-icon-bg-teal-solid)', - 'components-icon-bg-blue-light-solid': 'var(--color-components-icon-bg-blue-light-solid)', - 'components-icon-bg-blue-solid': 'var(--color-components-icon-bg-blue-solid)', - 'components-icon-bg-indigo-solid': 'var(--color-components-icon-bg-indigo-solid)', - 'components-icon-bg-violet-solid': 'var(--color-components-icon-bg-violet-solid)', - 'components-icon-bg-midnight-solid': 'var(--color-components-icon-bg-midnight-solid)', - 'components-icon-bg-rose-soft': 'var(--color-components-icon-bg-rose-soft)', - 'components-icon-bg-pink-soft': 'var(--color-components-icon-bg-pink-soft)', - 'components-icon-bg-orange-dark-soft': 'var(--color-components-icon-bg-orange-dark-soft)', - 'components-icon-bg-yellow-soft': 'var(--color-components-icon-bg-yellow-soft)', - 'components-icon-bg-green-soft': 'var(--color-components-icon-bg-green-soft)', - 'components-icon-bg-teal-soft': 'var(--color-components-icon-bg-teal-soft)', - 'components-icon-bg-blue-light-soft': 'var(--color-components-icon-bg-blue-light-soft)', - 'components-icon-bg-blue-soft': 'var(--color-components-icon-bg-blue-soft)', - 'components-icon-bg-indigo-soft': 'var(--color-components-icon-bg-indigo-soft)', - 'components-icon-bg-violet-soft': 'var(--color-components-icon-bg-violet-soft)', - 'components-icon-bg-midnight-soft': 'var(--color-components-icon-bg-midnight-soft)', - 'components-icon-bg-red-soft': 'var(--color-components-icon-bg-red-soft)', - 'components-icon-bg-orange-solid': 'var(--color-components-icon-bg-orange-solid)', - 'components-icon-bg-orange-soft': 'var(--color-components-icon-bg-orange-soft)', - - 'components-marketplace-header-bg': 'var(--color-components-marketplace-header-bg)', - - 'text-primary': 'var(--color-text-primary)', - 'text-secondary': 'var(--color-text-secondary)', - 'text-tertiary': 'var(--color-text-tertiary)', - 'text-quaternary': 'var(--color-text-quaternary)', - 'text-destructive': 'var(--color-text-destructive)', - 'text-success': 'var(--color-text-success)', - 'text-warning': 'var(--color-text-warning)', - 'text-destructive-secondary': 'var(--color-text-destructive-secondary)', - 'text-success-secondary': 'var(--color-text-success-secondary)', - 'text-warning-secondary': 'var(--color-text-warning-secondary)', - 'text-accent': 'var(--color-text-accent)', - 'text-primary-on-surface': 'var(--color-text-primary-on-surface)', - 'text-placeholder': 'var(--color-text-placeholder)', - 'text-disabled': 'var(--color-text-disabled)', - 'text-accent-secondary': 'var(--color-text-accent-secondary)', - 'text-accent-light-mode-only': 'var(--color-text-accent-light-mode-only)', - 'text-text-selected': 'var(--color-text-text-selected)', - 'text-secondary-on-surface': 'var(--color-text-secondary-on-surface)', - 'text-logo-text': 'var(--color-text-logo-text)', - 'text-empty-state-icon': 'var(--color-text-empty-state-icon)', - 'text-inverted': 'var(--color-text-inverted)', - 'text-inverted-dimmed': 'var(--color-text-inverted-dimmed)', - - 'background-body': 'var(--color-background-body)', - 'background-default-subtle': 'var(--color-background-default-subtle)', - 'background-neutral-subtle': 'var(--color-background-neutral-subtle)', - 'background-sidenav-bg': 'var(--color-background-sidenav-bg)', - 'background-default': 'var(--color-background-default)', - 'background-soft': 'var(--color-background-soft)', - 'background-gradient-bg-fill-chat-bg-1': 'var(--color-background-gradient-bg-fill-chat-bg-1)', - 'background-gradient-bg-fill-chat-bg-2': 'var(--color-background-gradient-bg-fill-chat-bg-2)', - 'background-gradient-bg-fill-chat-bubble-bg-1': 'var(--color-background-gradient-bg-fill-chat-bubble-bg-1)', - 'background-gradient-bg-fill-chat-bubble-bg-2': 'var(--color-background-gradient-bg-fill-chat-bubble-bg-2)', - 'background-gradient-bg-fill-debug-bg-1': 'var(--color-background-gradient-bg-fill-debug-bg-1)', - 'background-gradient-bg-fill-debug-bg-2': 'var(--color-background-gradient-bg-fill-debug-bg-2)', - - 'background-gradient-mask-gray': 'var(--color-background-gradient-mask-gray)', - 'background-gradient-mask-transparent': 'var(--color-background-gradient-mask-transparent)', - 'background-gradient-mask-input-clear-2': 'var(--color-background-gradient-mask-input-clear-2)', - 'background-gradient-mask-input-clear-1': 'var(--color-background-gradient-mask-input-clear-1)', - 'background-gradient-mask-transparent-dark': 'var(--color-background-gradient-mask-transparent-dark)', - 'background-gradient-mask-side-panel-2': 'var(--color-background-gradient-mask-side-panel-2)', - 'background-gradient-mask-side-panel-1': 'var(--color-background-gradient-mask-side-panel-1)', - - 'background-default-burn': 'var(--color-background-default-burn)', - 'background-overlay-fullscreen': 'var(--color-background-overlay-fullscreen)', - 'background-default-lighter': 'var(--color-background-default-lighter)', - 'background-section': 'var(--color-background-section)', - 'background-interaction-from-bg-1': 'var(--color-background-interaction-from-bg-1)', - 'background-interaction-from-bg-2': 'var(--color-background-interaction-from-bg-2)', - 'background-section-burn': 'var(--color-background-section-burn)', - 'background-default-dodge': 'var(--color-background-default-dodge)', - 'background-overlay': 'var(--color-background-overlay)', - 'background-default-dimmed': 'var(--color-background-default-dimmed)', - 'background-default-hover': 'var(--color-background-default-hover)', - 'background-overlay-alt': 'var(--color-background-overlay-alt)', - 'background-surface-white': 'var(--color-background-surface-white)', - 'background-overlay-destructive': 'var(--color-background-overlay-destructive)', - 'background-overlay-backdrop': 'var(--color-background-overlay-backdrop)', - 'background-body-transparent': 'var(--color-background-body-transparent)', - 'background-section-burn-inverted': 'var(--color-background-section-burn-inverted)', - 'background-default-hover-alpha-0': 'var(--color-background-default-hover-alpha-0)', - - 'shadow-shadow-1': 'var(--color-shadow-shadow-1)', - 'shadow-shadow-3': 'var(--color-shadow-shadow-3)', - 'shadow-shadow-4': 'var(--color-shadow-shadow-4)', - 'shadow-shadow-5': 'var(--color-shadow-shadow-5)', - 'shadow-shadow-6': 'var(--color-shadow-shadow-6)', - 'shadow-shadow-7': 'var(--color-shadow-shadow-7)', - 'shadow-shadow-8': 'var(--color-shadow-shadow-8)', - 'shadow-shadow-9': 'var(--color-shadow-shadow-9)', - 'shadow-shadow-2': 'var(--color-shadow-shadow-2)', - 'shadow-shadow-10': 'var(--color-shadow-shadow-10)', - - 'workflow-block-border': 'var(--color-workflow-block-border)', - 'workflow-block-parma-bg': 'var(--color-workflow-block-parma-bg)', - 'workflow-block-bg': 'var(--color-workflow-block-bg)', - 'workflow-block-bg-transparent': 'var(--color-workflow-block-bg-transparent)', - 'workflow-block-border-highlight': 'var(--color-workflow-block-border-highlight)', - 'workflow-block-wrapper-bg-1': 'var(--color-workflow-block-wrapper-bg-1)', - 'workflow-block-wrapper-bg-2': 'var(--color-workflow-block-wrapper-bg-2)', - - 'workflow-canvas-workflow-dot-color': 'var(--color-workflow-canvas-workflow-dot-color)', - 'workflow-canvas-workflow-bg': 'var(--color-workflow-canvas-workflow-bg)', - 'workflow-canvas-workflow-top-bar-1': 'var(--color-workflow-canvas-workflow-top-bar-1)', - 'workflow-canvas-workflow-top-bar-2': 'var(--color-workflow-canvas-workflow-top-bar-2)', - 'workflow-canvas-canvas-overlay': 'var(--color-workflow-canvas-canvas-overlay)', - - 'workflow-link-line-active': 'var(--color-workflow-link-line-active)', - 'workflow-link-line-normal': 'var(--color-workflow-link-line-normal)', - 'workflow-link-line-handle': 'var(--color-workflow-link-line-handle)', - 'workflow-link-line-normal-transparent': 'var(--color-workflow-link-line-normal-transparent)', - 'workflow-link-line-failure-active': 'var(--color-workflow-link-line-failure-active)', - 'workflow-link-line-failure-handle': 'var(--color-workflow-link-line-failure-handle)', - 'workflow-link-line-failure-button-bg': 'var(--color-workflow-link-line-failure-button-bg)', - 'workflow-link-line-failure-button-hover': 'var(--color-workflow-link-line-failure-button-hover)', - - 'workflow-link-line-success-active': 'var(--color-workflow-link-line-success-active)', - 'workflow-link-line-success-handle': 'var(--color-workflow-link-line-success-handle)', - - 'workflow-link-line-error-active': 'var(--color-workflow-link-line-error-active)', - 'workflow-link-line-error-handle': 'var(--color-workflow-link-line-error-handle)', - - 'workflow-minimap-bg': 'var(--color-workflow-minimap-bg)', - 'workflow-minimap-block': 'var(--color-workflow-minimap-block)', - - 'workflow-display-success-bg': 'var(--color-workflow-display-success-bg)', - 'workflow-display-success-border-1': 'var(--color-workflow-display-success-border-1)', - 'workflow-display-success-border-2': 'var(--color-workflow-display-success-border-2)', - 'workflow-display-success-vignette-color': 'var(--color-workflow-display-success-vignette-color)', - 'workflow-display-success-bg-line-pattern': 'var(--color-workflow-display-success-bg-line-pattern)', - - 'workflow-display-glass-1': 'var(--color-workflow-display-glass-1)', - 'workflow-display-glass-2': 'var(--color-workflow-display-glass-2)', - 'workflow-display-vignette-dark': 'var(--color-workflow-display-vignette-dark)', - 'workflow-display-highlight': 'var(--color-workflow-display-highlight)', - 'workflow-display-outline': 'var(--color-workflow-display-outline)', - 'workflow-display-error-bg': 'var(--color-workflow-display-error-bg)', - 'workflow-display-error-bg-line-pattern': 'var(--color-workflow-display-error-bg-line-pattern)', - 'workflow-display-error-border-1': 'var(--color-workflow-display-error-border-1)', - 'workflow-display-error-border-2': 'var(--color-workflow-display-error-border-2)', - 'workflow-display-error-vignette-color': 'var(--color-workflow-display-error-vignette-color)', - - 'workflow-display-warning-bg': 'var(--color-workflow-display-warning-bg)', - 'workflow-display-warning-bg-line-pattern': 'var(--color-workflow-display-warning-bg-line-pattern)', - 'workflow-display-warning-border-1': 'var(--color-workflow-display-warning-border-1)', - 'workflow-display-warning-border-2': 'var(--color-workflow-display-warning-border-2)', - 'workflow-display-warning-vignette-color': 'var(--color-workflow-display-warning-vignette-color)', - - 'workflow-display-normal-bg': 'var(--color-workflow-display-normal-bg)', - 'workflow-display-normal-bg-line-pattern': 'var(--color-workflow-display-normal-bg-line-pattern)', - 'workflow-display-normal-border-1': 'var(--color-workflow-display-normal-border-1)', - 'workflow-display-normal-border-2': 'var(--color-workflow-display-normal-border-2)', - 'workflow-display-normal-vignette-color': 'var(--color-workflow-display-normal-vignette-color)', - - 'workflow-display-disabled-bg': 'var(--color-workflow-display-disabled-bg)', - 'workflow-display-disabled-bg-line-pattern': 'var(--color-workflow-display-disabled-bg-line-pattern)', - 'workflow-display-disabled-border-1': 'var(--color-workflow-display-disabled-border-1)', - 'workflow-display-disabled-border-2': 'var(--color-workflow-display-disabled-border-2)', - 'workflow-display-disabled-vignette-color': 'var(--color-workflow-display-disabled-vignette-color)', - 'workflow-display-disabled-outline': 'var(--color-workflow-display-disabled-outline)', - - 'workflow-workflow-progress-bg-1': 'var(--color-workflow-workflow-progress-bg-1)', - 'workflow-workflow-progress-bg-2': 'var(--color-workflow-workflow-progress-bg-2)', - - 'workflow-debug-run-status-bg': 'var(--color-workflow-debug-run-status-bg)', - 'workflow-debug-breakpoint': 'var(--color-workflow-debug-breakpoint)', - 'workflow-debug-text': 'var(--color-workflow-debug-text)', - 'workflow-debug-text-disabled': 'var(--color-workflow-debug-text-disabled)', - 'workflow-debug-run-status-bg-alt': 'var(--color-workflow-debug-run-status-bg-alt)', - - 'workflow-test-run-run-status-bg': 'var(--color-workflow-test-run-run-status-bg)', - 'workflow-test-run-text': 'var(--color-workflow-test-run-text)', - 'workflow-test-run-run-status-bg-alt': 'var(--color-workflow-test-run-run-status-bg-alt)', - 'workflow-test-run-paused-bg': 'var(--color-workflow-test-run-paused-bg)', - 'workflow-test-run-paused-text': 'var(--color-workflow-test-run-paused-text)', - - 'divider-subtle': 'var(--color-divider-subtle)', - 'divider-regular': 'var(--color-divider-regular)', - 'divider-deep': 'var(--color-divider-deep)', - 'divider-burn': 'var(--color-divider-burn)', - 'divider-intense': 'var(--color-divider-intense)', - 'divider-solid': 'var(--color-divider-solid)', - 'divider-solid-alt': 'var(--color-divider-solid-alt)', - 'divider-accent': 'var(--color-divider-accent)', - - 'state-base-hover': 'var(--color-state-base-hover)', - 'state-base-active': 'var(--color-state-base-active)', - 'state-base-hover-alt': 'var(--color-state-base-hover-alt)', - 'state-base-handle': 'var(--color-state-base-handle)', - 'state-base-handle-hover': 'var(--color-state-base-handle-hover)', - 'state-base-hover-subtle': 'var(--color-state-base-hover-subtle)', - - 'state-accent-hover': 'var(--color-state-accent-hover)', - 'state-accent-active': 'var(--color-state-accent-active)', - 'state-accent-hover-alt': 'var(--color-state-accent-hover-alt)', - 'state-accent-solid': 'var(--color-state-accent-solid)', - 'state-accent-active-alt': 'var(--color-state-accent-active-alt)', - - 'state-destructive-hover': 'var(--color-state-destructive-hover)', - 'state-destructive-hover-alt': 'var(--color-state-destructive-hover-alt)', - 'state-destructive-active': 'var(--color-state-destructive-active)', - 'state-destructive-solid': 'var(--color-state-destructive-solid)', - 'state-destructive-border': 'var(--color-state-destructive-border)', - 'state-destructive-hover-transparent': 'var(--color-state-destructive-hover-transparent)', - - 'state-success-hover': 'var(--color-state-success-hover)', - 'state-success-hover-alt': 'var(--color-state-success-hover-alt)', - 'state-success-active': 'var(--color-state-success-active)', - 'state-success-solid': 'var(--color-state-success-solid)', - - 'state-warning-hover': 'var(--color-state-warning-hover)', - 'state-warning-hover-alt': 'var(--color-state-warning-hover-alt)', - 'state-warning-active': 'var(--color-state-warning-active)', - 'state-warning-solid': 'var(--color-state-warning-solid)', - 'state-warning-hover-transparent': 'var(--color-state-warning-hover-transparent)', - - 'effects-highlight': 'var(--color-effects-highlight)', - 'effects-highlight-lightmode-off': 'var(--color-effects-highlight-lightmode-off)', - 'effects-image-frame': 'var(--color-effects-image-frame)', - 'effects-icon-border': 'var(--color-effects-icon-border)', - 'effects-highlight-subtle': 'var(--color-effects-highlight-subtle)', - - 'util-colors-orange-dark-orange-dark-50': 'var(--color-util-colors-orange-dark-orange-dark-50)', - 'util-colors-orange-dark-orange-dark-100': 'var(--color-util-colors-orange-dark-orange-dark-100)', - 'util-colors-orange-dark-orange-dark-200': 'var(--color-util-colors-orange-dark-orange-dark-200)', - 'util-colors-orange-dark-orange-dark-300': 'var(--color-util-colors-orange-dark-orange-dark-300)', - 'util-colors-orange-dark-orange-dark-400': 'var(--color-util-colors-orange-dark-orange-dark-400)', - 'util-colors-orange-dark-orange-dark-500': 'var(--color-util-colors-orange-dark-orange-dark-500)', - 'util-colors-orange-dark-orange-dark-600': 'var(--color-util-colors-orange-dark-orange-dark-600)', - 'util-colors-orange-dark-orange-dark-700': 'var(--color-util-colors-orange-dark-orange-dark-700)', - - 'util-colors-orange-orange-50': 'var(--color-util-colors-orange-orange-50)', - 'util-colors-orange-orange-100': 'var(--color-util-colors-orange-orange-100)', - 'util-colors-orange-orange-200': 'var(--color-util-colors-orange-orange-200)', - 'util-colors-orange-orange-300': 'var(--color-util-colors-orange-orange-300)', - 'util-colors-orange-orange-400': 'var(--color-util-colors-orange-orange-400)', - 'util-colors-orange-orange-500': 'var(--color-util-colors-orange-orange-500)', - 'util-colors-orange-orange-600': 'var(--color-util-colors-orange-orange-600)', - 'util-colors-orange-orange-700': 'var(--color-util-colors-orange-orange-700)', - 'util-colors-orange-orange-100-transparent': 'var(--color-util-colors-orange-orange-100-transparent)', - - 'util-colors-pink-pink-50': 'var(--color-util-colors-pink-pink-50)', - 'util-colors-pink-pink-100': 'var(--color-util-colors-pink-pink-100)', - 'util-colors-pink-pink-200': 'var(--color-util-colors-pink-pink-200)', - 'util-colors-pink-pink-300': 'var(--color-util-colors-pink-pink-300)', - 'util-colors-pink-pink-400': 'var(--color-util-colors-pink-pink-400)', - 'util-colors-pink-pink-500': 'var(--color-util-colors-pink-pink-500)', - 'util-colors-pink-pink-600': 'var(--color-util-colors-pink-pink-600)', - 'util-colors-pink-pink-700': 'var(--color-util-colors-pink-pink-700)', - - 'util-colors-fuchsia-fuchsia-50': 'var(--color-util-colors-fuchsia-fuchsia-50)', - 'util-colors-fuchsia-fuchsia-100': 'var(--color-util-colors-fuchsia-fuchsia-100)', - 'util-colors-fuchsia-fuchsia-200': 'var(--color-util-colors-fuchsia-fuchsia-200)', - 'util-colors-fuchsia-fuchsia-300': 'var(--color-util-colors-fuchsia-fuchsia-300)', - 'util-colors-fuchsia-fuchsia-400': 'var(--color-util-colors-fuchsia-fuchsia-400)', - 'util-colors-fuchsia-fuchsia-500': 'var(--color-util-colors-fuchsia-fuchsia-500)', - 'util-colors-fuchsia-fuchsia-600': 'var(--color-util-colors-fuchsia-fuchsia-600)', - 'util-colors-fuchsia-fuchsia-700': 'var(--color-util-colors-fuchsia-fuchsia-700)', - - 'util-colors-purple-purple-50': 'var(--color-util-colors-purple-purple-50)', - 'util-colors-purple-purple-100': 'var(--color-util-colors-purple-purple-100)', - 'util-colors-purple-purple-200': 'var(--color-util-colors-purple-purple-200)', - 'util-colors-purple-purple-300': 'var(--color-util-colors-purple-purple-300)', - 'util-colors-purple-purple-400': 'var(--color-util-colors-purple-purple-400)', - 'util-colors-purple-purple-500': 'var(--color-util-colors-purple-purple-500)', - 'util-colors-purple-purple-600': 'var(--color-util-colors-purple-purple-600)', - 'util-colors-purple-purple-700': 'var(--color-util-colors-purple-purple-700)', - - 'util-colors-indigo-indigo-50': 'var(--color-util-colors-indigo-indigo-50)', - 'util-colors-indigo-indigo-100': 'var(--color-util-colors-indigo-indigo-100)', - 'util-colors-indigo-indigo-200': 'var(--color-util-colors-indigo-indigo-200)', - 'util-colors-indigo-indigo-300': 'var(--color-util-colors-indigo-indigo-300)', - 'util-colors-indigo-indigo-400': 'var(--color-util-colors-indigo-indigo-400)', - 'util-colors-indigo-indigo-500': 'var(--color-util-colors-indigo-indigo-500)', - 'util-colors-indigo-indigo-600': 'var(--color-util-colors-indigo-indigo-600)', - 'util-colors-indigo-indigo-700': 'var(--color-util-colors-indigo-indigo-700)', - - 'util-colors-blue-blue-50': 'var(--color-util-colors-blue-blue-50)', - 'util-colors-blue-blue-100': 'var(--color-util-colors-blue-blue-100)', - 'util-colors-blue-blue-200': 'var(--color-util-colors-blue-blue-200)', - 'util-colors-blue-blue-300': 'var(--color-util-colors-blue-blue-300)', - 'util-colors-blue-blue-400': 'var(--color-util-colors-blue-blue-400)', - 'util-colors-blue-blue-500': 'var(--color-util-colors-blue-blue-500)', - 'util-colors-blue-blue-600': 'var(--color-util-colors-blue-blue-600)', - 'util-colors-blue-blue-700': 'var(--color-util-colors-blue-blue-700)', - - 'util-colors-blue-light-blue-light-50': 'var(--color-util-colors-blue-light-blue-light-50)', - 'util-colors-blue-light-blue-light-100': 'var(--color-util-colors-blue-light-blue-light-100)', - 'util-colors-blue-light-blue-light-200': 'var(--color-util-colors-blue-light-blue-light-200)', - 'util-colors-blue-light-blue-light-300': 'var(--color-util-colors-blue-light-blue-light-300)', - 'util-colors-blue-light-blue-light-400': 'var(--color-util-colors-blue-light-blue-light-400)', - 'util-colors-blue-light-blue-light-500': 'var(--color-util-colors-blue-light-blue-light-500)', - 'util-colors-blue-light-blue-light-600': 'var(--color-util-colors-blue-light-blue-light-600)', - 'util-colors-blue-light-blue-light-700': 'var(--color-util-colors-blue-light-blue-light-700)', - - 'util-colors-gray-blue-gray-blue-50': 'var(--color-util-colors-gray-blue-gray-blue-50)', - 'util-colors-gray-blue-gray-blue-100': 'var(--color-util-colors-gray-blue-gray-blue-100)', - 'util-colors-gray-blue-gray-blue-200': 'var(--color-util-colors-gray-blue-gray-blue-200)', - 'util-colors-gray-blue-gray-blue-300': 'var(--color-util-colors-gray-blue-gray-blue-300)', - 'util-colors-gray-blue-gray-blue-400': 'var(--color-util-colors-gray-blue-gray-blue-400)', - 'util-colors-gray-blue-gray-blue-500': 'var(--color-util-colors-gray-blue-gray-blue-500)', - 'util-colors-gray-blue-gray-blue-600': 'var(--color-util-colors-gray-blue-gray-blue-600)', - 'util-colors-gray-blue-gray-blue-700': 'var(--color-util-colors-gray-blue-gray-blue-700)', - - 'util-colors-blue-brand-blue-brand-50': 'var(--color-util-colors-blue-brand-blue-brand-50)', - 'util-colors-blue-brand-blue-brand-100': 'var(--color-util-colors-blue-brand-blue-brand-100)', - 'util-colors-blue-brand-blue-brand-200': 'var(--color-util-colors-blue-brand-blue-brand-200)', - 'util-colors-blue-brand-blue-brand-300': 'var(--color-util-colors-blue-brand-blue-brand-300)', - 'util-colors-blue-brand-blue-brand-400': 'var(--color-util-colors-blue-brand-blue-brand-400)', - 'util-colors-blue-brand-blue-brand-500': 'var(--color-util-colors-blue-brand-blue-brand-500)', - 'util-colors-blue-brand-blue-brand-600': 'var(--color-util-colors-blue-brand-blue-brand-600)', - 'util-colors-blue-brand-blue-brand-700': 'var(--color-util-colors-blue-brand-blue-brand-700)', - - 'util-colors-red-red-50': 'var(--color-util-colors-red-red-50)', - 'util-colors-red-red-100': 'var(--color-util-colors-red-red-100)', - 'util-colors-red-red-200': 'var(--color-util-colors-red-red-200)', - 'util-colors-red-red-300': 'var(--color-util-colors-red-red-300)', - 'util-colors-red-red-400': 'var(--color-util-colors-red-red-400)', - 'util-colors-red-red-500': 'var(--color-util-colors-red-red-500)', - 'util-colors-red-red-600': 'var(--color-util-colors-red-red-600)', - 'util-colors-red-red-700': 'var(--color-util-colors-red-red-700)', - - 'util-colors-green-green-50': 'var(--color-util-colors-green-green-50)', - 'util-colors-green-green-100': 'var(--color-util-colors-green-green-100)', - 'util-colors-green-green-200': 'var(--color-util-colors-green-green-200)', - 'util-colors-green-green-300': 'var(--color-util-colors-green-green-300)', - 'util-colors-green-green-400': 'var(--color-util-colors-green-green-400)', - 'util-colors-green-green-500': 'var(--color-util-colors-green-green-500)', - 'util-colors-green-green-600': 'var(--color-util-colors-green-green-600)', - 'util-colors-green-green-700': 'var(--color-util-colors-green-green-700)', - - 'util-colors-warning-warning-50': 'var(--color-util-colors-warning-warning-50)', - 'util-colors-warning-warning-100': 'var(--color-util-colors-warning-warning-100)', - 'util-colors-warning-warning-200': 'var(--color-util-colors-warning-warning-200)', - 'util-colors-warning-warning-300': 'var(--color-util-colors-warning-warning-300)', - 'util-colors-warning-warning-400': 'var(--color-util-colors-warning-warning-400)', - 'util-colors-warning-warning-500': 'var(--color-util-colors-warning-warning-500)', - 'util-colors-warning-warning-600': 'var(--color-util-colors-warning-warning-600)', - 'util-colors-warning-warning-700': 'var(--color-util-colors-warning-warning-700)', - - 'util-colors-yellow-yellow-50': 'var(--color-util-colors-yellow-yellow-50)', - 'util-colors-yellow-yellow-100': 'var(--color-util-colors-yellow-yellow-100)', - 'util-colors-yellow-yellow-200': 'var(--color-util-colors-yellow-yellow-200)', - 'util-colors-yellow-yellow-300': 'var(--color-util-colors-yellow-yellow-300)', - 'util-colors-yellow-yellow-400': 'var(--color-util-colors-yellow-yellow-400)', - 'util-colors-yellow-yellow-500': 'var(--color-util-colors-yellow-yellow-500)', - 'util-colors-yellow-yellow-600': 'var(--color-util-colors-yellow-yellow-600)', - 'util-colors-yellow-yellow-700': 'var(--color-util-colors-yellow-yellow-700)', - - 'util-colors-teal-teal-50': 'var(--color-util-colors-teal-teal-50)', - 'util-colors-teal-teal-100': 'var(--color-util-colors-teal-teal-100)', - 'util-colors-teal-teal-200': 'var(--color-util-colors-teal-teal-200)', - 'util-colors-teal-teal-300': 'var(--color-util-colors-teal-teal-300)', - 'util-colors-teal-teal-400': 'var(--color-util-colors-teal-teal-400)', - 'util-colors-teal-teal-500': 'var(--color-util-colors-teal-teal-500)', - 'util-colors-teal-teal-600': 'var(--color-util-colors-teal-teal-600)', - 'util-colors-teal-teal-700': 'var(--color-util-colors-teal-teal-700)', - - 'util-colors-cyan-cyan-50': 'var(--color-util-colors-cyan-cyan-50)', - 'util-colors-cyan-cyan-100': 'var(--color-util-colors-cyan-cyan-100)', - 'util-colors-cyan-cyan-200': 'var(--color-util-colors-cyan-cyan-200)', - 'util-colors-cyan-cyan-300': 'var(--color-util-colors-cyan-cyan-300)', - 'util-colors-cyan-cyan-400': 'var(--color-util-colors-cyan-cyan-400)', - 'util-colors-cyan-cyan-500': 'var(--color-util-colors-cyan-cyan-500)', - 'util-colors-cyan-cyan-600': 'var(--color-util-colors-cyan-cyan-600)', - 'util-colors-cyan-cyan-700': 'var(--color-util-colors-cyan-cyan-700)', - - 'util-colors-violet-violet-50': 'var(--color-util-colors-violet-violet-50)', - 'util-colors-violet-violet-100': 'var(--color-util-colors-violet-violet-100)', - 'util-colors-violet-violet-200': 'var(--color-util-colors-violet-violet-200)', - 'util-colors-violet-violet-300': 'var(--color-util-colors-violet-violet-300)', - 'util-colors-violet-violet-400': 'var(--color-util-colors-violet-violet-400)', - 'util-colors-violet-violet-500': 'var(--color-util-colors-violet-violet-500)', - 'util-colors-violet-violet-600': 'var(--color-util-colors-violet-violet-600)', - 'util-colors-violet-violet-700': 'var(--color-util-colors-violet-violet-700)', - - 'util-colors-gray-gray-50': 'var(--color-util-colors-gray-gray-50)', - 'util-colors-gray-gray-100': 'var(--color-util-colors-gray-gray-100)', - 'util-colors-gray-gray-200': 'var(--color-util-colors-gray-gray-200)', - 'util-colors-gray-gray-300': 'var(--color-util-colors-gray-gray-300)', - 'util-colors-gray-gray-400': 'var(--color-util-colors-gray-gray-400)', - 'util-colors-gray-gray-500': 'var(--color-util-colors-gray-gray-500)', - 'util-colors-gray-gray-600': 'var(--color-util-colors-gray-gray-600)', - 'util-colors-gray-gray-700': 'var(--color-util-colors-gray-gray-700)', - - 'util-colors-green-light-green-light-50': 'var(--color-util-colors-green-light-green-light-50)', - 'util-colors-green-light-green-light-100': 'var(--color-util-colors-green-light-green-light-100)', - 'util-colors-green-light-green-light-200': 'var(--color-util-colors-green-light-green-light-200)', - 'util-colors-green-light-green-light-300': 'var(--color-util-colors-green-light-green-light-300)', - 'util-colors-green-light-green-light-500': 'var(--color-util-colors-green-light-green-light-500)', - 'util-colors-green-light-green-light-400': 'var(--color-util-colors-green-light-green-light-400)', - 'util-colors-green-light-green-light-600': 'var(--color-util-colors-green-light-green-light-600)', - 'util-colors-green-light-green-light-700': 'var(--color-util-colors-green-light-green-light-700)', - - 'util-colors-rose-rose-50': 'var(--color-util-colors-rose-rose-50)', - 'util-colors-rose-rose-100': 'var(--color-util-colors-rose-rose-100)', - 'util-colors-rose-rose-200': 'var(--color-util-colors-rose-rose-200)', - 'util-colors-rose-rose-300': 'var(--color-util-colors-rose-rose-300)', - 'util-colors-rose-rose-400': 'var(--color-util-colors-rose-rose-400)', - 'util-colors-rose-rose-500': 'var(--color-util-colors-rose-rose-500)', - 'util-colors-rose-rose-600': 'var(--color-util-colors-rose-rose-600)', - 'util-colors-rose-rose-700': 'var(--color-util-colors-rose-rose-700)', - - 'util-colors-midnight-midnight-50': 'var(--color-util-colors-midnight-midnight-50)', - 'util-colors-midnight-midnight-100': 'var(--color-util-colors-midnight-midnight-100)', - 'util-colors-midnight-midnight-200': 'var(--color-util-colors-midnight-midnight-200)', - 'util-colors-midnight-midnight-300': 'var(--color-util-colors-midnight-midnight-300)', - 'util-colors-midnight-midnight-400': 'var(--color-util-colors-midnight-midnight-400)', - 'util-colors-midnight-midnight-500': 'var(--color-util-colors-midnight-midnight-500)', - 'util-colors-midnight-midnight-600': 'var(--color-util-colors-midnight-midnight-600)', - 'util-colors-midnight-midnight-700': 'var(--color-util-colors-midnight-midnight-700)', - - 'third-party-LangChain': 'var(--color-third-party-LangChain)', - 'third-party-Langfuse': 'var(--color-third-party-Langfuse)', - 'third-party-Github': 'var(--color-third-party-Github)', - 'third-party-Github-tertiary': 'var(--color-third-party-Github-tertiary)', - 'third-party-Github-secondary': 'var(--color-third-party-Github-secondary)', - 'third-party-model-bg-openai': 'var(--color-third-party-model-bg-openai)', - 'third-party-model-bg-anthropic': 'var(--color-third-party-model-bg-anthropic)', - 'third-party-model-bg-default': 'var(--color-third-party-model-bg-default)', - - 'third-party-aws': 'var(--color-third-party-aws)', - 'third-party-aws-alt': 'var(--color-third-party-aws-alt)', - - 'saas-background': 'var(--color-saas-background)', - 'saas-pricing-grid-bg': 'var(--color-saas-pricing-grid-bg)', - 'saas-dify-blue-static': 'var(--color-saas-dify-blue-static)', - 'saas-dify-blue-static-hover': 'var(--color-saas-dify-blue-static-hover)', - 'saas-dify-blue-accessible': 'var(--color-saas-dify-blue-accessible)', - 'saas-dify-blue-inverted': 'var(--color-saas-dify-blue-inverted)', - 'saas-dify-blue-inverted-dimmed': 'var(--color-saas-dify-blue-inverted-dimmed)', - - 'saas-background-inverted': 'var(--color-saas-background-inverted)', - 'saas-background-inverted-hover': 'var(--color-saas-background-inverted-hover)', - - 'dify-logo-blue': 'var(--color-dify-logo-blue)', - 'dify-logo-black': 'var(--color-dify-logo-black)', - 'dify-logo-outline-1': 'var(--color-dify-logo-outline-1)', - 'dify-logo-outline-2': 'var(--color-dify-logo-outline-2)', - -} -export default vars diff --git a/packages/dify-ui/src/themes/theme.css b/packages/dify-ui/src/themes/theme.css new file mode 100644 index 0000000000..04e02d852c --- /dev/null +++ b/packages/dify-ui/src/themes/theme.css @@ -0,0 +1,772 @@ +/* Attention: Generated. Do not edit by hand. */ +/* + * Semantic design tokens. Each entry registers a Tailwind v4 utility + * (e.g. bg-components-input-bg-normal) that resolves to var(--color-...). + * Runtime values come from themes/light.css and themes/dark.css scoped + * under html[data-theme=...] — @theme inline avoids duplicating them at :root. + */ + +@theme inline { + --color-components-input-bg-normal: var(--color-components-input-bg-normal); + --color-components-input-text-placeholder: var(--color-components-input-text-placeholder); + --color-components-input-bg-hover: var(--color-components-input-bg-hover); + --color-components-input-bg-active: var(--color-components-input-bg-active); + --color-components-input-border-active: var(--color-components-input-border-active); + --color-components-input-border-destructive: var(--color-components-input-border-destructive); + --color-components-input-text-filled: var(--color-components-input-text-filled); + --color-components-input-bg-destructive: var(--color-components-input-bg-destructive); + --color-components-input-bg-disabled: var(--color-components-input-bg-disabled); + --color-components-input-text-disabled: var(--color-components-input-text-disabled); + --color-components-input-text-filled-disabled: var(--color-components-input-text-filled-disabled); + --color-components-input-border-hover: var(--color-components-input-border-hover); + --color-components-input-border-active-prompt-1: var(--color-components-input-border-active-prompt-1); + --color-components-input-border-active-prompt-2: var(--color-components-input-border-active-prompt-2); + + --color-components-kbd-bg-gray: var(--color-components-kbd-bg-gray); + --color-components-kbd-bg-white: var(--color-components-kbd-bg-white); + + --color-components-tooltip-bg: var(--color-components-tooltip-bg); + + --color-components-button-primary-text: var(--color-components-button-primary-text); + --color-components-button-primary-bg: var(--color-components-button-primary-bg); + --color-components-button-primary-border: var(--color-components-button-primary-border); + --color-components-button-primary-bg-hover: var(--color-components-button-primary-bg-hover); + --color-components-button-primary-border-hover: var(--color-components-button-primary-border-hover); + --color-components-button-primary-bg-disabled: var(--color-components-button-primary-bg-disabled); + --color-components-button-primary-border-disabled: var(--color-components-button-primary-border-disabled); + --color-components-button-primary-text-disabled: var(--color-components-button-primary-text-disabled); + + --color-components-button-secondary-text: var(--color-components-button-secondary-text); + --color-components-button-secondary-text-disabled: var(--color-components-button-secondary-text-disabled); + --color-components-button-secondary-bg: var(--color-components-button-secondary-bg); + --color-components-button-secondary-bg-hover: var(--color-components-button-secondary-bg-hover); + --color-components-button-secondary-bg-disabled: var(--color-components-button-secondary-bg-disabled); + --color-components-button-secondary-border: var(--color-components-button-secondary-border); + --color-components-button-secondary-border-hover: var(--color-components-button-secondary-border-hover); + --color-components-button-secondary-border-disabled: var(--color-components-button-secondary-border-disabled); + + --color-components-button-tertiary-text: var(--color-components-button-tertiary-text); + --color-components-button-tertiary-text-disabled: var(--color-components-button-tertiary-text-disabled); + --color-components-button-tertiary-bg: var(--color-components-button-tertiary-bg); + --color-components-button-tertiary-bg-hover: var(--color-components-button-tertiary-bg-hover); + --color-components-button-tertiary-bg-disabled: var(--color-components-button-tertiary-bg-disabled); + + --color-components-button-ghost-text: var(--color-components-button-ghost-text); + --color-components-button-ghost-text-disabled: var(--color-components-button-ghost-text-disabled); + --color-components-button-ghost-bg-hover: var(--color-components-button-ghost-bg-hover); + + --color-components-button-destructive-primary-text: var(--color-components-button-destructive-primary-text); + --color-components-button-destructive-primary-text-disabled: var(--color-components-button-destructive-primary-text-disabled); + --color-components-button-destructive-primary-bg: var(--color-components-button-destructive-primary-bg); + --color-components-button-destructive-primary-bg-hover: var(--color-components-button-destructive-primary-bg-hover); + --color-components-button-destructive-primary-bg-disabled: var(--color-components-button-destructive-primary-bg-disabled); + --color-components-button-destructive-primary-border: var(--color-components-button-destructive-primary-border); + --color-components-button-destructive-primary-border-hover: var(--color-components-button-destructive-primary-border-hover); + --color-components-button-destructive-primary-border-disabled: var(--color-components-button-destructive-primary-border-disabled); + + --color-components-button-destructive-secondary-text: var(--color-components-button-destructive-secondary-text); + --color-components-button-destructive-secondary-text-disabled: var(--color-components-button-destructive-secondary-text-disabled); + --color-components-button-destructive-secondary-bg: var(--color-components-button-destructive-secondary-bg); + --color-components-button-destructive-secondary-bg-hover: var(--color-components-button-destructive-secondary-bg-hover); + --color-components-button-destructive-secondary-bg-disabled: var(--color-components-button-destructive-secondary-bg-disabled); + --color-components-button-destructive-secondary-border: var(--color-components-button-destructive-secondary-border); + --color-components-button-destructive-secondary-border-hover: var(--color-components-button-destructive-secondary-border-hover); + --color-components-button-destructive-secondary-border-disabled: var(--color-components-button-destructive-secondary-border-disabled); + + --color-components-button-destructive-tertiary-text: var(--color-components-button-destructive-tertiary-text); + --color-components-button-destructive-tertiary-text-disabled: var(--color-components-button-destructive-tertiary-text-disabled); + --color-components-button-destructive-tertiary-bg: var(--color-components-button-destructive-tertiary-bg); + --color-components-button-destructive-tertiary-bg-hover: var(--color-components-button-destructive-tertiary-bg-hover); + --color-components-button-destructive-tertiary-bg-disabled: var(--color-components-button-destructive-tertiary-bg-disabled); + + --color-components-button-destructive-ghost-text: var(--color-components-button-destructive-ghost-text); + --color-components-button-destructive-ghost-text-disabled: var(--color-components-button-destructive-ghost-text-disabled); + --color-components-button-destructive-ghost-bg-hover: var(--color-components-button-destructive-ghost-bg-hover); + + --color-components-button-secondary-accent-text: var(--color-components-button-secondary-accent-text); + --color-components-button-secondary-accent-text-disabled: var(--color-components-button-secondary-accent-text-disabled); + --color-components-button-secondary-accent-bg: var(--color-components-button-secondary-accent-bg); + --color-components-button-secondary-accent-bg-hover: var(--color-components-button-secondary-accent-bg-hover); + --color-components-button-secondary-accent-bg-disabled: var(--color-components-button-secondary-accent-bg-disabled); + --color-components-button-secondary-accent-border: var(--color-components-button-secondary-accent-border); + --color-components-button-secondary-accent-border-hover: var(--color-components-button-secondary-accent-border-hover); + --color-components-button-secondary-accent-border-disabled: var(--color-components-button-secondary-accent-border-disabled); + + --color-components-button-indigo-bg: var(--color-components-button-indigo-bg); + --color-components-button-indigo-bg-hover: var(--color-components-button-indigo-bg-hover); + --color-components-button-indigo-bg-disabled: var(--color-components-button-indigo-bg-disabled); + + --color-components-checkbox-icon: var(--color-components-checkbox-icon); + --color-components-checkbox-icon-disabled: var(--color-components-checkbox-icon-disabled); + --color-components-checkbox-bg: var(--color-components-checkbox-bg); + --color-components-checkbox-bg-hover: var(--color-components-checkbox-bg-hover); + --color-components-checkbox-bg-disabled: var(--color-components-checkbox-bg-disabled); + --color-components-checkbox-border: var(--color-components-checkbox-border); + --color-components-checkbox-border-hover: var(--color-components-checkbox-border-hover); + --color-components-checkbox-border-disabled: var(--color-components-checkbox-border-disabled); + --color-components-checkbox-bg-unchecked: var(--color-components-checkbox-bg-unchecked); + --color-components-checkbox-bg-unchecked-hover: var(--color-components-checkbox-bg-unchecked-hover); + --color-components-checkbox-bg-disabled-checked: var(--color-components-checkbox-bg-disabled-checked); + + --color-components-radio-border-checked: var(--color-components-radio-border-checked); + --color-components-radio-border-checked-hover: var(--color-components-radio-border-checked-hover); + --color-components-radio-border-checked-disabled: var(--color-components-radio-border-checked-disabled); + --color-components-radio-bg-disabled: var(--color-components-radio-bg-disabled); + --color-components-radio-border: var(--color-components-radio-border); + --color-components-radio-border-hover: var(--color-components-radio-border-hover); + --color-components-radio-border-disabled: var(--color-components-radio-border-disabled); + --color-components-radio-bg: var(--color-components-radio-bg); + --color-components-radio-bg-hover: var(--color-components-radio-bg-hover); + + --color-components-toggle-knob: var(--color-components-toggle-knob); + --color-components-toggle-knob-disabled: var(--color-components-toggle-knob-disabled); + --color-components-toggle-bg: var(--color-components-toggle-bg); + --color-components-toggle-bg-hover: var(--color-components-toggle-bg-hover); + --color-components-toggle-bg-disabled: var(--color-components-toggle-bg-disabled); + --color-components-toggle-bg-unchecked: var(--color-components-toggle-bg-unchecked); + --color-components-toggle-bg-unchecked-hover: var(--color-components-toggle-bg-unchecked-hover); + --color-components-toggle-bg-unchecked-disabled: var(--color-components-toggle-bg-unchecked-disabled); + --color-components-toggle-knob-hover: var(--color-components-toggle-knob-hover); + + --color-components-card-bg: var(--color-components-card-bg); + --color-components-card-border: var(--color-components-card-border); + --color-components-card-bg-alt: var(--color-components-card-bg-alt); + --color-components-card-bg-transparent: var(--color-components-card-bg-transparent); + --color-components-card-bg-alt-transparent: var(--color-components-card-bg-alt-transparent); + + --color-components-menu-item-text: var(--color-components-menu-item-text); + --color-components-menu-item-text-active: var(--color-components-menu-item-text-active); + --color-components-menu-item-text-hover: var(--color-components-menu-item-text-hover); + --color-components-menu-item-text-active-accent: var(--color-components-menu-item-text-active-accent); + --color-components-menu-item-bg-active: var(--color-components-menu-item-bg-active); + --color-components-menu-item-bg-hover: var(--color-components-menu-item-bg-hover); + + --color-components-panel-bg: var(--color-components-panel-bg); + --color-components-panel-bg-blur: var(--color-components-panel-bg-blur); + --color-components-panel-border: var(--color-components-panel-border); + --color-components-panel-border-subtle: var(--color-components-panel-border-subtle); + --color-components-panel-gradient-2: var(--color-components-panel-gradient-2); + --color-components-panel-gradient-1: var(--color-components-panel-gradient-1); + --color-components-panel-bg-alt: var(--color-components-panel-bg-alt); + --color-components-panel-on-panel-item-bg: var(--color-components-panel-on-panel-item-bg); + --color-components-panel-on-panel-item-bg-hover: var(--color-components-panel-on-panel-item-bg-hover); + --color-components-panel-on-panel-item-bg-alt: var(--color-components-panel-on-panel-item-bg-alt); + --color-components-panel-on-panel-item-bg-transparent: var(--color-components-panel-on-panel-item-bg-transparent); + --color-components-panel-on-panel-item-bg-hover-transparent: var(--color-components-panel-on-panel-item-bg-hover-transparent); + --color-components-panel-on-panel-item-bg-destructive-hover-transparent: var(--color-components-panel-on-panel-item-bg-destructive-hover-transparent); + + --color-components-panel-bg-transparent: var(--color-components-panel-bg-transparent); + + --color-components-main-nav-nav-button-text: var(--color-components-main-nav-nav-button-text); + --color-components-main-nav-nav-button-text-active: var(--color-components-main-nav-nav-button-text-active); + --color-components-main-nav-nav-button-bg: var(--color-components-main-nav-nav-button-bg); + --color-components-main-nav-nav-button-bg-active: var(--color-components-main-nav-nav-button-bg-active); + --color-components-main-nav-nav-button-border: var(--color-components-main-nav-nav-button-border); + --color-components-main-nav-nav-button-bg-hover: var(--color-components-main-nav-nav-button-bg-hover); + + --color-components-main-nav-nav-user-border: var(--color-components-main-nav-nav-user-border); + + --color-components-slider-knob: var(--color-components-slider-knob); + --color-components-slider-knob-hover: var(--color-components-slider-knob-hover); + --color-components-slider-knob-disabled: var(--color-components-slider-knob-disabled); + --color-components-slider-range: var(--color-components-slider-range); + --color-components-slider-track: var(--color-components-slider-track); + --color-components-slider-knob-border-hover: var(--color-components-slider-knob-border-hover); + --color-components-slider-knob-border: var(--color-components-slider-knob-border); + + --color-components-segmented-control-item-active-bg: var(--color-components-segmented-control-item-active-bg); + --color-components-segmented-control-item-active-border: var(--color-components-segmented-control-item-active-border); + --color-components-segmented-control-bg-normal: var(--color-components-segmented-control-bg-normal); + --color-components-segmented-control-item-active-accent-bg: var(--color-components-segmented-control-item-active-accent-bg); + --color-components-segmented-control-item-active-accent-border: var(--color-components-segmented-control-item-active-accent-border); + + --color-components-option-card-option-bg: var(--color-components-option-card-option-bg); + --color-components-option-card-option-selected-bg: var(--color-components-option-card-option-selected-bg); + --color-components-option-card-option-selected-border: var(--color-components-option-card-option-selected-border); + --color-components-option-card-option-border: var(--color-components-option-card-option-border); + --color-components-option-card-option-bg-hover: var(--color-components-option-card-option-bg-hover); + --color-components-option-card-option-border-hover: var(--color-components-option-card-option-border-hover); + + --color-components-tab-active: var(--color-components-tab-active); + + --color-components-badge-white-to-dark: var(--color-components-badge-white-to-dark); + --color-components-badge-status-light-success-bg: var(--color-components-badge-status-light-success-bg); + --color-components-badge-status-light-success-border-inner: var(--color-components-badge-status-light-success-border-inner); + --color-components-badge-status-light-success-halo: var(--color-components-badge-status-light-success-halo); + + --color-components-badge-status-light-border-outer: var(--color-components-badge-status-light-border-outer); + --color-components-badge-status-light-high-light: var(--color-components-badge-status-light-high-light); + --color-components-badge-status-light-warning-bg: var(--color-components-badge-status-light-warning-bg); + --color-components-badge-status-light-warning-border-inner: var(--color-components-badge-status-light-warning-border-inner); + --color-components-badge-status-light-warning-halo: var(--color-components-badge-status-light-warning-halo); + + --color-components-badge-status-light-error-bg: var(--color-components-badge-status-light-error-bg); + --color-components-badge-status-light-error-border-inner: var(--color-components-badge-status-light-error-border-inner); + --color-components-badge-status-light-error-halo: var(--color-components-badge-status-light-error-halo); + + --color-components-badge-status-light-normal-bg: var(--color-components-badge-status-light-normal-bg); + --color-components-badge-status-light-normal-border-inner: var(--color-components-badge-status-light-normal-border-inner); + --color-components-badge-status-light-normal-halo: var(--color-components-badge-status-light-normal-halo); + + --color-components-badge-status-light-disabled-bg: var(--color-components-badge-status-light-disabled-bg); + --color-components-badge-status-light-disabled-border-inner: var(--color-components-badge-status-light-disabled-border-inner); + --color-components-badge-status-light-disabled-halo: var(--color-components-badge-status-light-disabled-halo); + + --color-components-badge-bg-green-soft: var(--color-components-badge-bg-green-soft); + --color-components-badge-bg-orange-soft: var(--color-components-badge-bg-orange-soft); + --color-components-badge-bg-red-soft: var(--color-components-badge-bg-red-soft); + --color-components-badge-bg-blue-light-soft: var(--color-components-badge-bg-blue-light-soft); + --color-components-badge-bg-gray-soft: var(--color-components-badge-bg-gray-soft); + --color-components-badge-bg-dimm: var(--color-components-badge-bg-dimm); + + --color-components-chart-line: var(--color-components-chart-line); + --color-components-chart-area-1: var(--color-components-chart-area-1); + --color-components-chart-area-2: var(--color-components-chart-area-2); + --color-components-chart-current-1: var(--color-components-chart-current-1); + --color-components-chart-current-2: var(--color-components-chart-current-2); + --color-components-chart-bg: var(--color-components-chart-bg); + + --color-components-actionbar-bg: var(--color-components-actionbar-bg); + --color-components-actionbar-border: var(--color-components-actionbar-border); + --color-components-actionbar-bg-accent: var(--color-components-actionbar-bg-accent); + --color-components-actionbar-border-accent: var(--color-components-actionbar-border-accent); + + --color-components-dropzone-bg-alt: var(--color-components-dropzone-bg-alt); + --color-components-dropzone-bg: var(--color-components-dropzone-bg); + --color-components-dropzone-bg-accent: var(--color-components-dropzone-bg-accent); + --color-components-dropzone-border: var(--color-components-dropzone-border); + --color-components-dropzone-border-alt: var(--color-components-dropzone-border-alt); + --color-components-dropzone-border-accent: var(--color-components-dropzone-border-accent); + + --color-components-progress-brand-progress: var(--color-components-progress-brand-progress); + --color-components-progress-brand-border: var(--color-components-progress-brand-border); + --color-components-progress-brand-bg: var(--color-components-progress-brand-bg); + + --color-components-progress-white-progress: var(--color-components-progress-white-progress); + --color-components-progress-white-border: var(--color-components-progress-white-border); + --color-components-progress-white-bg: var(--color-components-progress-white-bg); + + --color-components-progress-gray-progress: var(--color-components-progress-gray-progress); + --color-components-progress-gray-border: var(--color-components-progress-gray-border); + --color-components-progress-gray-bg: var(--color-components-progress-gray-bg); + + --color-components-progress-warning-progress: var(--color-components-progress-warning-progress); + --color-components-progress-warning-border: var(--color-components-progress-warning-border); + --color-components-progress-warning-bg: var(--color-components-progress-warning-bg); + + --color-components-progress-error-progress: var(--color-components-progress-error-progress); + --color-components-progress-error-border: var(--color-components-progress-error-border); + --color-components-progress-error-bg: var(--color-components-progress-error-bg); + + --color-components-chat-input-audio-bg: var(--color-components-chat-input-audio-bg); + --color-components-chat-input-audio-wave-default: var(--color-components-chat-input-audio-wave-default); + --color-components-chat-input-bg-mask-1: var(--color-components-chat-input-bg-mask-1); + --color-components-chat-input-bg-mask-2: var(--color-components-chat-input-bg-mask-2); + --color-components-chat-input-border: var(--color-components-chat-input-border); + --color-components-chat-input-audio-wave-active: var(--color-components-chat-input-audio-wave-active); + --color-components-chat-input-audio-bg-alt: var(--color-components-chat-input-audio-bg-alt); + + --color-components-avatar-shape-fill-stop-0: var(--color-components-avatar-shape-fill-stop-0); + --color-components-avatar-shape-fill-stop-100: var(--color-components-avatar-shape-fill-stop-100); + + --color-components-avatar-bg-mask-stop-0: var(--color-components-avatar-bg-mask-stop-0); + --color-components-avatar-bg-mask-stop-100: var(--color-components-avatar-bg-mask-stop-100); + + --color-components-avatar-default-avatar-bg: var(--color-components-avatar-default-avatar-bg); + --color-components-avatar-mask-darkmode-dimmed: var(--color-components-avatar-mask-darkmode-dimmed); + + --color-components-label-gray: var(--color-components-label-gray); + + --color-components-premium-badge-blue-bg-stop-0: var(--color-components-premium-badge-blue-bg-stop-0); + --color-components-premium-badge-blue-bg-stop-100: var(--color-components-premium-badge-blue-bg-stop-100); + --color-components-premium-badge-blue-stroke-stop-0: var(--color-components-premium-badge-blue-stroke-stop-0); + --color-components-premium-badge-blue-stroke-stop-100: var(--color-components-premium-badge-blue-stroke-stop-100); + --color-components-premium-badge-blue-text-stop-0: var(--color-components-premium-badge-blue-text-stop-0); + --color-components-premium-badge-blue-text-stop-100: var(--color-components-premium-badge-blue-text-stop-100); + --color-components-premium-badge-blue-glow: var(--color-components-premium-badge-blue-glow); + --color-components-premium-badge-blue-bg-stop-0-hover: var(--color-components-premium-badge-blue-bg-stop-0-hover); + --color-components-premium-badge-blue-bg-stop-100-hover: var(--color-components-premium-badge-blue-bg-stop-100-hover); + --color-components-premium-badge-blue-glow-hover: var(--color-components-premium-badge-blue-glow-hover); + --color-components-premium-badge-blue-stroke-stop-0-hover: var(--color-components-premium-badge-blue-stroke-stop-0-hover); + --color-components-premium-badge-blue-stroke-stop-100-hover: var(--color-components-premium-badge-blue-stroke-stop-100-hover); + + --color-components-premium-badge-highlight-stop-0: var(--color-components-premium-badge-highlight-stop-0); + --color-components-premium-badge-highlight-stop-100: var(--color-components-premium-badge-highlight-stop-100); + --color-components-premium-badge-indigo-bg-stop-0: var(--color-components-premium-badge-indigo-bg-stop-0); + --color-components-premium-badge-indigo-bg-stop-100: var(--color-components-premium-badge-indigo-bg-stop-100); + --color-components-premium-badge-indigo-stroke-stop-0: var(--color-components-premium-badge-indigo-stroke-stop-0); + --color-components-premium-badge-indigo-stroke-stop-100: var(--color-components-premium-badge-indigo-stroke-stop-100); + --color-components-premium-badge-indigo-text-stop-0: var(--color-components-premium-badge-indigo-text-stop-0); + --color-components-premium-badge-indigo-text-stop-100: var(--color-components-premium-badge-indigo-text-stop-100); + --color-components-premium-badge-indigo-glow: var(--color-components-premium-badge-indigo-glow); + --color-components-premium-badge-indigo-glow-hover: var(--color-components-premium-badge-indigo-glow-hover); + --color-components-premium-badge-indigo-bg-stop-0-hover: var(--color-components-premium-badge-indigo-bg-stop-0-hover); + --color-components-premium-badge-indigo-bg-stop-100-hover: var(--color-components-premium-badge-indigo-bg-stop-100-hover); + --color-components-premium-badge-indigo-stroke-stop-0-hover: var(--color-components-premium-badge-indigo-stroke-stop-0-hover); + --color-components-premium-badge-indigo-stroke-stop-100-hover: var(--color-components-premium-badge-indigo-stroke-stop-100-hover); + + --color-components-premium-badge-grey-bg-stop-0: var(--color-components-premium-badge-grey-bg-stop-0); + --color-components-premium-badge-grey-bg-stop-100: var(--color-components-premium-badge-grey-bg-stop-100); + --color-components-premium-badge-grey-stroke-stop-0: var(--color-components-premium-badge-grey-stroke-stop-0); + --color-components-premium-badge-grey-stroke-stop-100: var(--color-components-premium-badge-grey-stroke-stop-100); + --color-components-premium-badge-grey-text-stop-0: var(--color-components-premium-badge-grey-text-stop-0); + --color-components-premium-badge-grey-text-stop-100: var(--color-components-premium-badge-grey-text-stop-100); + --color-components-premium-badge-grey-glow: var(--color-components-premium-badge-grey-glow); + --color-components-premium-badge-grey-glow-hover: var(--color-components-premium-badge-grey-glow-hover); + --color-components-premium-badge-grey-bg-stop-0-hover: var(--color-components-premium-badge-grey-bg-stop-0-hover); + --color-components-premium-badge-grey-bg-stop-100-hover: var(--color-components-premium-badge-grey-bg-stop-100-hover); + --color-components-premium-badge-grey-stroke-stop-0-hover: var(--color-components-premium-badge-grey-stroke-stop-0-hover); + --color-components-premium-badge-grey-stroke-stop-100-hover: var(--color-components-premium-badge-grey-stroke-stop-100-hover); + + --color-components-premium-badge-orange-bg-stop-0: var(--color-components-premium-badge-orange-bg-stop-0); + --color-components-premium-badge-orange-bg-stop-100: var(--color-components-premium-badge-orange-bg-stop-100); + --color-components-premium-badge-orange-stroke-stop-0: var(--color-components-premium-badge-orange-stroke-stop-0); + --color-components-premium-badge-orange-stroke-stop-100: var(--color-components-premium-badge-orange-stroke-stop-100); + --color-components-premium-badge-orange-text-stop-0: var(--color-components-premium-badge-orange-text-stop-0); + --color-components-premium-badge-orange-text-stop-100: var(--color-components-premium-badge-orange-text-stop-100); + --color-components-premium-badge-orange-glow: var(--color-components-premium-badge-orange-glow); + --color-components-premium-badge-orange-glow-hover: var(--color-components-premium-badge-orange-glow-hover); + --color-components-premium-badge-orange-bg-stop-0-hover: var(--color-components-premium-badge-orange-bg-stop-0-hover); + --color-components-premium-badge-orange-bg-stop-100-hover: var(--color-components-premium-badge-orange-bg-stop-100-hover); + --color-components-premium-badge-orange-stroke-stop-0-hover: var(--color-components-premium-badge-orange-stroke-stop-0-hover); + --color-components-premium-badge-orange-stroke-stop-100-hover: var(--color-components-premium-badge-orange-stroke-stop-100-hover); + + --color-components-progress-bar-bg: var(--color-components-progress-bar-bg); + --color-components-progress-bar-progress: var(--color-components-progress-bar-progress); + --color-components-progress-bar-border: var(--color-components-progress-bar-border); + --color-components-progress-bar-progress-solid: var(--color-components-progress-bar-progress-solid); + --color-components-progress-bar-progress-highlight: var(--color-components-progress-bar-progress-highlight); + + --color-components-icon-bg-red-solid: var(--color-components-icon-bg-red-solid); + --color-components-icon-bg-rose-solid: var(--color-components-icon-bg-rose-solid); + --color-components-icon-bg-pink-solid: var(--color-components-icon-bg-pink-solid); + --color-components-icon-bg-orange-dark-solid: var(--color-components-icon-bg-orange-dark-solid); + --color-components-icon-bg-yellow-solid: var(--color-components-icon-bg-yellow-solid); + --color-components-icon-bg-green-solid: var(--color-components-icon-bg-green-solid); + --color-components-icon-bg-teal-solid: var(--color-components-icon-bg-teal-solid); + --color-components-icon-bg-blue-light-solid: var(--color-components-icon-bg-blue-light-solid); + --color-components-icon-bg-blue-solid: var(--color-components-icon-bg-blue-solid); + --color-components-icon-bg-indigo-solid: var(--color-components-icon-bg-indigo-solid); + --color-components-icon-bg-violet-solid: var(--color-components-icon-bg-violet-solid); + --color-components-icon-bg-midnight-solid: var(--color-components-icon-bg-midnight-solid); + --color-components-icon-bg-rose-soft: var(--color-components-icon-bg-rose-soft); + --color-components-icon-bg-pink-soft: var(--color-components-icon-bg-pink-soft); + --color-components-icon-bg-orange-dark-soft: var(--color-components-icon-bg-orange-dark-soft); + --color-components-icon-bg-yellow-soft: var(--color-components-icon-bg-yellow-soft); + --color-components-icon-bg-green-soft: var(--color-components-icon-bg-green-soft); + --color-components-icon-bg-teal-soft: var(--color-components-icon-bg-teal-soft); + --color-components-icon-bg-blue-light-soft: var(--color-components-icon-bg-blue-light-soft); + --color-components-icon-bg-blue-soft: var(--color-components-icon-bg-blue-soft); + --color-components-icon-bg-indigo-soft: var(--color-components-icon-bg-indigo-soft); + --color-components-icon-bg-violet-soft: var(--color-components-icon-bg-violet-soft); + --color-components-icon-bg-midnight-soft: var(--color-components-icon-bg-midnight-soft); + --color-components-icon-bg-red-soft: var(--color-components-icon-bg-red-soft); + --color-components-icon-bg-orange-solid: var(--color-components-icon-bg-orange-solid); + --color-components-icon-bg-orange-soft: var(--color-components-icon-bg-orange-soft); + + --color-text-primary: var(--color-text-primary); + --color-text-secondary: var(--color-text-secondary); + --color-text-tertiary: var(--color-text-tertiary); + --color-text-quaternary: var(--color-text-quaternary); + --color-text-destructive: var(--color-text-destructive); + --color-text-success: var(--color-text-success); + --color-text-warning: var(--color-text-warning); + --color-text-destructive-secondary: var(--color-text-destructive-secondary); + --color-text-success-secondary: var(--color-text-success-secondary); + --color-text-warning-secondary: var(--color-text-warning-secondary); + --color-text-accent: var(--color-text-accent); + --color-text-primary-on-surface: var(--color-text-primary-on-surface); + --color-text-placeholder: var(--color-text-placeholder); + --color-text-disabled: var(--color-text-disabled); + --color-text-accent-secondary: var(--color-text-accent-secondary); + --color-text-accent-light-mode-only: var(--color-text-accent-light-mode-only); + --color-text-text-selected: var(--color-text-text-selected); + --color-text-secondary-on-surface: var(--color-text-secondary-on-surface); + --color-text-logo-text: var(--color-text-logo-text); + --color-text-empty-state-icon: var(--color-text-empty-state-icon); + --color-text-inverted: var(--color-text-inverted); + --color-text-inverted-dimmed: var(--color-text-inverted-dimmed); + + --color-background-body: var(--color-background-body); + --color-background-default-subtle: var(--color-background-default-subtle); + --color-background-neutral-subtle: var(--color-background-neutral-subtle); + --color-background-sidenav-bg: var(--color-background-sidenav-bg); + --color-background-default: var(--color-background-default); + --color-background-soft: var(--color-background-soft); + --color-background-gradient-bg-fill-chat-bg-1: var(--color-background-gradient-bg-fill-chat-bg-1); + --color-background-gradient-bg-fill-chat-bg-2: var(--color-background-gradient-bg-fill-chat-bg-2); + --color-background-gradient-bg-fill-chat-bubble-bg-1: var(--color-background-gradient-bg-fill-chat-bubble-bg-1); + --color-background-gradient-bg-fill-chat-bubble-bg-2: var(--color-background-gradient-bg-fill-chat-bubble-bg-2); + --color-background-gradient-bg-fill-debug-bg-1: var(--color-background-gradient-bg-fill-debug-bg-1); + --color-background-gradient-bg-fill-debug-bg-2: var(--color-background-gradient-bg-fill-debug-bg-2); + + --color-background-gradient-mask-gray: var(--color-background-gradient-mask-gray); + --color-background-gradient-mask-transparent: var(--color-background-gradient-mask-transparent); + --color-background-gradient-mask-input-clear-2: var(--color-background-gradient-mask-input-clear-2); + --color-background-gradient-mask-input-clear-1: var(--color-background-gradient-mask-input-clear-1); + --color-background-gradient-mask-transparent-dark: var(--color-background-gradient-mask-transparent-dark); + --color-background-gradient-mask-side-panel-2: var(--color-background-gradient-mask-side-panel-2); + --color-background-gradient-mask-side-panel-1: var(--color-background-gradient-mask-side-panel-1); + + --color-background-default-burn: var(--color-background-default-burn); + --color-background-overlay-fullscreen: var(--color-background-overlay-fullscreen); + --color-background-default-lighter: var(--color-background-default-lighter); + --color-background-section: var(--color-background-section); + --color-background-interaction-from-bg-1: var(--color-background-interaction-from-bg-1); + --color-background-interaction-from-bg-2: var(--color-background-interaction-from-bg-2); + --color-background-section-burn: var(--color-background-section-burn); + --color-background-default-dodge: var(--color-background-default-dodge); + --color-background-overlay: var(--color-background-overlay); + --color-background-default-dimmed: var(--color-background-default-dimmed); + --color-background-default-hover: var(--color-background-default-hover); + --color-background-overlay-alt: var(--color-background-overlay-alt); + --color-background-surface-white: var(--color-background-surface-white); + --color-background-overlay-destructive: var(--color-background-overlay-destructive); + --color-background-overlay-backdrop: var(--color-background-overlay-backdrop); + --color-background-body-transparent: var(--color-background-body-transparent); + --color-background-section-burn-inverted: var(--color-background-section-burn-inverted); + + --color-shadow-shadow-1: var(--color-shadow-shadow-1); + --color-shadow-shadow-3: var(--color-shadow-shadow-3); + --color-shadow-shadow-4: var(--color-shadow-shadow-4); + --color-shadow-shadow-5: var(--color-shadow-shadow-5); + --color-shadow-shadow-6: var(--color-shadow-shadow-6); + --color-shadow-shadow-7: var(--color-shadow-shadow-7); + --color-shadow-shadow-8: var(--color-shadow-shadow-8); + --color-shadow-shadow-9: var(--color-shadow-shadow-9); + --color-shadow-shadow-2: var(--color-shadow-shadow-2); + --color-shadow-shadow-10: var(--color-shadow-shadow-10); + + --color-workflow-block-border: var(--color-workflow-block-border); + --color-workflow-block-parma-bg: var(--color-workflow-block-parma-bg); + --color-workflow-block-bg: var(--color-workflow-block-bg); + --color-workflow-block-bg-transparent: var(--color-workflow-block-bg-transparent); + --color-workflow-block-border-highlight: var(--color-workflow-block-border-highlight); + --color-workflow-block-wrapper-bg-1: var(--color-workflow-block-wrapper-bg-1); + --color-workflow-block-wrapper-bg-2: var(--color-workflow-block-wrapper-bg-2); + + --color-workflow-canvas-workflow-dot-color: var(--color-workflow-canvas-workflow-dot-color); + --color-workflow-canvas-workflow-bg: var(--color-workflow-canvas-workflow-bg); + --color-workflow-canvas-workflow-top-bar-1: var(--color-workflow-canvas-workflow-top-bar-1); + --color-workflow-canvas-workflow-top-bar-2: var(--color-workflow-canvas-workflow-top-bar-2); + --color-workflow-canvas-canvas-overlay: var(--color-workflow-canvas-canvas-overlay); + + --color-workflow-link-line-active: var(--color-workflow-link-line-active); + --color-workflow-link-line-normal: var(--color-workflow-link-line-normal); + --color-workflow-link-line-handle: var(--color-workflow-link-line-handle); + --color-workflow-link-line-normal-transparent: var(--color-workflow-link-line-normal-transparent); + --color-workflow-link-line-failure-active: var(--color-workflow-link-line-failure-active); + --color-workflow-link-line-failure-handle: var(--color-workflow-link-line-failure-handle); + --color-workflow-link-line-failure-button-bg: var(--color-workflow-link-line-failure-button-bg); + --color-workflow-link-line-failure-button-hover: var(--color-workflow-link-line-failure-button-hover); + + --color-workflow-link-line-success-active: var(--color-workflow-link-line-success-active); + --color-workflow-link-line-success-handle: var(--color-workflow-link-line-success-handle); + + --color-workflow-link-line-error-active: var(--color-workflow-link-line-error-active); + --color-workflow-link-line-error-handle: var(--color-workflow-link-line-error-handle); + + --color-workflow-minimap-bg: var(--color-workflow-minimap-bg); + --color-workflow-minimap-block: var(--color-workflow-minimap-block); + + --color-workflow-display-success-bg: var(--color-workflow-display-success-bg); + --color-workflow-display-success-border-1: var(--color-workflow-display-success-border-1); + --color-workflow-display-success-border-2: var(--color-workflow-display-success-border-2); + --color-workflow-display-success-vignette-color: var(--color-workflow-display-success-vignette-color); + --color-workflow-display-success-bg-line-pattern: var(--color-workflow-display-success-bg-line-pattern); + + --color-workflow-display-glass-1: var(--color-workflow-display-glass-1); + --color-workflow-display-glass-2: var(--color-workflow-display-glass-2); + --color-workflow-display-vignette-dark: var(--color-workflow-display-vignette-dark); + --color-workflow-display-highlight: var(--color-workflow-display-highlight); + --color-workflow-display-outline: var(--color-workflow-display-outline); + --color-workflow-display-error-bg: var(--color-workflow-display-error-bg); + --color-workflow-display-error-bg-line-pattern: var(--color-workflow-display-error-bg-line-pattern); + --color-workflow-display-error-border-1: var(--color-workflow-display-error-border-1); + --color-workflow-display-error-border-2: var(--color-workflow-display-error-border-2); + --color-workflow-display-error-vignette-color: var(--color-workflow-display-error-vignette-color); + + --color-workflow-display-warning-bg: var(--color-workflow-display-warning-bg); + --color-workflow-display-warning-bg-line-pattern: var(--color-workflow-display-warning-bg-line-pattern); + --color-workflow-display-warning-border-1: var(--color-workflow-display-warning-border-1); + --color-workflow-display-warning-border-2: var(--color-workflow-display-warning-border-2); + --color-workflow-display-warning-vignette-color: var(--color-workflow-display-warning-vignette-color); + + --color-workflow-display-normal-bg: var(--color-workflow-display-normal-bg); + --color-workflow-display-normal-bg-line-pattern: var(--color-workflow-display-normal-bg-line-pattern); + --color-workflow-display-normal-border-1: var(--color-workflow-display-normal-border-1); + --color-workflow-display-normal-border-2: var(--color-workflow-display-normal-border-2); + --color-workflow-display-normal-vignette-color: var(--color-workflow-display-normal-vignette-color); + + --color-workflow-display-disabled-bg: var(--color-workflow-display-disabled-bg); + --color-workflow-display-disabled-bg-line-pattern: var(--color-workflow-display-disabled-bg-line-pattern); + --color-workflow-display-disabled-border-1: var(--color-workflow-display-disabled-border-1); + --color-workflow-display-disabled-border-2: var(--color-workflow-display-disabled-border-2); + --color-workflow-display-disabled-vignette-color: var(--color-workflow-display-disabled-vignette-color); + --color-workflow-display-disabled-outline: var(--color-workflow-display-disabled-outline); + + --color-workflow-workflow-progress-bg-1: var(--color-workflow-workflow-progress-bg-1); + --color-workflow-workflow-progress-bg-2: var(--color-workflow-workflow-progress-bg-2); + + --color-divider-subtle: var(--color-divider-subtle); + --color-divider-regular: var(--color-divider-regular); + --color-divider-deep: var(--color-divider-deep); + --color-divider-burn: var(--color-divider-burn); + --color-divider-intense: var(--color-divider-intense); + --color-divider-solid: var(--color-divider-solid); + --color-divider-solid-alt: var(--color-divider-solid-alt); + --color-divider-accent: var(--color-divider-accent); + + --color-state-base-hover: var(--color-state-base-hover); + --color-state-base-active: var(--color-state-base-active); + --color-state-base-hover-alt: var(--color-state-base-hover-alt); + --color-state-base-handle: var(--color-state-base-handle); + --color-state-base-handle-hover: var(--color-state-base-handle-hover); + --color-state-base-hover-subtle: var(--color-state-base-hover-subtle); + + --color-state-accent-hover: var(--color-state-accent-hover); + --color-state-accent-active: var(--color-state-accent-active); + --color-state-accent-hover-alt: var(--color-state-accent-hover-alt); + --color-state-accent-solid: var(--color-state-accent-solid); + --color-state-accent-active-alt: var(--color-state-accent-active-alt); + + --color-state-destructive-hover: var(--color-state-destructive-hover); + --color-state-destructive-hover-alt: var(--color-state-destructive-hover-alt); + --color-state-destructive-active: var(--color-state-destructive-active); + --color-state-destructive-solid: var(--color-state-destructive-solid); + --color-state-destructive-border: var(--color-state-destructive-border); + --color-state-destructive-hover-transparent: var(--color-state-destructive-hover-transparent); + + --color-state-success-hover: var(--color-state-success-hover); + --color-state-success-hover-alt: var(--color-state-success-hover-alt); + --color-state-success-active: var(--color-state-success-active); + --color-state-success-solid: var(--color-state-success-solid); + + --color-state-warning-hover: var(--color-state-warning-hover); + --color-state-warning-hover-alt: var(--color-state-warning-hover-alt); + --color-state-warning-active: var(--color-state-warning-active); + --color-state-warning-solid: var(--color-state-warning-solid); + --color-state-warning-hover-transparent: var(--color-state-warning-hover-transparent); + + --color-effects-highlight: var(--color-effects-highlight); + --color-effects-highlight-lightmode-off: var(--color-effects-highlight-lightmode-off); + --color-effects-image-frame: var(--color-effects-image-frame); + --color-effects-icon-border: var(--color-effects-icon-border); + + --color-util-colors-orange-dark-orange-dark-50: var(--color-util-colors-orange-dark-orange-dark-50); + --color-util-colors-orange-dark-orange-dark-100: var(--color-util-colors-orange-dark-orange-dark-100); + --color-util-colors-orange-dark-orange-dark-200: var(--color-util-colors-orange-dark-orange-dark-200); + --color-util-colors-orange-dark-orange-dark-300: var(--color-util-colors-orange-dark-orange-dark-300); + --color-util-colors-orange-dark-orange-dark-400: var(--color-util-colors-orange-dark-orange-dark-400); + --color-util-colors-orange-dark-orange-dark-500: var(--color-util-colors-orange-dark-orange-dark-500); + --color-util-colors-orange-dark-orange-dark-600: var(--color-util-colors-orange-dark-orange-dark-600); + --color-util-colors-orange-dark-orange-dark-700: var(--color-util-colors-orange-dark-orange-dark-700); + + --color-util-colors-orange-orange-50: var(--color-util-colors-orange-orange-50); + --color-util-colors-orange-orange-100: var(--color-util-colors-orange-orange-100); + --color-util-colors-orange-orange-200: var(--color-util-colors-orange-orange-200); + --color-util-colors-orange-orange-300: var(--color-util-colors-orange-orange-300); + --color-util-colors-orange-orange-400: var(--color-util-colors-orange-orange-400); + --color-util-colors-orange-orange-500: var(--color-util-colors-orange-orange-500); + --color-util-colors-orange-orange-600: var(--color-util-colors-orange-orange-600); + --color-util-colors-orange-orange-700: var(--color-util-colors-orange-orange-700); + --color-util-colors-orange-orange-100-transparent: var(--color-util-colors-orange-orange-100-transparent); + + --color-util-colors-pink-pink-50: var(--color-util-colors-pink-pink-50); + --color-util-colors-pink-pink-100: var(--color-util-colors-pink-pink-100); + --color-util-colors-pink-pink-200: var(--color-util-colors-pink-pink-200); + --color-util-colors-pink-pink-300: var(--color-util-colors-pink-pink-300); + --color-util-colors-pink-pink-400: var(--color-util-colors-pink-pink-400); + --color-util-colors-pink-pink-500: var(--color-util-colors-pink-pink-500); + --color-util-colors-pink-pink-600: var(--color-util-colors-pink-pink-600); + --color-util-colors-pink-pink-700: var(--color-util-colors-pink-pink-700); + + --color-util-colors-fuchsia-fuchsia-50: var(--color-util-colors-fuchsia-fuchsia-50); + --color-util-colors-fuchsia-fuchsia-100: var(--color-util-colors-fuchsia-fuchsia-100); + --color-util-colors-fuchsia-fuchsia-200: var(--color-util-colors-fuchsia-fuchsia-200); + --color-util-colors-fuchsia-fuchsia-300: var(--color-util-colors-fuchsia-fuchsia-300); + --color-util-colors-fuchsia-fuchsia-400: var(--color-util-colors-fuchsia-fuchsia-400); + --color-util-colors-fuchsia-fuchsia-500: var(--color-util-colors-fuchsia-fuchsia-500); + --color-util-colors-fuchsia-fuchsia-600: var(--color-util-colors-fuchsia-fuchsia-600); + --color-util-colors-fuchsia-fuchsia-700: var(--color-util-colors-fuchsia-fuchsia-700); + + --color-util-colors-purple-purple-50: var(--color-util-colors-purple-purple-50); + --color-util-colors-purple-purple-100: var(--color-util-colors-purple-purple-100); + --color-util-colors-purple-purple-200: var(--color-util-colors-purple-purple-200); + --color-util-colors-purple-purple-300: var(--color-util-colors-purple-purple-300); + --color-util-colors-purple-purple-400: var(--color-util-colors-purple-purple-400); + --color-util-colors-purple-purple-500: var(--color-util-colors-purple-purple-500); + --color-util-colors-purple-purple-600: var(--color-util-colors-purple-purple-600); + --color-util-colors-purple-purple-700: var(--color-util-colors-purple-purple-700); + + --color-util-colors-indigo-indigo-50: var(--color-util-colors-indigo-indigo-50); + --color-util-colors-indigo-indigo-100: var(--color-util-colors-indigo-indigo-100); + --color-util-colors-indigo-indigo-200: var(--color-util-colors-indigo-indigo-200); + --color-util-colors-indigo-indigo-300: var(--color-util-colors-indigo-indigo-300); + --color-util-colors-indigo-indigo-400: var(--color-util-colors-indigo-indigo-400); + --color-util-colors-indigo-indigo-500: var(--color-util-colors-indigo-indigo-500); + --color-util-colors-indigo-indigo-600: var(--color-util-colors-indigo-indigo-600); + --color-util-colors-indigo-indigo-700: var(--color-util-colors-indigo-indigo-700); + + --color-util-colors-blue-blue-50: var(--color-util-colors-blue-blue-50); + --color-util-colors-blue-blue-100: var(--color-util-colors-blue-blue-100); + --color-util-colors-blue-blue-200: var(--color-util-colors-blue-blue-200); + --color-util-colors-blue-blue-300: var(--color-util-colors-blue-blue-300); + --color-util-colors-blue-blue-400: var(--color-util-colors-blue-blue-400); + --color-util-colors-blue-blue-500: var(--color-util-colors-blue-blue-500); + --color-util-colors-blue-blue-600: var(--color-util-colors-blue-blue-600); + --color-util-colors-blue-blue-700: var(--color-util-colors-blue-blue-700); + + --color-util-colors-blue-light-blue-light-50: var(--color-util-colors-blue-light-blue-light-50); + --color-util-colors-blue-light-blue-light-100: var(--color-util-colors-blue-light-blue-light-100); + --color-util-colors-blue-light-blue-light-200: var(--color-util-colors-blue-light-blue-light-200); + --color-util-colors-blue-light-blue-light-300: var(--color-util-colors-blue-light-blue-light-300); + --color-util-colors-blue-light-blue-light-400: var(--color-util-colors-blue-light-blue-light-400); + --color-util-colors-blue-light-blue-light-500: var(--color-util-colors-blue-light-blue-light-500); + --color-util-colors-blue-light-blue-light-600: var(--color-util-colors-blue-light-blue-light-600); + --color-util-colors-blue-light-blue-light-700: var(--color-util-colors-blue-light-blue-light-700); + + --color-util-colors-gray-blue-gray-blue-50: var(--color-util-colors-gray-blue-gray-blue-50); + --color-util-colors-gray-blue-gray-blue-100: var(--color-util-colors-gray-blue-gray-blue-100); + --color-util-colors-gray-blue-gray-blue-200: var(--color-util-colors-gray-blue-gray-blue-200); + --color-util-colors-gray-blue-gray-blue-300: var(--color-util-colors-gray-blue-gray-blue-300); + --color-util-colors-gray-blue-gray-blue-400: var(--color-util-colors-gray-blue-gray-blue-400); + --color-util-colors-gray-blue-gray-blue-500: var(--color-util-colors-gray-blue-gray-blue-500); + --color-util-colors-gray-blue-gray-blue-600: var(--color-util-colors-gray-blue-gray-blue-600); + --color-util-colors-gray-blue-gray-blue-700: var(--color-util-colors-gray-blue-gray-blue-700); + + --color-util-colors-blue-brand-blue-brand-50: var(--color-util-colors-blue-brand-blue-brand-50); + --color-util-colors-blue-brand-blue-brand-100: var(--color-util-colors-blue-brand-blue-brand-100); + --color-util-colors-blue-brand-blue-brand-200: var(--color-util-colors-blue-brand-blue-brand-200); + --color-util-colors-blue-brand-blue-brand-300: var(--color-util-colors-blue-brand-blue-brand-300); + --color-util-colors-blue-brand-blue-brand-400: var(--color-util-colors-blue-brand-blue-brand-400); + --color-util-colors-blue-brand-blue-brand-500: var(--color-util-colors-blue-brand-blue-brand-500); + --color-util-colors-blue-brand-blue-brand-600: var(--color-util-colors-blue-brand-blue-brand-600); + --color-util-colors-blue-brand-blue-brand-700: var(--color-util-colors-blue-brand-blue-brand-700); + + --color-util-colors-red-red-50: var(--color-util-colors-red-red-50); + --color-util-colors-red-red-100: var(--color-util-colors-red-red-100); + --color-util-colors-red-red-200: var(--color-util-colors-red-red-200); + --color-util-colors-red-red-300: var(--color-util-colors-red-red-300); + --color-util-colors-red-red-400: var(--color-util-colors-red-red-400); + --color-util-colors-red-red-500: var(--color-util-colors-red-red-500); + --color-util-colors-red-red-600: var(--color-util-colors-red-red-600); + --color-util-colors-red-red-700: var(--color-util-colors-red-red-700); + + --color-util-colors-green-green-50: var(--color-util-colors-green-green-50); + --color-util-colors-green-green-100: var(--color-util-colors-green-green-100); + --color-util-colors-green-green-200: var(--color-util-colors-green-green-200); + --color-util-colors-green-green-300: var(--color-util-colors-green-green-300); + --color-util-colors-green-green-400: var(--color-util-colors-green-green-400); + --color-util-colors-green-green-500: var(--color-util-colors-green-green-500); + --color-util-colors-green-green-600: var(--color-util-colors-green-green-600); + --color-util-colors-green-green-700: var(--color-util-colors-green-green-700); + + --color-util-colors-warning-warning-50: var(--color-util-colors-warning-warning-50); + --color-util-colors-warning-warning-100: var(--color-util-colors-warning-warning-100); + --color-util-colors-warning-warning-200: var(--color-util-colors-warning-warning-200); + --color-util-colors-warning-warning-300: var(--color-util-colors-warning-warning-300); + --color-util-colors-warning-warning-400: var(--color-util-colors-warning-warning-400); + --color-util-colors-warning-warning-500: var(--color-util-colors-warning-warning-500); + --color-util-colors-warning-warning-600: var(--color-util-colors-warning-warning-600); + --color-util-colors-warning-warning-700: var(--color-util-colors-warning-warning-700); + + --color-util-colors-yellow-yellow-50: var(--color-util-colors-yellow-yellow-50); + --color-util-colors-yellow-yellow-100: var(--color-util-colors-yellow-yellow-100); + --color-util-colors-yellow-yellow-200: var(--color-util-colors-yellow-yellow-200); + --color-util-colors-yellow-yellow-300: var(--color-util-colors-yellow-yellow-300); + --color-util-colors-yellow-yellow-400: var(--color-util-colors-yellow-yellow-400); + --color-util-colors-yellow-yellow-500: var(--color-util-colors-yellow-yellow-500); + --color-util-colors-yellow-yellow-600: var(--color-util-colors-yellow-yellow-600); + --color-util-colors-yellow-yellow-700: var(--color-util-colors-yellow-yellow-700); + + --color-util-colors-teal-teal-50: var(--color-util-colors-teal-teal-50); + --color-util-colors-teal-teal-100: var(--color-util-colors-teal-teal-100); + --color-util-colors-teal-teal-200: var(--color-util-colors-teal-teal-200); + --color-util-colors-teal-teal-300: var(--color-util-colors-teal-teal-300); + --color-util-colors-teal-teal-400: var(--color-util-colors-teal-teal-400); + --color-util-colors-teal-teal-500: var(--color-util-colors-teal-teal-500); + --color-util-colors-teal-teal-600: var(--color-util-colors-teal-teal-600); + --color-util-colors-teal-teal-700: var(--color-util-colors-teal-teal-700); + + --color-util-colors-cyan-cyan-50: var(--color-util-colors-cyan-cyan-50); + --color-util-colors-cyan-cyan-100: var(--color-util-colors-cyan-cyan-100); + --color-util-colors-cyan-cyan-200: var(--color-util-colors-cyan-cyan-200); + --color-util-colors-cyan-cyan-300: var(--color-util-colors-cyan-cyan-300); + --color-util-colors-cyan-cyan-400: var(--color-util-colors-cyan-cyan-400); + --color-util-colors-cyan-cyan-500: var(--color-util-colors-cyan-cyan-500); + --color-util-colors-cyan-cyan-600: var(--color-util-colors-cyan-cyan-600); + --color-util-colors-cyan-cyan-700: var(--color-util-colors-cyan-cyan-700); + + --color-util-colors-violet-violet-50: var(--color-util-colors-violet-violet-50); + --color-util-colors-violet-violet-100: var(--color-util-colors-violet-violet-100); + --color-util-colors-violet-violet-200: var(--color-util-colors-violet-violet-200); + --color-util-colors-violet-violet-300: var(--color-util-colors-violet-violet-300); + --color-util-colors-violet-violet-400: var(--color-util-colors-violet-violet-400); + --color-util-colors-violet-violet-500: var(--color-util-colors-violet-violet-500); + --color-util-colors-violet-violet-600: var(--color-util-colors-violet-violet-600); + --color-util-colors-violet-violet-700: var(--color-util-colors-violet-violet-700); + + --color-util-colors-gray-gray-50: var(--color-util-colors-gray-gray-50); + --color-util-colors-gray-gray-100: var(--color-util-colors-gray-gray-100); + --color-util-colors-gray-gray-200: var(--color-util-colors-gray-gray-200); + --color-util-colors-gray-gray-300: var(--color-util-colors-gray-gray-300); + --color-util-colors-gray-gray-400: var(--color-util-colors-gray-gray-400); + --color-util-colors-gray-gray-500: var(--color-util-colors-gray-gray-500); + --color-util-colors-gray-gray-600: var(--color-util-colors-gray-gray-600); + --color-util-colors-gray-gray-700: var(--color-util-colors-gray-gray-700); + + --color-util-colors-green-light-green-light-50: var(--color-util-colors-green-light-green-light-50); + --color-util-colors-green-light-green-light-100: var(--color-util-colors-green-light-green-light-100); + --color-util-colors-green-light-green-light-200: var(--color-util-colors-green-light-green-light-200); + --color-util-colors-green-light-green-light-300: var(--color-util-colors-green-light-green-light-300); + --color-util-colors-green-light-green-light-500: var(--color-util-colors-green-light-green-light-500); + --color-util-colors-green-light-green-light-400: var(--color-util-colors-green-light-green-light-400); + --color-util-colors-green-light-green-light-600: var(--color-util-colors-green-light-green-light-600); + --color-util-colors-green-light-green-light-700: var(--color-util-colors-green-light-green-light-700); + + --color-util-colors-rose-rose-50: var(--color-util-colors-rose-rose-50); + --color-util-colors-rose-rose-100: var(--color-util-colors-rose-rose-100); + --color-util-colors-rose-rose-200: var(--color-util-colors-rose-rose-200); + --color-util-colors-rose-rose-300: var(--color-util-colors-rose-rose-300); + --color-util-colors-rose-rose-400: var(--color-util-colors-rose-rose-400); + --color-util-colors-rose-rose-500: var(--color-util-colors-rose-rose-500); + --color-util-colors-rose-rose-600: var(--color-util-colors-rose-rose-600); + --color-util-colors-rose-rose-700: var(--color-util-colors-rose-rose-700); + + --color-util-colors-midnight-midnight-50: var(--color-util-colors-midnight-midnight-50); + --color-util-colors-midnight-midnight-100: var(--color-util-colors-midnight-midnight-100); + --color-util-colors-midnight-midnight-200: var(--color-util-colors-midnight-midnight-200); + --color-util-colors-midnight-midnight-300: var(--color-util-colors-midnight-midnight-300); + --color-util-colors-midnight-midnight-400: var(--color-util-colors-midnight-midnight-400); + --color-util-colors-midnight-midnight-500: var(--color-util-colors-midnight-midnight-500); + --color-util-colors-midnight-midnight-600: var(--color-util-colors-midnight-midnight-600); + --color-util-colors-midnight-midnight-700: var(--color-util-colors-midnight-midnight-700); + + --color-third-party-LangChain: var(--color-third-party-LangChain); + --color-third-party-Langfuse: var(--color-third-party-Langfuse); + --color-third-party-Github: var(--color-third-party-Github); + --color-third-party-Github-tertiary: var(--color-third-party-Github-tertiary); + --color-third-party-Github-secondary: var(--color-third-party-Github-secondary); + --color-third-party-model-bg-openai: var(--color-third-party-model-bg-openai); + --color-third-party-model-bg-anthropic: var(--color-third-party-model-bg-anthropic); + --color-third-party-model-bg-default: var(--color-third-party-model-bg-default); + + --color-third-party-aws: var(--color-third-party-aws); + --color-third-party-aws-alt: var(--color-third-party-aws-alt); + + --color-saas-background: var(--color-saas-background); + --color-saas-pricing-grid-bg: var(--color-saas-pricing-grid-bg); + --color-saas-dify-blue-static: var(--color-saas-dify-blue-static); + --color-saas-dify-blue-static-hover: var(--color-saas-dify-blue-static-hover); + --color-saas-dify-blue-accessible: var(--color-saas-dify-blue-accessible); + --color-saas-dify-blue-inverted: var(--color-saas-dify-blue-inverted); + --color-saas-dify-blue-inverted-dimmed: var(--color-saas-dify-blue-inverted-dimmed); + + --color-saas-background-inverted: var(--color-saas-background-inverted); + --color-saas-background-inverted-hover: var(--color-saas-background-inverted-hover); + + --color-dify-logo-blue: var(--color-dify-logo-blue); + --color-dify-logo-black: var(--color-dify-logo-black); + +} diff --git a/packages/dify-ui/tailwind.config.ts b/packages/dify-ui/tailwind.config.ts deleted file mode 100644 index bcf1731775..0000000000 --- a/packages/dify-ui/tailwind.config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Config } from 'tailwindcss' -import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons' -import difyUIPreset from './src/tailwind-preset' - -const config: Config = { - content: [ - './src/**/*.{js,ts,jsx,tsx,mdx}', - './.storybook/**/*.{js,ts,jsx,tsx,mdx}', - ], - presets: [difyUIPreset], - plugins: [ - iconsPlugin({ - collections: getIconCollections(['ri']), - extraProperties: { - width: '1rem', - height: '1rem', - display: 'block', - }, - }), - ], -} - -export default config diff --git a/packages/dify-ui/tsconfig.json b/packages/dify-ui/tsconfig.json index 514954c807..ae1a2b78dd 100644 --- a/packages/dify-ui/tsconfig.json +++ b/packages/dify-ui/tsconfig.json @@ -3,6 +3,6 @@ "compilerOptions": { "types": ["vite-plus/test/globals"] }, - "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts", "tailwind.config.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"], "exclude": ["node_modules", "dist", "storybook-static", "coverage"] } diff --git a/web/.storybook/preview.tsx b/web/.storybook/preview.tsx index 92b5baab0d..0209fc535c 100644 --- a/web/.storybook/preview.tsx +++ b/web/.storybook/preview.tsx @@ -6,7 +6,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { I18nClientProvider as I18N } from '../app/components/provider/i18n' import commonEnUS from '../i18n/en-US/common.json' -import '../app/styles/globals.css' import '../app/styles/markdown.css' import './storybook.css' diff --git a/web/.storybook/storybook.css b/web/.storybook/storybook.css index 85df1087a0..159deff4fe 100644 --- a/web/.storybook/storybook.css +++ b/web/.storybook/storybook.css @@ -1,3 +1,16 @@ +@import '../app/styles/tailwind-core.css'; + +@source '../app'; +@source '../context'; +@source '../hooks'; +@source '.'; +@source '../../packages/dify-ui/src'; +@source '../node_modules/streamdown/dist'; +@source '../node_modules/@streamdown/math/dist'; +@source not '../**/*.{spec,test}.{js,ts,jsx,tsx}'; +@source not '../../packages/dify-ui/src/**/*.{spec,test}.{ts,tsx}'; +@source not '../../packages/dify-ui/src/**/*.stories.{ts,tsx}'; + html, body { max-width: unset; diff --git a/web/__tests__/real-browser-flicker.test.tsx b/web/__tests__/real-browser-flicker.test.tsx index 7c3b45af19..288a5198a8 100644 --- a/web/__tests__/real-browser-flicker.test.tsx +++ b/web/__tests__/real-browser-flicker.test.tsx @@ -255,7 +255,6 @@ const TestThemeProvider = ({ children }: { children: React.ReactNode }) => ( defaultTheme="system" enableSystem disableTransitionOnChange - enableColorScheme={false} > {children} diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx index bcfcb5cbe6..fc2b3899e5 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/Form.tsx @@ -15,9 +15,9 @@ import type { import { cn } from '@langgenius/dify-ui/cn' import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' import { useCallback, useState } from 'react' +import { Infotip } from '@/app/components/base/infotip' import Radio from '@/app/components/base/radio' import RadioE from '@/app/components/base/radio/ui' -import Tooltip from '@/app/components/base/tooltip' import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector' import MultipleToolSelector from '@/app/components/plugins/plugin-detail-panel/multiple-tool-selector' @@ -28,6 +28,23 @@ import { FormTypeEnum } from '../declarations' import { useLanguage } from '../hooks' import Input from './Input' +const radioGridColumnsClassNames: Record = { + 1: 'grid-cols-1', + 2: 'grid-cols-2', + 3: 'grid-cols-3', + 4: 'grid-cols-4', + 5: 'grid-cols-5', + 6: 'grid-cols-6', + 7: 'grid-cols-7', + 8: 'grid-cols-8', + 9: 'grid-cols-9', + 10: 'grid-cols-10', + 11: 'grid-cols-11', + 12: 'grid-cols-12', +} + +type ModelSelectorValue = Record + type FormProps< CustomFormSchema extends Omit & { type: string } = never, > = { @@ -100,7 +117,7 @@ function Form< fieldMoreInfo, } - const handleFormChange = (key: string, val: string | boolean) => { + const handleFormChange = (key: string, val: FormValue[string]) => { if (isEditMode && (key === '__model_type' || key === '__model_name')) return @@ -115,7 +132,7 @@ function Form< onChange({ ...value, [key]: val, ...shouldClearVariable }) } - const handleModelChanged = useCallback((key: string, model: any) => { + const handleModelChanged = useCallback((key: string, model: ModelSelectorValue) => { const newValue = { ...value[key], ...model, @@ -125,17 +142,16 @@ function Form< }, [onChange, value]) const renderField = (formSchema: CredentialFormSchema | CustomFormSchema) => { - const tooltip = formSchema.tooltip - const tooltipContent = (tooltip && ( - - {tooltip[language] || tooltip.en_US} -
- )} - triggerClassName="ml-1 w-4 h-4" - asChild={false} - /> + const infotip = formSchema.tooltip + const infotipText = infotip?.[language] || infotip?.en_US + const infotipContent = (infotipText && ( + + {infotipText} + )) if (override) { const [overrideTypes, overrideRender] = override @@ -166,7 +182,7 @@ function Form< {required && ( * )} - {tooltipContent} + {infotipContent}
@@ -209,10 +226,9 @@ function Form< {required && ( * )} - {tooltipContent} + {infotipContent}
- {/* eslint-disable-next-line tailwindcss/no-unknown-classes */} -
+
{options.filter((option) => { if (option.show_on.length) return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value) @@ -272,7 +288,7 @@ function Form< {required && ( * )} - {tooltipContent} + {infotipContent}
+ {/* name & icon */} +
+
+
{t(`${prefixSettings}.webName`, { ns: 'appOverview' })}
+ +
+ { setShowAppIconPicker(true) }} + className="mt-2 cursor-pointer" + iconType={appIcon.type} + icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon} + background={appIcon.type === 'image' ? undefined : appIcon.background} + imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} />
- { setShowAppIconPicker(true) }} - className="mt-2 cursor-pointer" - iconType={appIcon.type} - icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon} - background={appIcon.type === 'image' ? undefined : appIcon.background} - imageUrl={appIcon.type === 'image' ? appIcon.url : undefined} - /> -
- {/* description */} -
-
{t(`${prefixSettings}.webDesc`, { ns: 'appOverview' })}
- + +
+
- {latestParams.length > 0 && ( -
-
-
{t('mcp.server.modal.parameters', { ns: 'tools' })}
- -
-
{t('mcp.server.modal.parametersTip', { ns: 'tools' })}
-
- {latestParams.map(paramItem => ( - handleParamChange(paramItem.variable, value)} - /> - ))} +
+ {!data ? t('mcp.server.modal.addTitle', { ns: 'tools' }) : t('mcp.server.modal.editTitle', { ns: 'tools' })} +
+
+
+
+
{t('mcp.server.modal.description', { ns: 'tools' })}
+
*
+
- )} -
-
- - -
- + {latestParams.length > 0 && ( +
+
+
{t('mcp.server.modal.parameters', { ns: 'tools' })}
+ +
+
{t('mcp.server.modal.parametersTip', { ns: 'tools' })}
+
+ {latestParams.map((paramItem) => { + if (!paramItem.variable) + return null + + const { variable } = paramItem + + return ( + handleParamChange(variable, value)} + /> + ) + })} +
+
+ )} +
+
+ + +
+ + ) } diff --git a/web/app/components/tools/mcp/modal.tsx b/web/app/components/tools/mcp/modal.tsx index 165535127d..79179ae3dc 100644 --- a/web/app/components/tools/mcp/modal.tsx +++ b/web/app/components/tools/mcp/modal.tsx @@ -4,17 +4,15 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker' import type { ToolWithProvider } from '@/app/components/workflow/types' import type { AppIconType } from '@/types/app' import { Button } from '@langgenius/dify-ui/button' -import { cn } from '@langgenius/dify-ui/cn' +import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import { RiCloseLine, RiEditLine } from '@remixicon/react' import { useHover } from 'ahooks' -import { noop } from 'es-toolkit/function' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import AppIconPicker from '@/app/components/base/app-icon-picker' import { Mcp } from '@/app/components/base/icons/src/vender/other' import Input from '@/app/components/base/input' -import Modal from '@/app/components/base/modal' import TabSlider from '@/app/components/base/tab-slider' import { MCPAuthMethod } from '@/app/components/tools/types' import { shouldUseMcpIconForAppIcon } from '@/utils/mcp' @@ -281,18 +279,16 @@ const MCPModal: FC = ({ const formKey = data?.id ?? 'create' return ( - - - + + + + + ) } diff --git a/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx b/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx index 241cd7d762..684c700648 100644 --- a/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx +++ b/web/app/components/workflow/__tests__/update-dsl-modal.spec.tsx @@ -126,6 +126,15 @@ describe('UpdateDSLModal', () => { expect(defaultProps.onBackup).toHaveBeenCalledTimes(1) }) + it('should call cancel handler when the import dialog requests close', () => { + const onCancel = vi.fn() + renderModal({ ...defaultProps, onCancel }) + + fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' }) + + expect(onCancel).toHaveBeenCalledTimes(1) + }) + it('should import a valid file and emit workflow update payload', async () => { renderModal() @@ -228,6 +237,32 @@ describe('UpdateDSLModal', () => { }) }) + it('should close the pending modal when dialog requests close', async () => { + mockImportDSL.mockResolvedValue({ + id: 'import-8', + status: DSLImportStatus.PENDING, + imported_dsl_version: '1.0.0', + current_dsl_version: '2.0.0', + }) + + renderModal() + + fireEvent.change(screen.getByTestId('dsl-file-input'), { + target: { files: [new File(['workflow'], 'workflow.yml', { type: 'text/yaml' })] }, + }) + fireEvent.click(screen.getByRole('button', { name: 'workflow.common.overwriteAndImport' })) + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'app.newApp.Confirm' })).toBeInTheDocument() + }) + + fireEvent.keyDown(document, { key: 'Escape', code: 'Escape' }) + + await waitFor(() => { + expect(screen.queryByRole('button', { name: 'app.newApp.Confirm' })).not.toBeInTheDocument() + }) + }) + it('should show an error when the selected file content is invalid for the current app mode', async () => { class InvalidDSLFileReader extends MockFileReader { override readAsText(_file: Blob) { diff --git a/web/app/components/workflow/header/online-users.tsx b/web/app/components/workflow/header/online-users.tsx index 17e1de3feb..93e9d1fa85 100644 --- a/web/app/components/workflow/header/online-users.tsx +++ b/web/app/components/workflow/header/online-users.tsx @@ -189,7 +189,6 @@ const OnlineUsers = () => { placement="bottom-start" sideOffset={8} alignOffset={-48} - className="z-[9999]" popupClassName={cn( 'mt-1.5 flex max-h-[200px] w-[240px] flex-col overflow-y-auto', 'rounded-xl border-[0.5px] border-components-panel-border', diff --git a/web/app/components/workflow/nodes/http/components/authorization/index.tsx b/web/app/components/workflow/nodes/http/components/authorization/index.tsx index b72e52911d..684f943d5f 100644 --- a/web/app/components/workflow/nodes/http/components/authorization/index.tsx +++ b/web/app/components/workflow/nodes/http/components/authorization/index.tsx @@ -4,12 +4,12 @@ import type { Authorization as AuthorizationPayloadType } from '../../types' import type { Var } from '@/app/components/workflow/types' import { Button } from '@langgenius/dify-ui/button' import { cn } from '@langgenius/dify-ui/cn' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { produce } from 'immer' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import BaseInput from '@/app/components/base/input' -import Modal from '@/app/components/base/modal' import Input from '@/app/components/workflow/nodes/_base/components/input-support-select-var' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import { VarType } from '@/app/components/workflow/types' @@ -115,70 +115,78 @@ const Authorization: FC = ({ onHide() }, [tempPayload, onChange, onHide]) return ( - { + if (!open) + onHide() + }} > -
-
- - - + + + {t(`${i18nPrefix}.authorization`, { ns: 'workflow' })} + - {tempPayload.type === AuthorizationType.apiKey && ( - <> - - - - {tempPayload.config?.type === APIType.custom && ( - - +
+ + + + + {tempPayload.type === AuthorizationType.apiKey && ( + <> + + - )} + {tempPayload.config?.type === APIType.custom && ( + + + + )} - -
- -
-
- - )} + +
+ +
+
+ + )} +
+
+ + +
-
- - -
-
-
+ + ) } export default React.memo(Authorization) diff --git a/web/app/components/workflow/nodes/http/components/curl-panel.tsx b/web/app/components/workflow/nodes/http/components/curl-panel.tsx index 8ba5fb36a9..87dc2a3427 100644 --- a/web/app/components/workflow/nodes/http/components/curl-panel.tsx +++ b/web/app/components/workflow/nodes/http/components/curl-panel.tsx @@ -2,11 +2,11 @@ import type { FC } from 'react' import type { HttpNodeType } from '../types' import { Button } from '@langgenius/dify-ui/button' +import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog' import { toast } from '@langgenius/dify-ui/toast' import * as React from 'react' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import Modal from '@/app/components/base/modal' import Textarea from '@/app/components/base/textarea' import { useNodesInteractions } from '@/app/components/workflow/hooks' import { parseCurl } from './curl-parser' @@ -42,28 +42,35 @@ const CurlPanel: FC = ({ nodeId, isShow, onHide, handleCurlImport }) => { }, [onHide, nodeId, inputString, handleNodeSelect, handleCurlImport]) return ( - { + if (!open) + onHide() + }} > -
-
- + + {latestParams.length > 0 && ( +
+
+
{t('mcp.server.modal.parameters', { ns: 'tools' })}
+ +
+
{t('mcp.server.modal.parametersTip', { ns: 'tools' })}
+
+ {latestParams.map((paramItem) => { + if (!paramItem.variable) + return null + + const { variable } = paramItem + + return ( + handleParamChange(variable, value)} + /> + ) + })} +
+
+ )}
- {latestParams.length > 0 && ( -
-
-
{t('mcp.server.modal.parameters', { ns: 'tools' })}
- -
-
{t('mcp.server.modal.parametersTip', { ns: 'tools' })}
-
- {latestParams.map((paramItem) => { - if (!paramItem.variable) - return null - - const { variable } = paramItem - - return ( - handleParamChange(variable, value)} - /> - ) - })} -
-
- )}
-
- +
+
diff --git a/web/app/components/tools/mcp/mcp-server-param-item.tsx b/web/app/components/tools/mcp/mcp-server-param-item.tsx index db27cfdf98..316bbca556 100644 --- a/web/app/components/tools/mcp/mcp-server-param-item.tsx +++ b/web/app/components/tools/mcp/mcp-server-param-item.tsx @@ -17,12 +17,12 @@ const MCPServerParamItem = ({ const { t } = useTranslation() return ( -
-
-
{data.label}
+
+
+
{data.label}
·
-
{data.variable}
-
{data.type}
+
{data.variable}
+
{data.type}