From f7afa103a522a3ca78785f36367eb9a319020d65 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Fri, 13 Mar 2026 13:43:29 +0800 Subject: [PATCH 001/120] feat: select snippets --- .../workflow/block-selector/hooks.ts | 5 + .../workflow/block-selector/main.tsx | 82 ++++-- .../workflow/block-selector/snippets.tsx | 247 ++++++++++++++++++ .../workflow/block-selector/tabs.tsx | 9 + .../workflow/block-selector/types.ts | 1 + web/i18n/en-US/workflow.json | 4 + web/i18n/zh-Hans/workflow.json | 4 + 7 files changed, 329 insertions(+), 23 deletions(-) create mode 100644 web/app/components/workflow/block-selector/snippets.tsx diff --git a/web/app/components/workflow/block-selector/hooks.ts b/web/app/components/workflow/block-selector/hooks.ts index ad21df1cb8..3a05bc09cc 100644 --- a/web/app/components/workflow/block-selector/hooks.ts +++ b/web/app/components/workflow/block-selector/hooks.ts @@ -71,6 +71,10 @@ export const useTabs = ({ name: t('tabs.start', { ns: 'workflow' }), show: shouldShowStartTab, disabled: shouldDisableStartTab, + }, { + key: TabsEnum.Snippets, + name: t('tabs.snippets', { ns: 'workflow' }), + show: true, }] return tabConfigs.filter(tab => tab.show) @@ -100,6 +104,7 @@ export const useTabs = ({ preferredOrder.push(TabsEnum.Sources) if (!noStart) preferredOrder.push(TabsEnum.Start) + preferredOrder.push(TabsEnum.Snippets) for (const tabKey of preferredOrder) { const validKey = getValidTabKey(tabKey) diff --git a/web/app/components/workflow/block-selector/main.tsx b/web/app/components/workflow/block-selector/main.tsx index 5229d273f3..aace98900c 100644 --- a/web/app/components/workflow/block-selector/main.tsx +++ b/web/app/components/workflow/block-selector/main.tsx @@ -15,6 +15,7 @@ import type { import { memo, useCallback, + useEffect, useMemo, useState, } from 'react' @@ -32,6 +33,7 @@ import SearchBox from '@/app/components/plugins/marketplace/search-box' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { BlockEnum, isTriggerNode } from '../types' import { useTabs } from './hooks' +import Snippets from './snippets' import Tabs from './tabs' import { TabsEnum } from './types' @@ -88,6 +90,7 @@ const NodeSelector: FC = ({ const { t } = useTranslation() const nodes = useNodes() const [searchText, setSearchText] = useState('') + const [snippetsLoading, setSnippetsLoading] = useState(() => Boolean(openFromProps) && defaultActiveTab === TabsEnum.Snippets) const [tags, setTags] = useState([]) const [localOpen, setLocalOpen] = useState(false) // Exclude nodes explicitly ignored (such as the node currently being edited) when checking canvas state. @@ -119,28 +122,6 @@ const NodeSelector: FC = ({ // Default rule: user input option is only available when no Start node nor Trigger node exists on canvas. const defaultAllowUserInputSelection = !hasUserInputNode && !hasTriggerNode const canSelectUserInput = allowUserInputSelection ?? defaultAllowUserInputSelection - const open = openFromProps === undefined ? localOpen : openFromProps - const handleOpenChange = useCallback((newOpen: boolean) => { - setLocalOpen(newOpen) - - if (!newOpen) - setSearchText('') - - if (onOpenChange) - onOpenChange(newOpen) - }, [onOpenChange]) - const handleTrigger = useCallback>((e) => { - if (disabled) - return - e.stopPropagation() - handleOpenChange(!open) - }, [handleOpenChange, open, disabled]) - - const handleSelect = useCallback((type, pluginDefaultValue) => { - handleOpenChange(false) - onSelect(type, pluginDefaultValue) - }, [handleOpenChange, onSelect]) - const { activeTab, setActiveTab, @@ -154,10 +135,51 @@ const NodeSelector: FC = ({ hasUserInputNode, forceEnableStartTab, }) + const open = openFromProps === undefined ? localOpen : openFromProps + const handleOpenChange = useCallback((newOpen: boolean) => { + setLocalOpen(newOpen) + + if (!newOpen) { + setSearchText('') + setSnippetsLoading(false) + } + else if (activeTab === TabsEnum.Snippets) { + setSnippetsLoading(true) + } + + if (onOpenChange) + onOpenChange(newOpen) + }, [activeTab, onOpenChange]) + const handleTrigger = useCallback>((e) => { + if (disabled) + return + e.stopPropagation() + handleOpenChange(!open) + }, [handleOpenChange, open, disabled]) + + const handleSelect = useCallback((type, pluginDefaultValue) => { + handleOpenChange(false) + onSelect(type, pluginDefaultValue) + }, [handleOpenChange, onSelect]) const handleActiveTabChange = useCallback((newActiveTab: TabsEnum) => { setActiveTab(newActiveTab) - }, [setActiveTab]) + if (open && newActiveTab === TabsEnum.Snippets) + setSnippetsLoading(true) + }, [open, setActiveTab]) + + useEffect(() => { + if (!snippetsLoading) + return + + const timer = window.setTimeout(() => { + setSnippetsLoading(false) + }, 200) + + return () => { + window.clearTimeout(timer) + } + }, [snippetsLoading]) const searchPlaceholder = useMemo(() => { if (activeTab === TabsEnum.Start) @@ -171,6 +193,8 @@ const NodeSelector: FC = ({ if (activeTab === TabsEnum.Sources) return t('tabs.searchDataSource', { ns: 'workflow' }) + if (activeTab === TabsEnum.Snippets) + return t('tabs.searchSnippets', { ns: 'workflow' }) return '' }, [activeTab, t]) @@ -257,6 +281,17 @@ const NodeSelector: FC = ({ inputClassName="grow" /> )} + {activeTab === TabsEnum.Snippets && ( + setSearchText(e.target.value)} + onClear={() => setSearchText('')} + /> + )} )} onSelect={handleSelect} @@ -268,6 +303,7 @@ const NodeSelector: FC = ({ noTools={noTools} onTagsChange={setTags} forceShowStartContent={forceShowStartContent} + snippetsElem={} /> diff --git a/web/app/components/workflow/block-selector/snippets.tsx b/web/app/components/workflow/block-selector/snippets.tsx new file mode 100644 index 0000000000..dfbdbd3178 --- /dev/null +++ b/web/app/components/workflow/block-selector/snippets.tsx @@ -0,0 +1,247 @@ +import type { ReactNode } from 'react' +import { + memo, + useDeferredValue, + useMemo, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import { + SearchMenu, +} from '@/app/components/base/icons/src/vender/line/others' +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from '@/app/components/base/ui/tooltip' +import { cn } from '@/utils/classnames' +import BlockIcon from '../block-icon' +import { BlockEnum } from '../types' + +type SnippetsProps = { + loading?: boolean + searchText: string +} + +type StaticSnippet = { + id: string + badge: string + badgeClassName: string + title: string + description: string + author?: string + relatedBlocks?: BlockEnum[] +} + +const STATIC_SNIPPETS: StaticSnippet[] = [ + { + id: 'customer-review', + badge: 'CR', + title: 'Customer Review', + description: 'Customer Review Description', + author: 'Evan', + relatedBlocks: [ + BlockEnum.LLM, + BlockEnum.Code, + BlockEnum.KnowledgeRetrieval, + BlockEnum.QuestionClassifier, + BlockEnum.IfElse, + ], + badgeClassName: 'bg-gradient-to-br from-orange-500 to-rose-500', + }, +] as const + +const LoadingSkeleton = () => { + return ( +
+
+ {['skeleton-1', 'skeleton-2', 'skeleton-3', 'skeleton-4'].map((key, index) => ( +
+
+
+
+
+
+ ))} +
+
+
+ ) +} + +const SnippetBadge = ({ + badge, + badgeClassName, +}: Pick) => { + return ( + + ) +} + +const SnippetDetailCard = ({ + author, + description, + relatedBlocks = [], + title, + triggerBadge, +}: { + author?: string + description?: string + relatedBlocks?: BlockEnum[] + title: string + triggerBadge: ReactNode +}) => { + return ( +
+
+
+ {triggerBadge} +
{title}
+
+ {!!description && ( +
+ {description} +
+ )} + {!!relatedBlocks.length && ( +
+ {relatedBlocks.map(block => ( + + ))} +
+ )} +
+ {!!author && ( +
+ {author} +
+ )} +
+ ) +} + +const Snippets = ({ + loading = false, + searchText, +}: SnippetsProps) => { + const { t } = useTranslation() + const deferredSearchText = useDeferredValue(searchText) + const [hoveredSnippetId, setHoveredSnippetId] = useState(null) + + const snippets = useMemo(() => { + return STATIC_SNIPPETS.map(item => ({ + ...item, + })) + }, []) + + const filteredSnippets = useMemo(() => { + const normalizedSearch = deferredSearchText.trim().toLowerCase() + if (!normalizedSearch) + return snippets + + return snippets.filter(item => item.title.toLowerCase().includes(normalizedSearch)) + }, [deferredSearchText, snippets]) + + if (loading) + return + + if (!filteredSnippets.length) { + return ( +
+ +
+ {t('tabs.noSnippetsFound', { ns: 'workflow' })} +
+ +
+ ) + } + + return ( +
+ {filteredSnippets.map((item) => { + const badge = ( + + ) + + const row = ( +
setHoveredSnippetId(item.id)} + onMouseLeave={() => setHoveredSnippetId(current => current === item.id ? null : current)} + > + {badge} +
+ {item.title} +
+ {hoveredSnippetId === item.id && item.author && ( +
+ {item.author} +
+ )} +
+ ) + + if (!item.description) + return
{row}
+ + return ( + + + + + + + ) + })} +
+ ) +} + +export default memo(Snippets) diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index f1eeba7435..601fa44a54 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -40,6 +40,7 @@ export type TabsProps = { noTools?: boolean forceShowStartContent?: boolean // Force show Start content even when noBlocks=true allowStartNodeSelection?: boolean // Allow user input option even when trigger node already exists (e.g. change-node flow or when no Start node yet). + snippetsElem?: React.ReactNode } const Tabs: FC = ({ activeTab, @@ -57,6 +58,7 @@ const Tabs: FC = ({ noTools, forceShowStartContent = false, allowStartNodeSelection = false, + snippetsElem, }) => { const { t } = useTranslation() const { data: buildInTools } = useAllBuiltInTools() @@ -234,6 +236,13 @@ const Tabs: FC = ({ /> ) } + { + activeTab === TabsEnum.Snippets && snippetsElem && ( +
+ {snippetsElem} +
+ ) + }
) } diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 39e7b033bd..108e1dec68 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -7,6 +7,7 @@ export enum TabsEnum { Blocks = 'blocks', Tools = 'tools', Sources = 'sources', + Snippets = 'snippets', } export enum ToolTypeEnum { diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 4d9f5adbac..f2d6398784 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -1090,6 +1090,7 @@ "tabs.allTool": "All", "tabs.allTriggers": "All triggers", "tabs.blocks": "Nodes", + "tabs.createSnippet": "Create a snippet", "tabs.customTool": "Custom", "tabs.featuredTools": "Featured", "tabs.hideActions": "Hide tools", @@ -1099,16 +1100,19 @@ "tabs.noFeaturedTriggers": "Discover more triggers in Marketplace", "tabs.noPluginsFound": "No plugins were found", "tabs.noResult": "No match found", + "tabs.noSnippetsFound": "No snippets were found", "tabs.plugin": "Plugin", "tabs.pluginByAuthor": "By {{author}}", "tabs.question-understand": "Question Understand", "tabs.requestToCommunity": "Requests to the community", "tabs.searchBlock": "Search node", "tabs.searchDataSource": "Search Data Source", + "tabs.searchSnippets": "Search snippets...", "tabs.searchTool": "Search tool", "tabs.searchTrigger": "Search triggers...", "tabs.showLessFeatured": "Show less", "tabs.showMoreFeatured": "Show more", + "tabs.snippets": "Snippets", "tabs.sources": "Sources", "tabs.start": "Start", "tabs.startDisabledTip": "Trigger node and user input node are mutually exclusive.", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index acda7db2fc..82516f5f9f 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -1090,6 +1090,7 @@ "tabs.allTool": "全部", "tabs.allTriggers": "全部触发器", "tabs.blocks": "节点", + "tabs.createSnippet": "创建 snippet", "tabs.customTool": "自定义", "tabs.featuredTools": "精选推荐", "tabs.hideActions": "收起工具", @@ -1099,16 +1100,19 @@ "tabs.noFeaturedTriggers": "前往插件市场查看更多触发器", "tabs.noPluginsFound": "未找到插件", "tabs.noResult": "未找到匹配项", + "tabs.noSnippetsFound": "未找到 snippets", "tabs.plugin": "插件", "tabs.pluginByAuthor": "来自 {{author}}", "tabs.question-understand": "问题理解", "tabs.requestToCommunity": "向社区反馈", "tabs.searchBlock": "搜索节点", "tabs.searchDataSource": "搜索数据源", + "tabs.searchSnippets": "搜索 snippets...", "tabs.searchTool": "搜索工具", "tabs.searchTrigger": "搜索触发器...", "tabs.showLessFeatured": "收起", "tabs.showMoreFeatured": "查看更多", + "tabs.snippets": "Snippets", "tabs.sources": "数据源", "tabs.start": "开始", "tabs.startDisabledTip": "触发节点与用户输入节点互斥。", From c1011f4e5c2a8250c5cd68cdd3dd5ec45fa46024 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Fri, 13 Mar 2026 14:29:59 +0800 Subject: [PATCH 002/120] feat: add to snippet --- .../workflow/create-snippet-dialog.tsx | 178 +++++++++++++++ .../workflow/selection-contextmenu.tsx | 216 +++++++++++------- web/i18n/en-US/workflow.json | 10 + web/i18n/zh-Hans/workflow.json | 10 + 4 files changed, 335 insertions(+), 79 deletions(-) create mode 100644 web/app/components/workflow/create-snippet-dialog.tsx diff --git a/web/app/components/workflow/create-snippet-dialog.tsx b/web/app/components/workflow/create-snippet-dialog.tsx new file mode 100644 index 0000000000..bb42b33d1f --- /dev/null +++ b/web/app/components/workflow/create-snippet-dialog.tsx @@ -0,0 +1,178 @@ +'use client' + +import type { FC } from 'react' +import type { AppIconSelection } from '@/app/components/base/app-icon-picker' +import { useKeyPress } from 'ahooks' +import { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import AppIcon from '@/app/components/base/app-icon' +import AppIconPicker from '@/app/components/base/app-icon-picker' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Textarea from '@/app/components/base/textarea' +import Toast from '@/app/components/base/toast' +import { Dialog, DialogCloseButton, DialogContent, DialogPortal, DialogTitle } from '@/app/components/base/ui/dialog' +import ShortcutsName from './shortcuts-name' + +export type CreateSnippetDialogPayload = { + name: string + description: string + icon: AppIconSelection + selectedNodeIds: string[] +} + +type CreateSnippetDialogProps = { + isOpen: boolean + selectedNodeIds: string[] + onClose: () => void + onConfirm: (payload: CreateSnippetDialogPayload) => void +} + +const defaultIcon: AppIconSelection = { + type: 'emoji', + icon: '🤖', + background: '#FFEAD5', +} + +const CreateSnippetDialog: FC = ({ + isOpen, + selectedNodeIds, + onClose, + onConfirm, +}) => { + const { t } = useTranslation() + const [name, setName] = useState('') + const [description, setDescription] = useState('') + const [icon, setIcon] = useState(defaultIcon) + const [showAppIconPicker, setShowAppIconPicker] = useState(false) + + const resetForm = useCallback(() => { + setName('') + setDescription('') + setIcon(defaultIcon) + setShowAppIconPicker(false) + }, []) + + const handleClose = useCallback(() => { + resetForm() + onClose() + }, [onClose, resetForm]) + + const handleConfirm = useCallback(() => { + const trimmedName = name.trim() + const trimmedDescription = description.trim() + + if (!trimmedName) + return + + const payload = { + name: trimmedName, + description: trimmedDescription, + icon, + selectedNodeIds, + } + + onConfirm(payload) + Toast.notify({ + type: 'success', + message: t('snippet.createSuccess', { ns: 'workflow' }), + }) + handleClose() + }, [description, handleClose, icon, name, onConfirm, selectedNodeIds, t]) + + useKeyPress(['meta.enter', 'ctrl.enter'], () => { + if (!isOpen) + return + + handleConfirm() + }) + + return ( + <> + !open && handleClose()}> + + + +
+ + {t('snippet.createDialogTitle', { ns: 'workflow' })} + +
+ +
+
+
+
+ {t('snippet.nameLabel', { ns: 'workflow' })} +
+ setName(e.target.value)} + placeholder={t('snippet.namePlaceholder', { ns: 'workflow' }) || ''} + autoFocus + /> +
+ + setShowAppIconPicker(true)} + /> +
+ +
+
+ {t('snippet.descriptionLabel', { ns: 'workflow' })} +
+ -

Status: "{{ status }}"

-
'''code block'''
- -""" - inputs = {"task": {"Task ID": "TASK-123", "Issues": "Line 1\nLine 2\nLine 3"}, "status": "completed"} - - result = CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code=template, inputs=inputs) - - # Verify the template rendered correctly with all special characters - output = result["result"] - assert 'value="TASK-123"' in output - assert "" in output - assert 'Status: "completed"' in output - assert "'''code block'''" in output - - -def test_jinja2_template_with_html_textarea_prefill(): - """ - Specific test for HTML textarea with Jinja2 variable pre-fill. - Verifies fix for issue #26818. - """ - template = "" - notes_content = "This is a multi-line note.\nWith special chars: 'single' and \"double\" quotes." - inputs = {"notes": notes_content} - - result = CodeExecutor.execute_workflow_code_template(language=CODE_LANGUAGE, code=template, inputs=inputs) - - expected_output = f"" - assert result["result"] == expected_output - - -def test_jinja2_assemble_runner_script_encodes_template(): - """Test that assemble_runner_script properly base64 encodes the template.""" - template = "Hello {{ name }}!" - inputs = {"name": "World"} - - script = Jinja2TemplateTransformer.assemble_runner_script(template, inputs) - - # The template should be base64 encoded in the script - template_b64 = base64.b64encode(template.encode("utf-8")).decode("utf-8") - assert template_b64 in script - # The raw template should NOT appear in the script (it's encoded) - assert "Hello {{ name }}!" not in script From 425457cb16845819d028775d45e3172cf6fa6003 Mon Sep 17 00:00:00 2001 From: James <63717587+jamesrayammons@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:53:53 +0200 Subject: [PATCH 105/120] test: remove legacy workflow draft variable api test (#35226) --- .../app/test_workflow_draft_variable.py | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py diff --git a/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py b/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py deleted file mode 100644 index 038f37af5f..0000000000 --- a/api/tests/integration_tests/controllers/console/app/test_workflow_draft_variable.py +++ /dev/null @@ -1,47 +0,0 @@ -import uuid -from unittest import mock - -from controllers.console.app import workflow_draft_variable as draft_variable_api -from controllers.console.app import wraps -from factories.variable_factory import build_segment -from models import App, AppMode -from models.workflow import WorkflowDraftVariable -from services.workflow_draft_variable_service import WorkflowDraftVariableList, WorkflowDraftVariableService - - -def _get_mock_srv_class() -> type[WorkflowDraftVariableService]: - return mock.create_autospec(WorkflowDraftVariableService) - - -class TestWorkflowDraftNodeVariableListApi: - def test_get(self, test_client, auth_header, monkeypatch): - srv_class = _get_mock_srv_class() - mock_app_model: App = App() - mock_app_model.id = str(uuid.uuid4()) - test_node_id = "test_node_id" - mock_app_model.mode = AppMode.ADVANCED_CHAT - mock_load_app_model = mock.Mock(return_value=mock_app_model) - - monkeypatch.setattr(draft_variable_api, "WorkflowDraftVariableService", srv_class) - monkeypatch.setattr(wraps, "_load_app_model", mock_load_app_model) - - var1 = WorkflowDraftVariable.new_node_variable( - app_id="test_app_1", - node_id="test_node_1", - name="str_var", - value=build_segment("str_value"), - node_execution_id=str(uuid.uuid4()), - ) - srv_instance = mock.create_autospec(WorkflowDraftVariableService, instance=True) - srv_class.return_value = srv_instance - srv_instance.list_node_variables.return_value = WorkflowDraftVariableList(variables=[var1]) - - response = test_client.get( - f"/console/api/apps/{mock_app_model.id}/workflows/draft/nodes/{test_node_id}/variables", - headers=auth_header, - ) - assert response.status_code == 200 - response_dict = response.json - assert isinstance(response_dict, dict) - assert "items" in response_dict - assert len(response_dict["items"]) == 1 From dbceb3067e5c1ce177bf60ec8617b53e2d831ac3 Mon Sep 17 00:00:00 2001 From: NVIDIAN Date: Wed, 15 Apr 2026 02:57:27 -0700 Subject: [PATCH 106/120] refactor(api): migrate console tag responses from marshal_with to BaseModel (#35208) Co-authored-by: ai-hpc --- api/controllers/console/tag/tags.py | 56 +++++++++++++------ .../controllers/console/tag/test_tags.py | 26 ++++++++- 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/api/controllers/console/tag/tags.py b/api/controllers/console/tag/tags.py index 39b84d3869..614bf03ea5 100644 --- a/api/controllers/console/tag/tags.py +++ b/api/controllers/console/tag/tags.py @@ -1,13 +1,14 @@ from typing import Literal from flask import request -from flask_restx import Namespace, Resource, fields, marshal_with -from pydantic import BaseModel, Field +from flask_restx import Resource +from pydantic import BaseModel, Field, field_validator from werkzeug.exceptions import Forbidden from controllers.common.schema import register_schema_models from controllers.console import console_ns from controllers.console.wraps import account_initialization_required, edit_permission_required, setup_required +from fields.base import ResponseModel from libs.login import current_account_with_tenant, login_required from models.enums import TagType from services.tag_service import ( @@ -18,17 +19,6 @@ from services.tag_service import ( UpdateTagPayload, ) -dataset_tag_fields = { - "id": fields.String, - "name": fields.String, - "type": fields.String, - "binding_count": fields.String, -} - - -def build_dataset_tag_fields(api_or_ns: Namespace): - return api_or_ns.model("DataSetTag", dataset_tag_fields) - class TagBasePayload(BaseModel): name: str = Field(description="Tag name", min_length=1, max_length=50) @@ -52,12 +42,36 @@ class TagListQueryParam(BaseModel): keyword: str | None = Field(None, description="Search keyword") +class TagResponse(ResponseModel): + id: str + name: str + type: str | None = None + binding_count: str | None = None + + @field_validator("type", mode="before") + @classmethod + def normalize_type(cls, value: TagType | str | None) -> str | None: + if value is None: + return None + if isinstance(value, TagType): + return value.value + return value + + @field_validator("binding_count", mode="before") + @classmethod + def normalize_binding_count(cls, value: int | str | None) -> str | None: + if value is None: + return None + return str(value) + + register_schema_models( console_ns, TagBasePayload, TagBindingPayload, TagBindingRemovePayload, TagListQueryParam, + TagResponse, ) @@ -69,14 +83,18 @@ class TagListApi(Resource): @console_ns.doc( params={"type": 'Tag type filter. Can be "knowledge" or "app".', "keyword": "Search keyword for tag name."} ) - @marshal_with(dataset_tag_fields) + @console_ns.doc(responses={200: ("Success", [console_ns.models[TagResponse.__name__]])}) def get(self): _, current_tenant_id = current_account_with_tenant() raw_args = request.args.to_dict() param = TagListQueryParam.model_validate(raw_args) tags = TagService.get_tags(param.type, current_tenant_id, param.keyword) - return tags, 200 + serialized_tags = [ + TagResponse.model_validate(tag, from_attributes=True).model_dump(mode="json") for tag in tags + ] + + return serialized_tags, 200 @console_ns.expect(console_ns.models[TagBasePayload.__name__]) @setup_required @@ -91,7 +109,9 @@ class TagListApi(Resource): payload = TagBasePayload.model_validate(console_ns.payload or {}) tag = TagService.save_tags(SaveTagPayload(name=payload.name, type=payload.type)) - response = {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0} + response = TagResponse.model_validate( + {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": 0} + ).model_dump(mode="json") return response, 200 @@ -114,7 +134,9 @@ class TagUpdateDeleteApi(Resource): binding_count = TagService.get_tag_binding_count(tag_id) - response = {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": binding_count} + response = TagResponse.model_validate( + {"id": tag.id, "name": tag.name, "type": tag.type, "binding_count": binding_count} + ).model_dump(mode="json") return response, 200 diff --git a/api/tests/unit_tests/controllers/console/tag/test_tags.py b/api/tests/unit_tests/controllers/console/tag/test_tags.py index e89b89c8b1..2be5a21f28 100644 --- a/api/tests/unit_tests/controllers/console/tag/test_tags.py +++ b/api/tests/unit_tests/controllers/console/tag/test_tags.py @@ -1,9 +1,11 @@ +from types import SimpleNamespace from unittest.mock import MagicMock, PropertyMock, patch import pytest from flask import Flask from werkzeug.exceptions import Forbidden +import controllers.console.tag.tags as module from controllers.console import console_ns from controllers.console.tag.tags import ( TagBindingCreateApi, @@ -83,13 +85,20 @@ class TestTagListApi: ), patch( "controllers.console.tag.tags.TagService.get_tags", - return_value=[{"id": "1", "name": "tag"}], + return_value=[ + SimpleNamespace( + id="1", + name="tag", + type=TagType.KNOWLEDGE, + binding_count=1, + ) + ], ), ): result, status = method(api) assert status == 200 - assert isinstance(result, list) + assert result == [{"id": "1", "name": "tag", "type": "knowledge", "binding_count": "1"}] def test_post_success(self, app, admin_user, tag, payload_patch): api = TagListApi() @@ -113,6 +122,7 @@ class TestTagListApi: assert status == 200 assert result["name"] == "test-tag" + assert result["binding_count"] == "0" def test_post_forbidden(self, app, readonly_user, payload_patch): api = TagListApi() @@ -158,7 +168,7 @@ class TestTagUpdateDeleteApi: result, status = method(api, "tag-1") assert status == 200 - assert result["binding_count"] == 3 + assert result["binding_count"] == "3" def test_patch_forbidden(self, app, readonly_user, payload_patch): api = TagUpdateDeleteApi() @@ -277,3 +287,13 @@ class TestTagBindingDeleteApi: ): with pytest.raises(Forbidden): method(api) + + +class TestTagResponseModel: + def test_tag_response_normalizes_enum_type(self): + payload = module.TagResponse.model_validate( + {"id": "tag-1", "name": "tag", "type": TagType.KNOWLEDGE, "binding_count": 1} + ).model_dump(mode="json") + + assert payload["type"] == "knowledge" + assert payload["binding_count"] == "1" From f1da2c76d100f615a6fa0c0e0d56be04c73f68f3 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Wed, 15 Apr 2026 18:22:20 +0800 Subject: [PATCH 107/120] fix(web): add page title for snippet --- web/app/components/apps/creators-filter.tsx | 2 +- .../__tests__/snippet-layout.spec.tsx | 108 ++++++++++++++++++ .../snippets/components/snippet-layout.tsx | 3 + 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 web/app/components/snippets/components/__tests__/snippet-layout.spec.tsx diff --git a/web/app/components/apps/creators-filter.tsx b/web/app/components/apps/creators-filter.tsx index 7a268c5136..4bcff660e5 100644 --- a/web/app/components/apps/creators-filter.tsx +++ b/web/app/components/apps/creators-filter.tsx @@ -103,7 +103,7 @@ const CreatorsFilter = ({ baseChipClassName, isSelected ? 'border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-state-base-hover' - : 'border-transparent bg-[#f9f9f9] text-text-tertiary hover:bg-components-input-bg-hover', + : 'border-transparent bg-components-input-bg-normal text-text-tertiary hover:bg-components-input-bg-hover', )} /> )} diff --git a/web/app/components/snippets/components/__tests__/snippet-layout.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-layout.spec.tsx new file mode 100644 index 0000000000..3202c3dc43 --- /dev/null +++ b/web/app/components/snippets/components/__tests__/snippet-layout.spec.tsx @@ -0,0 +1,108 @@ +import type { ReactNode } from 'react' +import type { SnippetDetail } from '@/models/snippet' +import { render, screen } from '@testing-library/react' +import SnippetLayout from '../snippet-layout' + +const mockSetAppSidebarExpand = vi.fn() +const mockUseDocumentTitle = vi.fn() + +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: { setAppSidebarExpand: typeof mockSetAppSidebarExpand }) => unknown) => selector({ + setAppSidebarExpand: mockSetAppSidebarExpand, + }), +})) + +vi.mock('@/hooks/use-breakpoints', () => ({ + default: () => 'desktop', + MediaType: { + mobile: 'mobile', + desktop: 'desktop', + }, +})) + +vi.mock('@/hooks/use-document-title', () => ({ + default: (title: string) => mockUseDocumentTitle(title), +})) + +vi.mock('@/app/components/app-sidebar', () => ({ + default: ({ + renderHeader, + renderNavigation, + }: { + renderHeader?: (mode: string) => ReactNode + renderNavigation?: (mode: string) => ReactNode + }) => ( +
+ {renderHeader?.('expand')} + {renderNavigation?.('expand')} +
+ ), +})) + +vi.mock('@/app/components/app-sidebar/nav-link', () => ({ + default: ({ name, href, active }: { name: string, href: string, active: boolean }) => ( + + {name} + + ), +})) + +vi.mock('@/app/components/app-sidebar/snippet-info', () => ({ + default: ({ snippet }: { snippet: SnippetDetail }) =>
{snippet.name}
, +})) + +const createSnippet = (overrides: Partial = {}): SnippetDetail => ({ + id: 'snippet-1', + name: 'Snippet Title', + description: 'Snippet description', + author: 'tester', + updatedAt: '2026-04-15', + usage: '42', + icon: 'emoji', + iconBackground: '#ffffff', + ...overrides, +}) + +describe('SnippetLayout', () => { + beforeEach(() => { + vi.clearAllMocks() + localStorage.clear() + }) + + describe('Document title', () => { + it('should set the document title to the snippet name when snippet detail is available', () => { + render( + +
content
+
, + ) + + expect(mockUseDocumentTitle).toHaveBeenCalledWith('Snippet Title') + }) + }) + + describe('Navigation', () => { + it('should render snippet navigation links', () => { + render( + +
content
+
, + ) + + expect(screen.getByRole('link', { name: 'snippet.sectionOrchestrate' })).toHaveAttribute('href', '/snippets/snippet-1/orchestrate') + expect(screen.getByRole('link', { name: 'snippet.sectionEvaluation' })).toHaveAttribute('href', '/snippets/snippet-1/evaluation') + expect(screen.getByRole('link', { name: 'snippet.sectionEvaluation' })).toHaveAttribute('aria-current', 'page') + }) + }) +}) diff --git a/web/app/components/snippets/components/snippet-layout.tsx b/web/app/components/snippets/components/snippet-layout.tsx index 37900f3969..2d03f3a065 100644 --- a/web/app/components/snippets/components/snippet-layout.tsx +++ b/web/app/components/snippets/components/snippet-layout.tsx @@ -16,6 +16,7 @@ import NavLink from '@/app/components/app-sidebar/nav-link' import SnippetInfo from '@/app/components/app-sidebar/snippet-info' import { useStore as useAppStore } from '@/app/components/app/store' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' +import useDocumentTitle from '@/hooks/use-document-title' type SnippetLayoutProps = { children: ReactNode @@ -45,6 +46,8 @@ const SnippetLayout = ({ const isMobile = media === MediaType.mobile const setAppSidebarExpand = useAppStore(state => state.setAppSidebarExpand) + useDocumentTitle(snippet.name || t('typeLabel')) + useEffect(() => { const localeMode = localStorage.getItem('app-detail-collapse-or-expand') || 'expand' const mode = isMobile ? 'collapse' : 'expand' From af7d5e60b47503a0c5c3751fa783d63654644a21 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:11:20 +0800 Subject: [PATCH 108/120] feat(ui): scaffold @langgenius/dify-ui and migrate design tokens (#35256) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- packages/dify-ui/AGENTS.md | 27 + packages/dify-ui/package.json | 24 + .../dify-ui/src/cn.ts | 0 packages/dify-ui/src/styles/styles.css | 3 + packages/dify-ui/src/styles/utilities.css | 272 ++ packages/dify-ui/src/tailwind-preset.ts | 87 + {web => packages/dify-ui/src}/themes/dark.css | 0 .../dify-ui/src}/themes/light.css | 0 .../src}/themes/tailwind-theme-var-define.ts | 0 packages/dify-ui/tsconfig.json | 18 + pnpm-lock.yaml | 85 +- pnpm-workspace.yaml | 85 +- web/AGENTS.md | 4 + web/README.md | 2 - .../plugins/plugin-auth-flow.test.tsx | 2 +- .../plugins/plugin-card-rendering.test.tsx | 2 +- .../tool-browsing-and-filtering.test.tsx | 2 +- .../tools/tool-provider-detail-flow.test.tsx | 2 +- .../(appDetailLayout)/[appId]/layout-main.tsx | 2 +- .../time-range-picker/date-picker.tsx | 4 +- .../time-range-picker/range-selector.tsx | 8 +- .../overview/tracing/config-button.tsx | 2 +- .../[appId]/overview/tracing/config-popup.tsx | 8 +- .../[appId]/overview/tracing/field.tsx | 2 +- .../[appId]/overview/tracing/panel.tsx | 12 +- .../overview/tracing/provider-panel.tsx | 6 +- .../[appId]/overview/tracing/tracing-icon.tsx | 4 +- .../[datasetId]/layout-main.tsx | 2 +- .../(humanInputLayout)/form/[token]/form.tsx | 14 +- .../webapp-reset-password/layout.tsx | 6 +- .../set-password/page.tsx | 2 +- .../(shareLayout)/webapp-signin/layout.tsx | 4 +- .../webapp-signin/normalForm.tsx | 24 +- web/app/account/(commonLayout)/avatar.tsx | 4 +- web/app/account/oauth/authorize/layout.tsx | 6 +- web/app/activate/activateForm.tsx | 6 +- web/app/activate/page.tsx | 2 +- .../text-squeeze-fix-verification.spec.tsx | 2 +- .../app-sidebar/app-info/app-info-trigger.tsx | 6 +- .../app-sidebar/app-info/app-operations.tsx | 2 +- .../app-sidebar/app-sidebar-dropdown.tsx | 12 +- .../app-sidebar/dataset-info/dropdown.tsx | 2 +- .../app-sidebar/dataset-info/index.tsx | 12 +- .../app-sidebar/dataset-sidebar-dropdown.tsx | 14 +- web/app/components/app-sidebar/index.tsx | 4 +- .../components/app-sidebar/nav-link/index.tsx | 8 +- .../components/app-sidebar/toggle-button.tsx | 2 +- .../app/annotation/batch-action.tsx | 4 +- .../csv-uploader.tsx | 2 +- .../edit-annotation-modal/edit-item/index.tsx | 2 +- .../app/annotation/header-opts/index.tsx | 4 +- web/app/components/app/annotation/index.tsx | 6 +- web/app/components/app/annotation/list.tsx | 18 +- .../view-annotation-modal/index.tsx | 22 +- .../access-control-dialog.tsx | 4 +- .../access-control-item.tsx | 4 +- .../add-member-or-group-pop.tsx | 2 +- .../app/app-publisher/suggested-action.tsx | 4 +- .../base/feature-panel/index.tsx | 4 +- .../base/operation-btn/index.tsx | 4 +- .../base/warning-mask/has-not-set-api.tsx | 10 +- .../config-prompt/advanced-prompt-input.tsx | 2 +- .../config-prompt/message-type-selector.tsx | 8 +- .../prompt-editor-height-resize-wrap.tsx | 2 +- .../config-prompt/simple-prompt-input.tsx | 8 +- .../config-var/config-modal/field.tsx | 6 +- .../config-var/config-modal/form-fields.tsx | 2 +- .../config-var/config-modal/type-select.tsx | 2 +- .../config-var/config-select/index.tsx | 8 +- .../app/configuration/config-var/index.tsx | 2 +- .../config-var/select-type-item/index.tsx | 4 +- .../app/configuration/config-var/var-item.tsx | 12 +- .../app/configuration/config-vision/index.tsx | 12 +- .../config-vision/param-config.tsx | 2 +- .../config/agent/agent-setting/item-panel.tsx | 4 +- .../config/agent/agent-tools/index.tsx | 2 +- .../agent-tools/setting-built-in-tool.tsx | 2 +- .../config/assistant-type-picker/index.tsx | 20 +- .../config/automatic/idea-output.tsx | 6 +- .../config/automatic/instruction-editor.tsx | 10 +- .../config/automatic/prompt-toast.tsx | 4 +- .../config/automatic/version-selector.tsx | 10 +- .../dataset-config/card-item/index.tsx | 4 +- .../dataset-config/context-var/index.tsx | 4 +- .../dataset-config/context-var/var-picker.tsx | 10 +- .../configuration/dataset-config/index.tsx | 4 +- .../params-config/config-content.tsx | 22 +- .../dataset-config/params-config/index.tsx | 2 +- .../dataset-config/select-dataset/index.tsx | 2 +- .../dataset-config/settings-modal/index.tsx | 2 +- .../settings-modal/retrieval-section.tsx | 10 +- .../configuration/debug/chat-user-input.tsx | 6 +- .../app/configuration/debug/index.tsx | 4 +- .../prompt-value-panel/index.tsx | 2 +- .../app/create-app-dialog/app-card/index.tsx | 4 +- .../app/create-app-dialog/app-list/index.tsx | 6 +- .../create-app-dialog/app-list/sidebar.tsx | 4 +- .../components/app/create-app-modal/index.tsx | 4 +- .../app/create-from-dsl-modal/index.tsx | 2 +- .../app/create-from-dsl-modal/uploader.tsx | 10 +- .../components/app/duplicate-modal/index.tsx | 2 +- .../components/app/in-site-message/index.tsx | 2 +- .../components/app/log-annotation/index.tsx | 2 +- web/app/components/app/log/list.tsx | 38 +- web/app/components/app/log/model-info.tsx | 8 +- web/app/components/app/log/var-panel.tsx | 14 +- .../app/overview/apikey-info-panel/index.tsx | 2 +- .../app/overview/embedded/index.tsx | 10 +- .../app/overview/settings/index.tsx | 2 +- .../components/app/switch-app-modal/index.tsx | 4 +- .../app/text-generate/item/action-groups.tsx | 6 +- .../app/text-generate/item/index.tsx | 10 +- .../app/text-generate/item/workflow-body.tsx | 2 +- .../app/text-generate/saved-items/index.tsx | 8 +- .../saved-items/no-data/index.tsx | 2 +- .../components/app/type-selector/index.tsx | 10 +- web/app/components/app/workflow-log/list.tsx | 28 +- web/app/components/apps/app-card.tsx | 22 +- web/app/components/apps/list.tsx | 18 +- web/app/components/apps/new-app-card.tsx | 10 +- .../components/base/action-button/index.tsx | 2 +- .../base/agent-log-modal/detail.tsx | 6 +- .../components/base/agent-log-modal/index.tsx | 4 +- .../base/agent-log-modal/iteration.tsx | 6 +- .../base/agent-log-modal/tool-call.tsx | 8 +- web/app/components/base/alert.tsx | 4 +- web/app/components/base/answer-icon/index.tsx | 2 +- .../base/app-icon-picker/ImageInput.tsx | 6 +- .../components/base/app-icon-picker/index.tsx | 2 +- .../base/app-icon/__tests__/index.spec.tsx | 6 +- web/app/components/base/app-icon/index.tsx | 34 +- web/app/components/base/app-unavailable.tsx | 4 +- .../base/audio-gallery/AudioPlayer.tsx | 10 +- .../base/auto-height-textarea/index.tsx | 4 +- web/app/components/base/badge.tsx | 6 +- web/app/components/base/badge/index.tsx | 2 +- web/app/components/base/block-input/index.tsx | 4 +- web/app/components/base/carousel/index.tsx | 2 +- .../chat/chat-with-history/chat-wrapper.tsx | 4 +- .../chat/chat-with-history/header/index.tsx | 2 +- .../chat-with-history/header/operation.tsx | 10 +- .../base/chat/chat-with-history/index.tsx | 2 +- .../chat-with-history/inputs-form/content.tsx | 2 +- .../chat-with-history/inputs-form/index.tsx | 2 +- .../chat/chat-with-history/sidebar/index.tsx | 4 +- .../chat/chat-with-history/sidebar/item.tsx | 4 +- .../chat-with-history/sidebar/operation.tsx | 8 +- .../base/chat/chat/answer/basic-content.tsx | 2 +- .../human-input-content/content-wrapper.tsx | 4 +- .../human-input-content/expiration-time.tsx | 4 +- .../base/chat/chat/answer/index.tsx | 10 +- .../base/chat/chat/answer/operation.tsx | 14 +- .../chat/chat/answer/suggested-questions.tsx | 4 +- .../base/chat/chat/answer/tool-detail.tsx | 16 +- .../chat/chat/answer/workflow-process.tsx | 16 +- .../base/chat/chat/chat-input-area/index.tsx | 8 +- .../chat/chat/chat-input-area/operation.tsx | 2 +- web/app/components/base/chat/chat/index.tsx | 2 +- .../base/chat/chat/loading-anim/index.tsx | 2 +- .../components/base/chat/chat/log/index.tsx | 2 +- .../components/base/chat/chat/question.tsx | 4 +- .../chat/embedded-chatbot/chat-wrapper.tsx | 6 +- .../chat/embedded-chatbot/header/index.tsx | 4 +- .../base/chat/embedded-chatbot/index.tsx | 2 +- .../embedded-chatbot/inputs-form/content.tsx | 8 +- .../embedded-chatbot/inputs-form/index.tsx | 2 +- .../inputs-form/view-form-dropdown.tsx | 4 +- .../components/base/checkbox-list/index.tsx | 2 +- web/app/components/base/checkbox/index.tsx | 4 +- web/app/components/base/chip/index.tsx | 4 +- .../components/base/content-dialog/index.tsx | 4 +- .../components/base/corner-label/index.tsx | 2 +- .../date-and-time-picker/calendar/item.tsx | 6 +- .../common/option-list-item.tsx | 4 +- .../date-picker/footer.tsx | 2 +- .../date-picker/index.tsx | 8 +- .../time-picker/index.tsx | 10 +- .../time-picker/options.tsx | 6 +- .../year-and-month-picker/options.tsx | 4 +- web/app/components/base/dialog/index.tsx | 6 +- web/app/components/base/divider/index.tsx | 2 +- web/app/components/base/drawer-plus/index.tsx | 8 +- web/app/components/base/drawer/index.tsx | 2 +- web/app/components/base/dropdown/index.tsx | 2 +- web/app/components/base/effect/index.tsx | 2 +- .../components/base/emoji-picker/Inner.tsx | 10 +- .../components/base/emoji-picker/index.tsx | 2 +- .../base/encrypted-bottom/index.tsx | 4 +- .../components/base/error-boundary/index.tsx | 2 +- .../conversation-opener/modal.tsx | 2 +- .../new-feature-panel/dialog-wrapper.tsx | 6 +- .../new-feature-panel/feature-bar.tsx | 2 +- .../moderation/moderation-setting-modal.tsx | 2 +- .../text-to-speech/param-config-content.tsx | 18 +- web/app/components/base/file-thumb/index.tsx | 4 +- .../file-from-link-or-local/index.tsx | 2 +- .../base/file-uploader/file-image-render.tsx | 2 +- .../base/file-uploader/file-list-in-log.tsx | 8 +- .../base/file-uploader/file-type-icon.tsx | 2 +- .../file-uploader-in-attachment/file-item.tsx | 8 +- .../file-uploader-in-attachment/index.tsx | 2 +- .../file-uploader-in-chat-input/file-item.tsx | 2 +- .../file-uploader-in-chat-input/file-list.tsx | 2 +- .../file-uploader-in-chat-input/index.tsx | 2 +- .../base/form/components/base/base-field.tsx | 10 +- .../base/form/components/base/base-form.tsx | 2 +- .../base/form/components/field/checkbox.tsx | 4 +- .../form/components/field/custom-select.tsx | 2 +- .../base/form/components/field/file-types.tsx | 2 +- .../form/components/field/file-uploader.tsx | 2 +- .../field/input-type-select/index.tsx | 2 +- .../field/input-type-select/trigger.tsx | 2 +- .../field/mixed-variable-text-input/index.tsx | 2 +- .../form/components/field/number-input.tsx | 2 +- .../form/components/field/number-slider.tsx | 4 +- .../base/form/components/field/options.tsx | 2 +- .../base/form/components/field/select.tsx | 2 +- .../base/form/components/field/text-area.tsx | 2 +- .../base/form/components/field/text.tsx | 2 +- .../form/components/field/upload-method.tsx | 2 +- .../field/variable-or-constant-input.tsx | 2 +- .../components/field/variable-selector.tsx | 2 +- .../components/base/form/components/label.tsx | 6 +- .../fullscreen-modal/__tests__/index.spec.tsx | 4 +- .../base/fullscreen-modal/index.tsx | 8 +- web/app/components/base/grid-mask/index.tsx | 2 +- .../icons/src/image/llm/BaichuanTextCn.tsx | 2 +- .../base/icons/src/image/llm/Minimax.tsx | 2 +- .../base/icons/src/image/llm/MinimaxText.tsx | 2 +- .../base/icons/src/image/llm/Tongyi.tsx | 2 +- .../base/icons/src/image/llm/TongyiText.tsx | 2 +- .../base/icons/src/image/llm/TongyiTextCn.tsx | 2 +- .../base/icons/src/image/llm/Wxyy.tsx | 2 +- .../base/icons/src/image/llm/WxyyText.tsx | 2 +- .../base/icons/src/image/llm/WxyyTextCn.tsx | 2 +- .../components/base/image-gallery/index.tsx | 2 +- .../image-uploader/chat-image-uploader.tsx | 4 +- .../base/image-uploader/image-list.tsx | 7 +- .../base/inline-delete-confirm/index.tsx | 4 +- .../components/base/input-with-copy/index.tsx | 6 +- .../base/input/__tests__/index.spec.tsx | 6 +- web/app/components/base/input/index.tsx | 16 +- .../base/linked-apps-panel/index.tsx | 6 +- web/app/components/base/list-empty/index.tsx | 14 +- web/app/components/base/loading/index.tsx | 2 +- web/app/components/base/logo/dify-logo.tsx | 2 +- .../base/logo/logo-embedded-chat-header.tsx | 2 +- web/app/components/base/logo/logo-site.tsx | 2 +- .../base/markdown-blocks/button.tsx | 2 +- .../base/markdown-blocks/think-block.tsx | 4 +- .../components/with-icon-card-item.tsx | 4 +- .../components/with-icon-card-list.tsx | 2 +- web/app/components/base/markdown/index.tsx | 2 +- web/app/components/base/mermaid/index.tsx | 4 +- .../base/message-log-modal/index.tsx | 6 +- .../components/base/modal-like-wrap/index.tsx | 2 +- web/app/components/base/modal/index.css | 7 - web/app/components/base/modal/index.tsx | 2 +- web/app/components/base/modal/modal.tsx | 2 +- web/app/components/base/node-status/index.tsx | 2 +- .../base/notion-connector/index.tsx | 2 +- web/app/components/base/notion-icon/index.tsx | 2 +- .../page-selector/page-row.tsx | 2 +- .../search-input/index.tsx | 4 +- web/app/components/base/pagination/index.tsx | 6 +- .../components/base/pagination/pagination.tsx | 2 +- web/app/components/base/popover/index.tsx | 2 +- .../base/portal-to-follow-elem/index.tsx | 2 +- .../premium-badge/__tests__/index.spec.tsx | 2 +- .../components/base/premium-badge/index.css | 10 +- .../components/base/premium-badge/index.tsx | 6 +- .../base/progress-bar/progress-circle.tsx | 2 +- .../components/base/prompt-editor/index.tsx | 2 +- .../plugins/current-block/component.tsx | 6 +- .../plugins/draggable-plugin/index.tsx | 8 +- .../plugins/error-message-block/component.tsx | 6 +- .../plugins/hitl-input-block/component-ui.tsx | 6 +- .../plugins/hitl-input-block/input-field.tsx | 4 +- .../plugins/hitl-input-block/pre-populate.tsx | 4 +- .../plugins/hitl-input-block/tag-label.tsx | 4 +- .../plugins/hitl-input-block/type-switch.tsx | 4 +- .../plugins/last-run-block/component.tsx | 6 +- .../prompt-editor/plugins/placeholder.tsx | 4 +- .../plugins/request-url-block/component.tsx | 6 +- .../plugins/shortcuts-popup-plugin/index.tsx | 2 +- .../prompt-editor/prompt-editor-content.tsx | 2 +- web/app/components/base/radio-card/index.tsx | 6 +- .../base/radio-card/simple/index.tsx | 6 +- .../base/radio/component/group/index.tsx | 2 +- .../base/radio/component/radio/index.tsx | 2 +- web/app/components/base/radio/ui.tsx | 2 +- .../components/base/search-input/index.tsx | 6 +- .../__tests__/index.spec.tsx | 4 +- .../base/segmented-control/index.css | 36 +- .../base/segmented-control/index.tsx | 26 +- web/app/components/base/select/custom.tsx | 6 +- web/app/components/base/select/index.tsx | 16 +- web/app/components/base/select/pure.tsx | 8 +- .../base/simple-pie-chart/index.tsx | 2 +- web/app/components/base/skeleton/index.tsx | 2 +- web/app/components/base/sort/index.tsx | 4 +- web/app/components/base/svg/index.tsx | 2 +- .../base/switch/__tests__/index.spec.tsx | 6 +- web/app/components/base/switch/index.tsx | 10 +- web/app/components/base/switch/skeleton.tsx | 6 +- web/app/components/base/tab-header/index.tsx | 4 +- .../components/base/tab-slider-new/index.tsx | 4 +- .../base/tab-slider-plain/index.tsx | 4 +- web/app/components/base/tab-slider/index.tsx | 8 +- web/app/components/base/tag-input/index.tsx | 6 +- .../components/base/tag-management/filter.tsx | 12 +- .../base/tag-management/selector.tsx | 2 +- .../base/tag-management/tag-item-editor.tsx | 6 +- .../base/tag-management/tag-remove-modal.tsx | 2 +- web/app/components/base/tag/index.tsx | 2 +- web/app/components/base/textarea/index.tsx | 2 +- web/app/components/base/theme-switcher.tsx | 4 +- .../components/base/timezone-label/index.tsx | 4 +- web/app/components/base/tooltip/index.tsx | 4 +- .../components/base/ui/alert-dialog/index.tsx | 2 +- web/app/components/base/ui/avatar/index.tsx | 2 +- web/app/components/base/ui/button/index.tsx | 2 +- .../components/base/ui/context-menu/index.tsx | 2 +- web/app/components/base/ui/dialog/index.tsx | 2 +- .../base/ui/dropdown-menu/index.tsx | 2 +- .../ui/number-field/__tests__/index.spec.tsx | 4 +- .../base/ui/number-field/index.stories.tsx | 2 +- .../components/base/ui/number-field/index.tsx | 6 +- web/app/components/base/ui/popover/index.tsx | 2 +- .../ui/scroll-area/__tests__/index.spec.tsx | 4 +- .../base/ui/scroll-area/index.stories.tsx | 20 +- .../components/base/ui/scroll-area/index.tsx | 4 +- web/app/components/base/ui/select/index.tsx | 4 +- web/app/components/base/ui/slider/index.tsx | 2 +- web/app/components/base/ui/toast/index.tsx | 2 +- web/app/components/base/ui/tooltip/index.tsx | 2 +- web/app/components/base/voice-input/index.tsx | 6 +- .../billing/annotation-full/index.tsx | 4 +- .../billing/annotation-full/modal.tsx | 4 +- .../billing/apps-full-in-dialog/index.tsx | 2 +- .../billing/header-billing-btn/index.tsx | 2 +- web/app/components/billing/pricing/footer.tsx | 2 +- web/app/components/billing/pricing/header.tsx | 2 +- .../billing/pricing/plan-switcher/tab.tsx | 2 +- .../pricing/plans/cloud-plan-item/button.tsx | 4 +- .../cloud-plan-item/list/item/tooltip.tsx | 4 +- .../plans/self-hosted-plan-item/button.tsx | 6 +- .../plans/self-hosted-plan-item/index.tsx | 18 +- .../billing/priority-label/index.tsx | 4 +- .../components/billing/progress-bar/index.tsx | 10 +- .../trigger-events-limit-modal/index.tsx | 2 +- .../components/billing/usage-info/index.tsx | 8 +- .../billing/vector-space-full/index.tsx | 4 +- .../components/chat-preview-card.tsx | 2 +- .../components/workflow-preview-card.tsx | 2 +- .../custom/custom-web-app-brand/index.tsx | 2 +- .../datasets/common/credential-icon.tsx | 4 +- .../common/document-picker/document-list.tsx | 2 +- .../datasets/common/document-picker/index.tsx | 6 +- .../preview-document-picker.tsx | 8 +- .../status-with-action.tsx | 4 +- .../datasets/common/image-list/index.tsx | 2 +- .../datasets/common/image-previewer/index.tsx | 2 +- .../image-uploader-in-chunk/image-input.tsx | 6 +- .../image-uploader-in-chunk/index.tsx | 2 +- .../index.tsx | 2 +- .../common/retrieval-param-config/index.tsx | 14 +- .../__tests__/index.spec.tsx | 4 +- .../create-from-dsl-modal/tab/item.tsx | 4 +- .../create-from-dsl-modal/uploader.tsx | 8 +- .../create-from-pipeline/list/create-card.tsx | 6 +- .../details/chunk-structure-card.tsx | 4 +- .../indexing-progress-item.tsx | 8 +- .../empty-dataset-creation-modal/index.tsx | 2 +- .../datasets/create/file-preview/index.tsx | 2 +- .../components/upload-dropzone.tsx | 4 +- .../datasets/create/file-uploader/index.tsx | 4 +- .../create/notion-page-preview/index.tsx | 2 +- .../components/data-source-type-selector.tsx | 2 +- .../datasets/create/step-one/index.tsx | 2 +- .../datasets/create/step-three/index.tsx | 10 +- .../components/indexing-mode-section.tsx | 2 +- .../step-two/components/option-card.tsx | 8 +- .../step-two/components/preview-panel.tsx | 2 +- .../datasets/create/step-two/index.tsx | 4 +- .../create/step-two/language-select/index.tsx | 4 +- .../datasets/create/stepper/step.tsx | 4 +- .../create/stop-embedding-modal/index.tsx | 2 +- .../datasets/create/top-bar/index.tsx | 6 +- .../website/base/checkbox-with-label.tsx | 2 +- .../website/base/crawled-result-item.tsx | 2 +- .../create/website/base/crawled-result.tsx | 2 +- .../create/website/base/error-message.tsx | 4 +- .../datasets/create/website/base/field.tsx | 2 +- .../datasets/create/website/base/header.tsx | 2 +- .../create/website/base/options-wrap.tsx | 6 +- .../create/website/firecrawl/options.tsx | 2 +- .../datasets/create/website/index.tsx | 16 +- .../create/website/jina-reader/options.tsx | 4 +- .../datasets/create/website/no-data.tsx | 2 +- .../datasets/create/website/preview.tsx | 4 +- .../create/website/watercrawl/options.tsx | 4 +- .../document-list/components/sort-header.tsx | 4 +- .../document-list/components/utils.tsx | 2 +- .../documents/components/operations.tsx | 2 +- .../data-source-options/datasource-icon.tsx | 2 +- .../data-source-options/option-card.tsx | 6 +- .../base/credential-selector/trigger.tsx | 4 +- .../local-file/components/file-list-item.tsx | 2 +- .../local-file/components/upload-dropzone.tsx | 4 +- .../online-drive/connect/index.tsx | 2 +- .../file-list/header/breadcrumbs/bucket.tsx | 4 +- .../file-list/header/breadcrumbs/drive.tsx | 2 +- .../header/breadcrumbs/dropdown/index.tsx | 2 +- .../file-list/header/breadcrumbs/item.tsx | 4 +- .../file-list/list/empty-folder.tsx | 2 +- .../file-list/list/empty-search-result.tsx | 2 +- .../online-drive/file-list/list/file-icon.tsx | 2 +- .../online-drive/file-list/list/index.tsx | 2 +- .../online-drive/file-list/list/item.tsx | 6 +- .../base/checkbox-with-label.tsx | 2 +- .../base/crawled-result-item.tsx | 2 +- .../website-crawl/base/crawled-result.tsx | 4 +- .../website-crawl/base/crawling.tsx | 6 +- .../website-crawl/base/error-message.tsx | 2 +- .../website-crawl/base/options/index.tsx | 2 +- .../processing/embedding-process/index.tsx | 2 +- .../create-from-pipeline/processing/index.tsx | 6 +- .../create-from-pipeline/step-indicator.tsx | 2 +- .../detail/batch-modal/csv-uploader.tsx | 2 +- .../detail/completed/child-segment-detail.tsx | 8 +- .../detail/completed/child-segment-list.tsx | 10 +- .../detail/completed/common/add-another.tsx | 2 +- .../detail/completed/common/batch-action.tsx | 6 +- .../detail/completed/common/chunk-content.tsx | 6 +- .../detail/completed/common/drawer.tsx | 2 +- .../completed/common/full-screen-drawer.tsx | 4 +- .../detail/completed/common/keywords.tsx | 2 +- .../completed/common/segment-index-tag.tsx | 2 +- .../detail/completed/common/summary-label.tsx | 4 +- .../detail/completed/common/summary-text.tsx | 4 +- .../documents/detail/completed/common/tag.tsx | 2 +- .../components/segment-list-content.tsx | 2 +- .../detail/completed/new-child-segment.tsx | 12 +- .../completed/segment-card/chunk-content.tsx | 6 +- .../detail/completed/segment-card/index.tsx | 4 +- .../detail/completed/segment-detail.tsx | 8 +- .../documents/detail/document-title.tsx | 2 +- .../embedding/components/progress-bar.tsx | 2 +- .../datasets/documents/detail/index.tsx | 8 +- .../metadata/components/doc-type-selector.tsx | 2 +- .../detail/metadata/components/field-info.tsx | 2 +- .../datasets/documents/detail/new-segment.tsx | 12 +- .../documents/detail/segment-add/index.tsx | 22 +- .../datasets/documents/status-item/index.tsx | 2 +- .../external-api/external-api-modal/Form.tsx | 4 +- .../external-api/external-api-panel/index.tsx | 2 +- .../create/RetrievalSettings.tsx | 2 +- .../datasets/extra-info/api-access/card.tsx | 4 +- .../datasets/extra-info/api-access/index.tsx | 6 +- .../datasets/extra-info/service-api/index.tsx | 4 +- .../formatted-text/flavours/edit-slice.tsx | 2 +- .../formatted-text/flavours/shared.tsx | 8 +- .../datasets/formatted-text/formatted.tsx | 2 +- .../components/chunk-detail-modal.tsx | 12 +- .../hit-testing/components/empty-records.tsx | 4 +- .../datasets/hit-testing/components/mask.tsx | 2 +- .../components/query-input/index.tsx | 4 +- .../components/query-input/textarea.tsx | 12 +- .../hit-testing/components/records.tsx | 8 +- .../components/result-item-external.tsx | 6 +- .../components/result-item-meta.tsx | 2 +- .../hit-testing/components/result-item.tsx | 4 +- .../datasets/hit-testing/components/score.tsx | 4 +- .../components/datasets/hit-testing/index.tsx | 8 +- .../components/dataset-card-footer.tsx | 4 +- .../components/dataset-card-header.tsx | 20 +- .../dataset-card/components/description.tsx | 4 +- .../components/operations-dropdown.tsx | 4 +- .../list/dataset-card/components/tag-area.tsx | 4 +- .../datasets/metadata/add-metadata-button.tsx | 2 +- .../datasets/metadata/base/date-picker.tsx | 4 +- .../metadata/edit-metadata-batch/add-row.tsx | 2 +- .../metadata/edit-metadata-batch/edit-row.tsx | 4 +- .../edit-metadata-batch/input-combined.tsx | 6 +- .../input-has-set-multiple-value.tsx | 8 +- .../metadata/edit-metadata-batch/label.tsx | 4 +- .../dataset-metadata-drawer.tsx | 4 +- .../metadata/metadata-document/index.tsx | 2 +- .../metadata/metadata-document/info-group.tsx | 8 +- .../components/datasets/preview/container.tsx | 6 +- .../components/datasets/preview/header.tsx | 4 +- .../datasets/rename-modal/index.tsx | 2 +- .../datasets/settings/index-method/index.tsx | 2 +- .../datasets/settings/option-card.tsx | 6 +- .../settings/permission-selector/index.tsx | 18 +- .../permission-selector/member-item.tsx | 8 +- web/app/components/develop/code.tsx | 12 +- web/app/components/develop/doc.tsx | 4 +- web/app/components/develop/md.tsx | 4 +- web/app/components/develop/tag.tsx | 4 +- web/app/components/develop/toc-panel.tsx | 10 +- web/app/components/explore/app-card/index.tsx | 2 +- web/app/components/explore/app-list/index.tsx | 2 +- .../components/explore/banner/banner-item.tsx | 18 +- .../explore/banner/indicator-button.tsx | 4 +- web/app/components/explore/category.tsx | 4 +- .../explore/item-operation/index.tsx | 4 +- .../explore/sidebar/app-nav-item/index.tsx | 6 +- web/app/components/explore/sidebar/index.tsx | 18 +- .../explore/sidebar/no-apps/index.tsx | 6 +- .../explore/try-app/__tests__/index.spec.tsx | 2 +- .../explore/try-app/app-info/index.tsx | 4 +- .../components/explore/try-app/app/chat.tsx | 4 +- .../explore/try-app/app/text-generation.tsx | 8 +- web/app/components/explore/try-app/index.tsx | 2 +- .../try-app/preview/flow-app-preview.tsx | 2 +- .../actions/__tests__/knowledge.spec.ts | 2 +- .../components/goto-anything/actions/app.tsx | 2 +- .../goto-anything/actions/knowledge.tsx | 2 +- .../header/account-dropdown/index.tsx | 16 +- .../account-dropdown/menu-item-content.tsx | 2 +- .../workplace-selector/index.tsx | 16 +- .../Integrations-page/index.tsx | 6 +- .../api-based-extension-page/empty.tsx | 6 +- .../header/account-setting/collapse/index.tsx | 4 +- .../data-source-page-new/card.tsx | 2 +- .../install-from-marketplace.tsx | 8 +- .../data-source-page-new/item.tsx | 2 +- .../header/account-setting/index.tsx | 2 +- .../edit-workspace-modal/index.tsx | 2 +- .../members-page/invite-modal/index.tsx | 2 +- .../invite-modal/role-selector.tsx | 10 +- .../members-page/operation/index.tsx | 18 +- .../operation/transfer-ownership.tsx | 8 +- .../member-selector.tsx | 14 +- .../header/account-setting/menu-dialog.tsx | 6 +- .../model-provider-page/index.tsx | 18 +- .../install-from-marketplace.tsx | 8 +- .../add-credential-in-load-balancing.tsx | 4 +- .../model-auth/add-custom-model.tsx | 2 +- .../model-auth/authorized/credential-item.tsx | 6 +- .../model-auth/authorized/index.tsx | 2 +- .../model-auth/config-model.tsx | 2 +- .../manage-custom-model-credentials.tsx | 2 +- .../switch-credential-in-load-balancing.tsx | 2 +- .../model-provider-page/model-badge/index.tsx | 4 +- .../model-provider-page/model-icon/index.tsx | 2 +- .../model-provider-page/model-modal/Form.tsx | 22 +- .../model-provider-page/model-name/index.tsx | 4 +- .../agent-model-trigger.tsx | 4 +- .../model-parameter-modal/index.tsx | 16 +- .../model-parameter-modal/parameter-item.tsx | 14 +- .../model-parameter-modal/trigger.tsx | 10 +- .../model-selector/feature-icon.tsx | 2 +- .../model-selector/model-selector-trigger.tsx | 8 +- .../model-selector/popup-item.tsx | 14 +- .../model-selector/popup.tsx | 6 +- .../provider-added-card/index.tsx | 18 +- .../model-auth-dropdown/api-key-section.tsx | 2 +- .../credits-exhausted-alert.tsx | 14 +- .../usage-priority-section.tsx | 10 +- .../provider-added-card/model-list-item.tsx | 6 +- .../model-load-balancing-configs.tsx | 6 +- .../model-load-balancing-modal.tsx | 2 +- .../provider-added-card/priority-selector.tsx | 2 +- .../provider-card-actions.tsx | 2 +- .../provider-added-card/quota-panel.tsx | 10 +- .../provider-added-card/system-quota-card.tsx | 2 +- .../provider-icon/index.tsx | 4 +- web/app/components/header/app-back/index.tsx | 6 +- .../components/header/app-selector/index.tsx | 12 +- .../components/header/explore-nav/index.tsx | 2 +- web/app/components/header/header-wrapper.tsx | 4 +- web/app/components/header/indicator/index.tsx | 2 +- web/app/components/header/nav/index.tsx | 4 +- .../header/nav/nav-selector/index.tsx | 14 +- .../components/header/plugins-nav/index.tsx | 6 +- web/app/components/header/tools-nav/index.tsx | 2 +- .../plugins/base/badges/icon-with-tooltip.tsx | 2 +- .../plugins/base/deprecation-notice.tsx | 6 +- .../plugins/base/key-value-item.tsx | 6 +- .../card/base/__tests__/placeholder.spec.tsx | 2 +- .../plugins/card/base/card-icon.tsx | 2 +- .../plugins/card/base/description.tsx | 2 +- .../components/plugins/card/base/org-info.tsx | 8 +- .../plugins/card/base/placeholder.tsx | 4 +- web/app/components/plugins/card/index.tsx | 6 +- .../install-plugin/base/loading-error.tsx | 8 +- .../install-plugin/install-bundle/index.tsx | 6 +- .../install-from-github/index.tsx | 8 +- .../install-from-local-package/index.tsx | 6 +- .../install-from-marketplace/index.tsx | 6 +- .../plugins/marketplace/empty/index.tsx | 16 +- .../plugins/marketplace/list/index.tsx | 2 +- .../marketplace/list/list-with-collection.tsx | 4 +- .../marketplace/plugin-type-switch.tsx | 4 +- .../search-box/__tests__/index.spec.tsx | 2 +- .../plugins/marketplace/search-box/index.tsx | 10 +- .../search-box/trigger/marketplace.tsx | 6 +- .../search-box/trigger/tool-selector.tsx | 10 +- .../sticky-search-and-switch-wrapper.tsx | 2 +- .../__tests__/add-oauth-button.spec.tsx | 2 +- .../authorize/add-oauth-button.tsx | 2 +- .../plugins/plugin-auth/authorize/index.tsx | 4 +- .../authorized-in-data-source-node.tsx | 2 +- .../plugin-auth/authorized-in-node.tsx | 2 +- .../plugins/plugin-auth/authorized/index.tsx | 2 +- .../plugins/plugin-auth/authorized/item.tsx | 4 +- .../plugin-auth/plugin-auth-in-agent.tsx | 2 +- .../plugins/plugin-auth/plugin-auth.tsx | 2 +- .../__tests__/detail-header.spec.tsx | 2 +- .../__tests__/endpoint-list.spec.tsx | 2 +- .../__tests__/index.spec.tsx | 2 +- .../__tests__/operation-dropdown.spec.tsx | 2 +- .../__tests__/strategy-detail.spec.tsx | 2 +- .../__tests__/strategy-item.spec.tsx | 2 +- .../__tests__/app-trigger.spec.tsx | 2 +- .../app-selector/app-inputs-panel.tsx | 4 +- .../app-selector/app-trigger.tsx | 6 +- .../detail-header/index.tsx | 2 +- .../plugin-detail-panel/endpoint-card.tsx | 2 +- .../plugin-detail-panel/endpoint-list.tsx | 10 +- .../plugin-detail-panel/endpoint-modal.tsx | 2 +- .../plugins/plugin-detail-panel/index.tsx | 4 +- .../model-selector/index.tsx | 4 +- .../model-selector/llm-params-panel.tsx | 4 +- .../multiple-tool-selector/index.tsx | 10 +- .../operation-dropdown.tsx | 2 +- .../plugin-detail-panel/strategy-detail.tsx | 18 +- .../plugin-detail-panel/strategy-item.tsx | 6 +- .../subscription-list/create/index.tsx | 2 +- .../subscription-list/list-view.tsx | 2 +- .../subscription-list/log-viewer.tsx | 10 +- .../subscription-list/selector-entry.tsx | 4 +- .../subscription-list/selector-view.tsx | 6 +- .../subscription-list/subscription-card.tsx | 6 +- .../__tests__/tool-credentials-form.spec.tsx | 2 +- .../components/reasoning-config-form.tsx | 12 +- .../components/tool-credentials-form.tsx | 2 +- .../tool-selector/components/tool-item.tsx | 2 +- .../tool-selector/components/tool-trigger.tsx | 6 +- .../tool-selector/index.tsx | 4 +- .../__tests__/event-detail-drawer.spec.tsx | 2 +- .../trigger/__tests__/event-list.spec.tsx | 2 +- .../trigger/event-detail-drawer.tsx | 18 +- .../trigger/event-list.tsx | 10 +- .../components/plugins/plugin-item/index.tsx | 18 +- .../__tests__/category-filter.spec.tsx | 2 +- .../filter-management/category-filter.tsx | 8 +- .../filter-management/tag-filter.tsx | 12 +- .../components/plugins/plugin-page/index.tsx | 2 +- .../plugin-page/install-plugin-dropdown.tsx | 2 +- .../components/task-status-indicator.tsx | 4 +- web/app/components/plugins/provider-card.tsx | 2 +- .../readme-panel/__tests__/entrance.spec.tsx | 2 +- .../plugins/readme-panel/entrance.tsx | 6 +- .../components/plugins/readme-panel/index.tsx | 6 +- .../auto-update-setting/index.tsx | 6 +- .../no-data-placeholder.tsx | 4 +- .../no-plugin-selected.tsx | 2 +- .../auto-update-setting/plugins-picker.tsx | 2 +- .../auto-update-setting/plugins-selected.tsx | 2 +- .../auto-update-setting/tool-picker.tsx | 2 +- .../plugins/reference-setting-modal/label.tsx | 4 +- .../update-plugin/from-market-place.tsx | 2 +- .../update-plugin/plugin-version-picker.tsx | 8 +- .../components/chunk-card-list/index.tsx | 2 +- .../panel/input-field/editor/index.tsx | 6 +- .../input-field/field-list/field-item.tsx | 8 +- .../field-list/field-list-container.tsx | 2 +- .../panel/input-field/field-list/index.tsx | 2 +- .../components/panel/input-field/index.tsx | 2 +- .../panel/input-field/preview/index.tsx | 2 +- .../data-source-options/option-card.tsx | 6 +- .../test-run/preparation/step-indicator.tsx | 2 +- .../panel/test-run/result/tabs/tab.tsx | 4 +- .../__tests__/run-mode.spec.tsx | 2 +- .../publisher/__tests__/popup.spec.tsx | 2 +- .../rag-pipeline-header/publisher/popup.tsx | 2 +- .../rag-pipeline-header/run-mode.tsx | 4 +- .../share/text-generation/index.tsx | 2 +- .../share/text-generation/info-modal.tsx | 6 +- .../share/text-generation/menu-dropdown.tsx | 10 +- .../run-batch/csv-reader/index.tsx | 4 +- .../share/text-generation/run-batch/index.tsx | 2 +- .../run-batch/res-download/index.tsx | 2 +- .../share/text-generation/run-once/index.tsx | 4 +- .../text-generation-result-panel.tsx | 14 +- .../text-generation-sidebar.tsx | 8 +- .../config-credentials.tsx | 2 +- .../edit-custom-collection-modal/index.tsx | 2 +- web/app/components/tools/labels/filter.tsx | 10 +- web/app/components/tools/labels/selector.tsx | 6 +- .../components/tools/mcp/detail/content.tsx | 2 +- .../tools/mcp/detail/list-loading.tsx | 2 +- .../tools/mcp/detail/operation-dropdown.tsx | 6 +- .../tools/mcp/detail/provider-detail.tsx | 4 +- .../components/tools/mcp/detail/tool-item.tsx | 12 +- .../components/tools/mcp/headers-input.tsx | 2 +- web/app/components/tools/mcp/index.tsx | 4 +- .../components/tools/mcp/mcp-server-modal.tsx | 2 +- .../components/tools/mcp/mcp-service-card.tsx | 2 +- web/app/components/tools/mcp/modal.tsx | 2 +- .../components/tools/mcp/provider-card.tsx | 2 +- .../mcp/sections/authentication-section.tsx | 4 +- web/app/components/tools/provider-list.tsx | 8 +- web/app/components/tools/provider/detail.tsx | 4 +- web/app/components/tools/provider/empty.tsx | 4 +- .../components/tools/provider/tool-item.tsx | 6 +- .../setting/build-in/config-credentials.tsx | 2 +- .../tools/workflow-tool/configure-button.tsx | 2 +- .../workflow-tool/confirm-modal/index.tsx | 2 +- .../components/tools/workflow-tool/index.tsx | 2 +- .../tools/workflow-tool/method-selector.tsx | 12 +- .../workflow-header/features-trigger.tsx | 2 +- .../start-node-selection-panel.tsx | 4 +- web/app/components/workflow/block-icon.tsx | 2 +- .../block-selector/all-start-blocks.tsx | 2 +- .../workflow/block-selector/all-tools.tsx | 2 +- .../workflow/block-selector/data-sources.tsx | 6 +- .../workflow/block-selector/index-bar.tsx | 4 +- .../market-place-plugin/action.tsx | 2 +- .../market-place-plugin/item.tsx | 12 +- .../market-place-plugin/list.tsx | 10 +- .../rag-tool-recommendations/list.tsx | 2 +- .../workflow/block-selector/tabs.tsx | 4 +- .../workflow/block-selector/tool-picker.tsx | 2 +- .../block-selector/tool/action-item.tsx | 8 +- .../workflow/block-selector/tool/tool.tsx | 6 +- .../workflow/block-selector/tools.tsx | 2 +- .../trigger-plugin/action-item.tsx | 8 +- .../block-selector/trigger-plugin/item.tsx | 6 +- .../block-selector/view-type-select.tsx | 4 +- web/app/components/workflow/custom-edge.tsx | 2 +- .../workflow/dsl-export-confirm-modal.tsx | 4 +- .../workflow/header/chat-variable-button.tsx | 2 +- .../workflow/header/checklist/index.tsx | 14 +- .../header/checklist/item-indicator.tsx | 2 +- .../workflow/header/checklist/node-group.tsx | 8 +- .../header/checklist/plugin-group.tsx | 2 +- .../components/workflow/header/env-button.tsx | 2 +- .../header/global-variable-button.tsx | 2 +- .../workflow/header/header-in-restoring.tsx | 2 +- .../workflow/header/run-and-history.tsx | 2 +- .../components/workflow/header/run-mode.tsx | 6 +- .../header/scroll-to-selected-node-button.tsx | 4 +- .../components/workflow/header/undo-redo.tsx | 6 +- .../header/version-history-button.tsx | 2 +- .../workflow/header/view-history.tsx | 14 +- .../workflow/header/view-workflow-history.tsx | 10 +- web/app/components/workflow/index.tsx | 2 +- .../nodes/_base/components/add-button.tsx | 2 +- .../components/agent-strategy-selector.tsx | 4 +- .../components/before-run-form/form-item.tsx | 10 +- .../_base/components/before-run-form/form.tsx | 2 +- .../components/before-run-form/index.tsx | 2 +- .../components/code-generator-button.tsx | 2 +- .../nodes/_base/components/collapse/index.tsx | 2 +- .../nodes/_base/components/editor/base.tsx | 4 +- .../code-editor/editor-support-vars.tsx | 2 +- .../components/editor/code-editor/index.tsx | 4 +- .../error-handle/error-handle-on-node.tsx | 4 +- .../error-handle/fail-branch-card.tsx | 6 +- .../workflow/nodes/_base/components/field.tsx | 6 +- .../nodes/_base/components/file-type-item.tsx | 8 +- .../_base/components/form-input-boolean.tsx | 10 +- .../components/form-input-item.sections.tsx | 6 +- .../_base/components/form-input-item.tsx | 2 +- .../components/form-input-type-switch.tsx | 4 +- .../workflow/nodes/_base/components/group.tsx | 4 +- .../components/input-support-select-var.tsx | 4 +- .../components/install-plugin-button.tsx | 2 +- .../nodes/_base/components/layout/box.tsx | 2 +- .../_base/components/layout/field-title.tsx | 6 +- .../nodes/_base/components/layout/group.tsx | 2 +- .../components/list-no-data-placeholder.tsx | 2 +- .../nodes/_base/components/memory-config.tsx | 6 +- .../mixed-variable-text-input/index.tsx | 2 +- .../_base/components/next-step/container.tsx | 6 +- .../nodes/_base/components/next-step/item.tsx | 2 +- .../nodes/_base/components/node-handle.tsx | 8 +- .../nodes/_base/components/node-resizer.tsx | 4 +- .../_base/components/node-status-icon.tsx | 2 +- .../nodes/_base/components/option-card.tsx | 6 +- .../nodes/_base/components/output-vars.tsx | 6 +- .../nodes/_base/components/prompt/editor.tsx | 12 +- .../readonly-input-with-select-var.tsx | 4 +- .../_base/components/retry/retry-on-node.tsx | 4 +- .../nodes/_base/components/selector.tsx | 8 +- .../nodes/_base/components/setting-item.tsx | 8 +- .../workflow/nodes/_base/components/split.tsx | 2 +- .../components/support-var-input/index.tsx | 2 +- .../components/switch-plugin-version.tsx | 2 +- .../object-child-tree-panel/picker/field.tsx | 6 +- .../object-child-tree-panel/picker/index.tsx | 6 +- .../object-child-tree-panel/show/field.tsx | 14 +- .../tree-indent-line.tsx | 4 +- .../_base/components/variable/var-list.tsx | 4 +- .../variable/var-reference-picker.trigger.tsx | 4 +- .../variable/var-reference-picker.tsx | 2 +- .../variable/var-reference-vars.tsx | 24 +- .../components/variable/var-type-picker.tsx | 4 +- .../variable-label/base/variable-icon.tsx | 2 +- .../variable-label/base/variable-label.tsx | 6 +- .../variable-label/base/variable-name.tsx | 4 +- .../variable-icon-with-color.tsx | 2 +- .../variable-label-in-editor.tsx | 2 +- .../variable-label/variable-label-in-node.tsx | 2 +- .../variable-label/variable-label-in-text.tsx | 2 +- .../_base/components/workflow-panel/index.tsx | 20 +- .../workflow-panel/trigger-subscription.tsx | 2 +- .../components/workflow/nodes/_base/node.tsx | 10 +- .../nodes/agent/components/tool-icon.tsx | 6 +- .../components/operation-selector.tsx | 10 +- .../nodes/data-source-empty/index.tsx | 2 +- .../nodes/http/components/api-input.tsx | 6 +- .../http/components/authorization/index.tsx | 2 +- .../components/authorization/radio-group.tsx | 6 +- .../nodes/http/components/edit-body/index.tsx | 4 +- .../key-value/key-value-edit/index.tsx | 6 +- .../key-value/key-value-edit/input-item.tsx | 4 +- .../key-value/key-value-edit/item.tsx | 4 +- .../components/workflow/nodes/http/panel.tsx | 6 +- .../components/button-style-dropdown.tsx | 2 +- .../delivery-method/email-configure-modal.tsx | 4 +- .../components/delivery-method/index.tsx | 2 +- .../delivery-method/mail-body-input.tsx | 2 +- .../delivery-method/method-item.tsx | 6 +- .../delivery-method/method-selector.tsx | 46 +- .../delivery-method/recipient/email-input.tsx | 4 +- .../delivery-method/recipient/email-item.tsx | 4 +- .../delivery-method/recipient/index.tsx | 12 +- .../delivery-method/recipient/member-list.tsx | 8 +- .../recipient/member-selector.tsx | 2 +- .../delivery-method/test-email-sender.tsx | 2 +- .../delivery-method/upgrade-modal.tsx | 2 +- .../human-input/components/form-content.tsx | 10 +- .../nodes/human-input/components/timeout.tsx | 8 +- .../components/variable-in-markdown.tsx | 2 +- .../workflow/nodes/human-input/node.tsx | 14 +- .../workflow/nodes/human-input/panel.tsx | 4 +- .../condition-list/condition-item.tsx | 10 +- .../condition-list/condition-operator.tsx | 2 +- .../components/condition-list/index.tsx | 10 +- .../components/condition-number-input.tsx | 2 +- .../if-else/components/condition-wrap.tsx | 4 +- .../workflow/nodes/iteration/add-block.tsx | 8 +- .../workflow/nodes/iteration/node.tsx | 2 +- .../components/chunk-structure/hooks.tsx | 2 +- .../chunk-structure/instruction/index.tsx | 10 +- .../components/index-method.tsx | 4 +- .../knowledge-base/components/option-card.tsx | 10 +- .../search-method-option.tsx | 8 +- .../workflow/nodes/knowledge-base/node.tsx | 8 +- .../condition-list/condition-date.tsx | 6 +- .../condition-list/condition-item.tsx | 8 +- .../condition-list/condition-operator.tsx | 2 +- .../condition-list/condition-value-method.tsx | 2 +- .../metadata/condition-list/index.tsx | 10 +- .../components/metadata/metadata-icon.tsx | 2 +- .../components/retrieval-config.tsx | 2 +- .../components/extract-input.tsx | 4 +- .../components/filter-condition.tsx | 2 +- .../list-operator/components/limit-config.tsx | 2 +- .../components/sub-variable-picker.tsx | 6 +- .../nodes/llm/components/config-prompt.tsx | 4 +- .../json-schema-config-modal/code-editor.tsx | 6 +- .../error-message.tsx | 4 +- .../json-importer.tsx | 2 +- .../json-schema-config.tsx | 2 +- .../json-schema-generator/index.tsx | 2 +- .../schema-editor.tsx | 4 +- .../edit-card/auto-width-input.tsx | 6 +- .../visual-editor/edit-card/index.tsx | 8 +- .../visual-editor/edit-card/type-selector.tsx | 4 +- .../visual-editor/index.tsx | 2 +- .../visual-editor/schema-node.tsx | 2 +- .../llm/components/prompt-generator-btn.tsx | 2 +- .../nodes/llm/components/structure-output.tsx | 4 +- .../workflow/nodes/loop/add-block.tsx | 10 +- .../condition-list/condition-item.tsx | 8 +- .../condition-list/condition-operator.tsx | 2 +- .../loop/components/condition-list/index.tsx | 10 +- .../components/condition-number-input.tsx | 2 +- .../nodes/loop/components/condition-wrap.tsx | 4 +- .../loop/components/loop-variables/empty.tsx | 2 +- .../components/loop-variables/form-item.tsx | 2 +- .../workflow/nodes/loop/insert-block.tsx | 4 +- .../components/workflow/nodes/loop/node.tsx | 2 +- .../extract-parameter/import-from-tool.tsx | 2 +- .../components/class-list.tsx | 8 +- .../nodes/start/components/var-item.tsx | 2 +- .../nodes/start/components/var-list.tsx | 6 +- .../nodes/tool/components/input-var-list.tsx | 2 +- .../mixed-variable-text-input/index.tsx | 2 +- .../mixed-variable-text-input/placeholder.tsx | 6 +- .../components/workflow/nodes/tool/node.tsx | 8 +- .../components/generic-table.tsx | 16 +- .../components/paragraph-input.tsx | 8 +- .../components/add-variable/index.tsx | 2 +- .../components/node-group-item.tsx | 8 +- .../components/node-variable-item.tsx | 4 +- .../nodes/variable-assigner/panel.tsx | 2 +- .../components/workflow/note-node/index.tsx | 4 +- .../plugins/link-editor-plugin/component.tsx | 2 +- .../note-editor/toolbar/color-picker.tsx | 6 +- .../note-node/note-editor/toolbar/command.tsx | 2 +- .../toolbar/font-size-selector.tsx | 6 +- .../note-editor/toolbar/operator.tsx | 2 +- .../workflow/operator/add-block.tsx | 2 +- .../components/workflow/operator/control.tsx | 2 +- .../workflow/operator/more-actions.tsx | 14 +- .../workflow/operator/zoom-in-out.tsx | 8 +- .../components/workflow/panel-contextmenu.tsx | 2 +- .../components/array-bool-list.tsx | 2 +- .../components/variable-item.tsx | 10 +- .../components/variable-modal.sections.tsx | 2 +- .../components/variable-modal.tsx | 2 +- .../components/variable-type-select.tsx | 12 +- .../panel/chat-variable-panel/index.tsx | 26 +- .../conversation-variable-modal.tsx | 18 +- .../panel/debug-and-preview/index.tsx | 8 +- .../panel/debug-and-preview/user-input.tsx | 4 +- .../workflow/panel/env-panel/env-item.tsx | 14 +- .../workflow/panel/env-panel/index.tsx | 8 +- .../panel/env-panel/variable-modal.tsx | 8 +- .../panel/global-variable-panel/index.tsx | 8 +- .../panel/global-variable-panel/item.tsx | 8 +- web/app/components/workflow/panel/index.tsx | 4 +- .../context-menu/menu-item.tsx | 6 +- .../version-history-panel/filter/index.tsx | 4 +- .../version-history-panel/loading/item.tsx | 6 +- .../version-history-item.tsx | 16 +- .../workflow/panel/workflow-preview.tsx | 2 +- .../workflow/run/agent-log/agent-log-item.tsx | 4 +- .../run/agent-log/agent-log-trigger.tsx | 10 +- web/app/components/workflow/run/index.tsx | 8 +- .../iteration-log/iteration-result-panel.tsx | 6 +- .../run/loop-log/loop-result-panel.tsx | 4 +- .../workflow/run/loop-result-panel.tsx | 10 +- web/app/components/workflow/run/node.tsx | 16 +- .../workflow/run/status-container.tsx | 6 +- web/app/components/workflow/run/status.tsx | 18 +- .../components/workflow/run/tracing-panel.tsx | 6 +- .../components/workflow/shortcuts-name.tsx | 4 +- .../components/workflow/simple-node/index.tsx | 6 +- .../variable-inspect/display-content.tsx | 10 +- .../workflow/variable-inspect/empty.tsx | 4 +- .../workflow/variable-inspect/group.tsx | 2 +- .../workflow/variable-inspect/index.tsx | 2 +- .../variable-inspect/large-data-alert.tsx | 6 +- .../workflow/variable-inspect/left.tsx | 2 +- .../workflow/variable-inspect/listening.tsx | 2 +- .../workflow/variable-inspect/panel.tsx | 10 +- .../workflow/variable-inspect/right.tsx | 12 +- .../workflow/variable-inspect/trigger.tsx | 10 +- .../value-content-sections.tsx | 4 +- .../variable-inspect/value-content.tsx | 2 +- .../components/error-handle-on-node.tsx | 4 +- .../components/node-handle.tsx | 6 +- .../components/nodes/base.tsx | 12 +- .../components/nodes/iteration/node.tsx | 2 +- .../components/nodes/loop/node.tsx | 2 +- .../components/note-node/index.tsx | 2 +- .../components/zoom-in-out.tsx | 6 +- .../workflow/workflow-preview/index.tsx | 2 +- web/app/education-apply/role-selector.tsx | 6 +- .../forgot-password/ChangePasswordForm.tsx | 6 +- web/app/forgot-password/page.tsx | 2 +- web/app/init/page.tsx | 2 +- web/app/install/installForm.tsx | 6 +- web/app/install/page.tsx | 2 +- web/app/reset-password/layout.tsx | 6 +- web/app/reset-password/set-password/page.tsx | 2 +- web/app/signin/components/social-auth.tsx | 2 +- web/app/signin/layout.tsx | 6 +- web/app/signin/normal-form.tsx | 2 +- web/app/signin/split.tsx | 2 +- web/app/signup/layout.tsx | 6 +- web/app/signup/set-password/page.tsx | 2 +- web/app/styles/globals.css | 849 +------ web/app/styles/markdown.css | 3 +- web/docs/test.md | 2 - web/eslint-suppressions.json | 2200 ----------------- web/package.json | 3 +- web/scripts/gen-icons.mjs | 2 +- web/tailwind-common-config.ts | 86 +- web/themes/markdown-light.css | 44 - .../{markdown-dark.css => markdown.css} | 1 + 990 files changed, 3065 insertions(+), 5786 deletions(-) create mode 100644 packages/dify-ui/AGENTS.md create mode 100644 packages/dify-ui/package.json rename web/utils/classnames.ts => packages/dify-ui/src/cn.ts (100%) create mode 100644 packages/dify-ui/src/styles/styles.css create mode 100644 packages/dify-ui/src/styles/utilities.css create mode 100644 packages/dify-ui/src/tailwind-preset.ts rename {web => packages/dify-ui/src}/themes/dark.css (100%) rename {web => packages/dify-ui/src}/themes/light.css (100%) rename {web => packages/dify-ui/src}/themes/tailwind-theme-var-define.ts (100%) create mode 100644 packages/dify-ui/tsconfig.json delete mode 100644 web/app/components/base/modal/index.css delete mode 100644 web/themes/markdown-light.css rename web/themes/{markdown-dark.css => markdown.css} (98%) diff --git a/packages/dify-ui/AGENTS.md b/packages/dify-ui/AGENTS.md new file mode 100644 index 0000000000..6875f8b4e9 --- /dev/null +++ b/packages/dify-ui/AGENTS.md @@ -0,0 +1,27 @@ +# @langgenius/dify-ui + +This package provides shared design tokens (colors, shadows, typography), the `cn()` utility, and a Tailwind CSS preset consumed by `web/`. + +## Border Radius: Figma Token → Tailwind Class Mapping + +The Figma design system uses `--radius/*` tokens whose scale is **offset by one step** from Tailwind CSS v4 defaults. When translating Figma specs to code, always use this mapping — never use `radius-*` as a CSS class, and never extend `borderRadius` in the preset. + +| Figma Token | Value | Tailwind Class | +|---|---|---| +| `--radius/2xs` | 2px | `rounded-xs` | +| `--radius/xs` | 4px | `rounded-sm` | +| `--radius/sm` | 6px | `rounded-md` | +| `--radius/md` | 8px | `rounded-lg` | +| `--radius/lg` | 10px | `rounded-[10px]` | +| `--radius/xl` | 12px | `rounded-xl` | +| `--radius/2xl` | 16px | `rounded-2xl` | +| `--radius/3xl` | 20px | `rounded-[20px]` | +| `--radius/6xl` | 28px | `rounded-[28px]` | +| `--radius/full` | 999px | `rounded-full` | + +### 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** 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/package.json b/packages/dify-ui/package.json new file mode 100644 index 0000000000..d8314a6be3 --- /dev/null +++ b/packages/dify-ui/package.json @@ -0,0 +1,24 @@ +{ + "name": "@langgenius/dify-ui", + "version": "0.0.1", + "private": true, + "type": "module", + "exports": { + "./styles.css": "./src/styles/styles.css", + "./tailwind-preset": { + "import": "./src/tailwind-preset.ts", + "types": "./src/tailwind-preset.ts" + }, + "./cn": { + "import": "./src/cn.ts", + "types": "./src/cn.ts" + } + }, + "dependencies": { + "clsx": "catalog:", + "tailwind-merge": "catalog:" + }, + "devDependencies": { + "tailwindcss": "catalog:" + } +} diff --git a/web/utils/classnames.ts b/packages/dify-ui/src/cn.ts similarity index 100% rename from web/utils/classnames.ts rename to packages/dify-ui/src/cn.ts diff --git a/packages/dify-ui/src/styles/styles.css b/packages/dify-ui/src/styles/styles.css new file mode 100644 index 0000000000..a712e9c5db --- /dev/null +++ b/packages/dify-ui/src/styles/styles.css @@ -0,0 +1,3 @@ +@import '../themes/light.css' layer(base); +@import '../themes/dark.css' layer(base); +@import './utilities.css'; diff --git a/packages/dify-ui/src/styles/utilities.css b/packages/dify-ui/src/styles/utilities.css new file mode 100644 index 0000000000..69b15d4c10 --- /dev/null +++ b/packages/dify-ui/src/styles/utilities.css @@ -0,0 +1,272 @@ +@utility system-kbd { + font-size: 12px; + font-weight: 500; + line-height: 16px; +} + +@utility system-2xs-regular-uppercase { + font-size: 10px; + font-weight: 400; + text-transform: uppercase; + line-height: 12px; +} + +@utility system-2xs-regular { + font-size: 10px; + font-weight: 400; + line-height: 12px; +} + +@utility system-2xs-medium { + font-size: 10px; + font-weight: 500; + line-height: 12px; +} + +@utility system-2xs-medium-uppercase { + font-size: 10px; + font-weight: 500; + text-transform: uppercase; + line-height: 12px; +} + +@utility system-2xs-semibold-uppercase { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + line-height: 12px; +} + +@utility system-xs-regular { + font-size: 12px; + font-weight: 400; + line-height: 16px; +} + +@utility system-xs-regular-uppercase { + font-size: 12px; + font-weight: 400; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-xs-medium { + font-size: 12px; + font-weight: 500; + line-height: 16px; +} + +@utility system-xs-medium-uppercase { + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-xs-semibold { + font-size: 12px; + font-weight: 600; + line-height: 16px; +} + +@utility system-xs-semibold-uppercase { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-sm-regular { + font-size: 13px; + font-weight: 400; + line-height: 16px; +} + +@utility system-sm-medium { + font-size: 13px; + font-weight: 500; + line-height: 16px; +} + +@utility system-sm-medium-uppercase { + font-size: 13px; + font-weight: 500; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-sm-semibold { + font-size: 13px; + font-weight: 600; + line-height: 16px; +} + +@utility system-sm-semibold-uppercase { + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + line-height: 16px; +} + +@utility system-md-regular { + font-size: 14px; + font-weight: 400; + line-height: 20px; +} + +@utility system-md-medium { + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +@utility system-md-semibold { + font-size: 14px; + font-weight: 600; + line-height: 20px; +} + +@utility system-md-semibold-uppercase { + font-size: 14px; + font-weight: 600; + text-transform: uppercase; + line-height: 20px; +} + +@utility system-xl-medium { + font-size: 16px; + font-weight: 500; + line-height: 24px; +} + +@utility system-xl-semibold { + font-size: 16px; + font-weight: 600; + line-height: 24px; +} + +@utility code-xs-regular { + font-size: 12px; + font-weight: 400; + line-height: 1.5; +} + +@utility code-sm-regular { + font-size: 13px; + font-weight: 400; + line-height: 1.5; +} + +@utility code-sm-semibold { + font-size: 13px; + font-weight: 600; + line-height: 1.5; +} + +@utility body-xs-regular { + font-size: 12px; + font-weight: 400; + line-height: 16px; +} + +@utility body-xs-medium { + font-size: 12px; + font-weight: 500; + line-height: 16px; +} + +@utility body-sm-regular { + font-size: 13px; + font-weight: 400; + line-height: 16px; +} + +@utility body-sm-medium { + font-size: 13px; + font-weight: 500; + line-height: 16px; +} + +@utility body-md-regular { + font-size: 14px; + font-weight: 400; + line-height: 20px; +} + +@utility body-md-medium { + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +@utility body-lg-regular { + font-size: 15px; + font-weight: 400; + line-height: 20px; +} + +@utility body-2xl-regular { + font-size: 18px; + font-weight: 400; + line-height: 1.5; +} + +@utility title-xs-semi-bold { + font-size: 12px; + font-weight: 600; + line-height: 16px; +} + +@utility title-sm-semi-bold { + font-size: 13px; + font-weight: 600; + line-height: 16px; +} + +@utility title-md-semi-bold { + font-size: 14px; + font-weight: 600; + line-height: 20px; +} + +@utility title-lg-bold { + font-size: 15px; + font-weight: 700; + line-height: 1.2; +} + +@utility title-xl-semi-bold { + font-size: 16px; + font-weight: 600; + line-height: 1.2; +} + +@utility title-2xl-semi-bold { + font-size: 18px; + font-weight: 600; + line-height: 1.2; +} + +@utility title-3xl-semi-bold { + font-size: 20px; + font-weight: 600; + line-height: 1.2; +} + +@utility title-3xl-bold { + font-size: 20px; + font-weight: 700; + line-height: 1.2; +} + +@utility title-4xl-semi-bold { + font-size: 24px; + font-weight: 600; + line-height: 1.2; +} + +@utility title-5xl-bold { + font-size: 30px; + font-weight: 700; + line-height: 1.2; +} diff --git a/packages/dify-ui/src/tailwind-preset.ts b/packages/dify-ui/src/tailwind-preset.ts new file mode 100644 index 0000000000..2dbf4781b0 --- /dev/null +++ b/packages/dify-ui/src/tailwind-preset.ts @@ -0,0 +1,87 @@ +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/web/themes/dark.css b/packages/dify-ui/src/themes/dark.css similarity index 100% rename from web/themes/dark.css rename to packages/dify-ui/src/themes/dark.css diff --git a/web/themes/light.css b/packages/dify-ui/src/themes/light.css similarity index 100% rename from web/themes/light.css rename to packages/dify-ui/src/themes/light.css diff --git a/web/themes/tailwind-theme-var-define.ts b/packages/dify-ui/src/themes/tailwind-theme-var-define.ts similarity index 100% rename from web/themes/tailwind-theme-var-define.ts rename to packages/dify-ui/src/themes/tailwind-theme-var-define.ts diff --git a/packages/dify-ui/tsconfig.json b/packages/dify-ui/tsconfig.json new file mode 100644 index 0000000000..3e912baba0 --- /dev/null +++ b/packages/dify-ui/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "isolatedModules": true, + "verbatimModuleSyntax": true + }, + "include": ["src"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4444981601..094faf78cb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -599,6 +599,19 @@ importers: specifier: 'catalog:' version: 0.1.16(@types/node@25.6.0)(@voidzero-dev/vite-plus-core@0.1.16(@types/node@25.6.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + packages/dify-ui: + dependencies: + clsx: + specifier: 'catalog:' + version: 2.1.1 + tailwind-merge: + specifier: 'catalog:' + version: 3.5.0 + devDependencies: + tailwindcss: + specifier: 'catalog:' + version: 4.2.2 + packages/iconify-collections: devDependencies: iconify-import-svg: @@ -739,9 +752,6 @@ importers: client-only: specifier: 'catalog:' version: 0.0.1 - clsx: - specifier: 'catalog:' - version: 2.1.1 cmdk: specifier: 'catalog:' version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -922,9 +932,6 @@ importers: string-ts: specifier: 'catalog:' version: 2.3.1 - tailwind-merge: - specifier: 'catalog:' - version: 3.5.0 tldts: specifier: 'catalog:' version: 7.0.28 @@ -971,6 +978,9 @@ importers: '@iconify-json/ri': specifier: 'catalog:' version: 1.2.10 + '@langgenius/dify-ui': + specifier: workspace:* + version: link:../packages/dify-ui '@mdx-js/loader': specifier: 'catalog:' version: 3.1.1(webpack@5.105.4(uglify-js@3.19.3)) @@ -1126,7 +1136,7 @@ importers: version: 4.12.12 knip: specifier: 'catalog:' - version: 6.4.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + version: 6.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) postcss: specifier: 'catalog:' version: 8.5.9 @@ -1553,14 +1563,17 @@ packages: peerDependencies: tailwindcss: '*' - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@emoji-mart/data@1.2.1': resolution: {integrity: sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==} @@ -8007,6 +8020,10 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} @@ -9153,9 +9170,9 @@ snapshots: '@iconify/utils': 3.1.0 tailwindcss: 4.2.2 - '@emnapi/core@1.9.1': + '@emnapi/core@1.9.2': dependencies: - '@emnapi/wasi-threads': 1.2.0 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true @@ -9164,7 +9181,12 @@ snapshots: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.0': + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true @@ -9944,10 +9966,10 @@ snapshots: react: 19.2.5 react-dom: 19.2.5(react@19.2.5) - '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 '@tybys/wasm-util': 0.10.1 optional: true @@ -10120,9 +10142,9 @@ snapshots: '@oxc-parser/binding-openharmony-arm64@0.121.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@oxc-parser/binding-wasm32-wasi@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -10191,9 +10213,9 @@ snapshots: '@oxc-resolver/binding-openharmony-arm64@11.19.1': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -11758,7 +11780,7 @@ snapshots: debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 semver: 7.7.4 - tinyglobby: 0.2.15 + tinyglobby: 0.2.16 ts-api-utils: 2.5.0(typescript@6.0.2) typescript: 6.0.2 transitivePeerDependencies: @@ -14140,7 +14162,7 @@ snapshots: khroma@2.1.0: {} - knip@6.4.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + knip@6.4.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@nodelib/fs.walk': 1.2.8 fast-glob: 3.3.3 @@ -14148,8 +14170,8 @@ snapshots: get-tsconfig: 4.13.7 jiti: 2.6.1 minimist: 1.2.8 - oxc-parser: 0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) - oxc-resolver: 11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + oxc-parser: 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) picocolors: 1.1.1 picomatch: 4.0.4 smol-toml: 1.6.1 @@ -15056,7 +15078,7 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 - oxc-parser@0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + oxc-parser@0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): dependencies: '@oxc-project/types': 0.121.0 optionalDependencies: @@ -15076,7 +15098,7 @@ snapshots: '@oxc-parser/binding-linux-x64-gnu': 0.121.0 '@oxc-parser/binding-linux-x64-musl': 0.121.0 '@oxc-parser/binding-openharmony-arm64': 0.121.0 - '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@oxc-parser/binding-wasm32-wasi': 0.121.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@oxc-parser/binding-win32-arm64-msvc': 0.121.0 '@oxc-parser/binding-win32-ia32-msvc': 0.121.0 '@oxc-parser/binding-win32-x64-msvc': 0.121.0 @@ -15084,7 +15106,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - oxc-resolver@11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1): + oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.19.1 '@oxc-resolver/binding-android-arm64': 11.19.1 @@ -15102,7 +15124,7 @@ snapshots: '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 '@oxc-resolver/binding-linux-x64-musl': 11.19.1 '@oxc-resolver/binding-openharmony-arm64': 11.19.1 - '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 @@ -16241,6 +16263,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + tinypool@2.1.0: {} tinyrainbow@2.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 98f1fcfa86..cd72c6bc0e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,57 +1,22 @@ -catalogMode: prefer -trustPolicy: no-downgrade -trustPolicyExclude: - - chokidar@4.0.3 - - reselect@5.1.1 - - semver@6.3.1 -blockExoticSubdeps: true -strictDepBuilds: true -allowBuilds: - "@parcel/watcher": false - canvas: false - esbuild: false - sharp: false packages: - web - e2e - sdks/nodejs-client - packages/* -overrides: - "@lexical/code": npm:lexical-code-no-prism@0.41.0 - "@monaco-editor/loader": 1.7.0 - brace-expansion@>=2.0.0 <2.0.3: 2.0.3 - canvas: ^3.2.2 - dompurify@>=3.1.3 <=3.3.1: 3.3.2 - esbuild@<0.27.2: 0.27.2 - flatted@<=3.4.1: 3.4.2 - glob@>=10.2.0 <10.5.0: 11.1.0 - is-core-module: npm:@nolyfill/is-core-module@^1.0.39 - lodash@>=4.0.0 <= 4.17.23: 4.18.0 - lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 - picomatch@<2.3.2: 2.3.2 - picomatch@>=4.0.0 <4.0.4: 4.0.4 - rollup@>=4.0.0 <4.59.0: 4.59.0 - safe-buffer: ^5.2.1 - safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 - side-channel: npm:@nolyfill/side-channel@^1.0.44 - smol-toml@<1.6.1: 1.6.1 - solid-js: 1.9.11 - string-width: ~8.2.0 - svgo@>=3.0.0 <3.3.3: 3.3.3 - tar@<=7.5.10: 7.5.11 - undici@>=7.0.0 <7.24.0: 7.24.0 - vite: npm:@voidzero-dev/vite-plus-core@0.1.16 - vitest: npm:@voidzero-dev/vite-plus-test@0.1.16 - yaml@>=2.0.0 <2.8.3: 2.8.3 - yauzl@<3.2.1: 3.2.1 +allowBuilds: + "@parcel/watcher": false + canvas: false + esbuild: false + sharp: false +blockExoticSubdeps: true catalog: "@amplitude/analytics-browser": 2.39.0 "@amplitude/plugin-session-replay-browser": 1.27.7 "@antfu/eslint-config": 8.2.0 "@base-ui/react": 1.4.0 - "@date-fns/tz": 1.4.1 "@chromatic-com/storybook": 5.1.2 "@cucumber/cucumber": 12.8.0 + "@date-fns/tz": 1.4.1 "@egoist/tailwindcss-icons": 1.9.2 "@emoji-mart/data": 1.2.1 "@eslint-react/eslint-plugin": 3.0.0 @@ -107,6 +72,7 @@ catalog: "@testing-library/jest-dom": 6.9.1 "@testing-library/react": 16.3.2 "@testing-library/user-event": 14.6.1 + '@tsdown/css': 0.21.8 "@tsslint/cli": 3.0.3 "@tsslint/compat-eslint": 3.0.3 "@tsslint/config": 3.0.3 @@ -230,3 +196,38 @@ catalog: zod: 4.3.6 zundo: 2.3.0 zustand: 5.0.12 +catalogMode: prefer +overrides: + "@lexical/code": npm:lexical-code-no-prism@0.41.0 + "@monaco-editor/loader": 1.7.0 + brace-expansion@>=2.0.0 <2.0.3: 2.0.3 + canvas: ^3.2.2 + dompurify@>=3.1.3 <=3.3.1: 3.3.2 + esbuild@<0.27.2: 0.27.2 + flatted@<=3.4.1: 3.4.2 + glob@>=10.2.0 <10.5.0: 11.1.0 + is-core-module: npm:@nolyfill/is-core-module@^1.0.39 + lodash-es@>=4.0.0 <= 4.17.23: 4.18.0 + lodash@>=4.0.0 <= 4.17.23: 4.18.0 + picomatch@<2.3.2: 2.3.2 + picomatch@>=4.0.0 <4.0.4: 4.0.4 + rollup@>=4.0.0 <4.59.0: 4.59.0 + safe-buffer: ^5.2.1 + safer-buffer: npm:@nolyfill/safer-buffer@^1.0.44 + side-channel: npm:@nolyfill/side-channel@^1.0.44 + smol-toml@<1.6.1: 1.6.1 + solid-js: 1.9.11 + string-width: ~8.2.0 + svgo@>=3.0.0 <3.3.3: 3.3.3 + tar@<=7.5.10: 7.5.11 + undici@>=7.0.0 <7.24.0: 7.24.0 + vite: npm:@voidzero-dev/vite-plus-core@0.1.16 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.16 + yaml@>=2.0.0 <2.8.3: 2.8.3 + yauzl@<3.2.1: 3.2.1 +strictDepBuilds: true +trustPolicy: no-downgrade +trustPolicyExclude: + - chokidar@4.0.3 + - reselect@5.1.1 + - semver@6.3.1 diff --git a/web/AGENTS.md b/web/AGENTS.md index 97f74441a7..4a705bf4b8 100644 --- a/web/AGENTS.md +++ b/web/AGENTS.md @@ -12,6 +12,10 @@ - `frontend-query-mutation` is the source of truth for Dify frontend contracts, query and mutation call-site patterns, conditional queries, invalidation, and mutation error handling. +## Design Token Mapping + +- When translating Figma designs to code, read `../packages/dify-ui/AGENTS.md` for the Figma `--radius/*` token to Tailwind `rounded-*` class mapping. The two scales are offset by one step. + ## Automated Test Generation - Use `./docs/test.md` as the canonical instruction set for generating frontend automated tests. diff --git a/web/README.md b/web/README.md index 2d69a94dbd..382c804264 100644 --- a/web/README.md +++ b/web/README.md @@ -136,7 +136,6 @@ pnpm -C web test If you are not familiar with writing tests, refer to: -- [classnames.spec.ts] - Utility function test example - [index.spec.tsx] - Component test example ### Analyze Component Complexity @@ -166,7 +165,6 @@ The Dify community can be found on [Discord community], where you can ask questi [Storybook]: https://storybook.js.org [Vite+]: https://viteplus.dev [Vitest]: https://vitest.dev -[classnames.spec.ts]: ./utils/classnames.spec.ts [index.spec.tsx]: ./app/components/base/button/index.spec.tsx [pnpm]: https://pnpm.io [vinext]: https://github.com/cloudflare/vinext diff --git a/web/__tests__/plugins/plugin-auth-flow.test.tsx b/web/__tests__/plugins/plugin-auth-flow.test.tsx index a2ec8703ca..c4d28e3f34 100644 --- a/web/__tests__/plugins/plugin-auth-flow.test.tsx +++ b/web/__tests__/plugins/plugin-auth-flow.test.tsx @@ -31,7 +31,7 @@ vi.mock('@/context/app-context', () => ({ }), })) -vi.mock('@/utils/classnames', () => ({ +vi.mock('@langgenius/dify-ui/cn', () => ({ cn: (...args: unknown[]) => args.filter(Boolean).join(' '), })) diff --git a/web/__tests__/plugins/plugin-card-rendering.test.tsx b/web/__tests__/plugins/plugin-card-rendering.test.tsx index 5bd7f0c8bf..fb0163f344 100644 --- a/web/__tests__/plugins/plugin-card-rendering.test.tsx +++ b/web/__tests__/plugins/plugin-card-rendering.test.tsx @@ -32,7 +32,7 @@ vi.mock('@/types/app', async () => { return vi.importActual('@/types/app') }) -vi.mock('@/utils/classnames', () => ({ +vi.mock('@langgenius/dify-ui/cn', () => ({ cn: (...args: unknown[]) => args.filter(a => typeof a === 'string' && a).join(' '), })) diff --git a/web/__tests__/tools/tool-browsing-and-filtering.test.tsx b/web/__tests__/tools/tool-browsing-and-filtering.test.tsx index dbefb1fdc3..aa8f59ca31 100644 --- a/web/__tests__/tools/tool-browsing-and-filtering.test.tsx +++ b/web/__tests__/tools/tool-browsing-and-filtering.test.tsx @@ -226,7 +226,7 @@ vi.mock('@/app/components/tools/mcp', () => ({ default: () =>
MCP List
, })) -vi.mock('@/utils/classnames', () => ({ +vi.mock('@langgenius/dify-ui/cn', () => ({ cn: (...args: unknown[]) => args.filter(Boolean).join(' '), })) diff --git a/web/__tests__/tools/tool-provider-detail-flow.test.tsx b/web/__tests__/tools/tool-provider-detail-flow.test.tsx index ce5ffe531e..c52871c946 100644 --- a/web/__tests__/tools/tool-provider-detail-flow.test.tsx +++ b/web/__tests__/tools/tool-provider-detail-flow.test.tsx @@ -112,7 +112,7 @@ vi.mock('@/service/use-tools', () => ({ useInvalidateAllWorkflowTools: () => vi.fn(), })) -vi.mock('@/utils/classnames', () => ({ +vi.mock('@langgenius/dify-ui/cn', () => ({ cn: (...args: unknown[]) => args.filter(Boolean).join(' '), })) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx index d3f15bdf46..8a1a6fd131 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/layout-main.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { NavIcon } from '@/app/components/app-sidebar/nav-link' import type { App } from '@/types/app' +import { cn } from '@langgenius/dify-ui/cn' import { RiDashboard2Fill, RiDashboard2Line, @@ -28,7 +29,6 @@ import dynamic from '@/next/dynamic' import { usePathname, useRouter } from '@/next/navigation' import { fetchAppDetailDirect } from '@/service/apps' import { AppModeEnum } from '@/types/app' -import { cn } from '@/utils/classnames' import s from './style.module.css' const TagManagementModal = dynamic(() => import('@/app/components/base/tag-management'), { diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx index 368c3dcfc3..dbc429cbdc 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/date-picker.tsx @@ -2,6 +2,7 @@ import type { Dayjs } from 'dayjs' import type { FC } from 'react' import type { TriggerProps } from '@/app/components/base/date-and-time-picker/types' +import { cn } from '@langgenius/dify-ui/cn' import { RiCalendarLine } from '@remixicon/react' import dayjs from 'dayjs' import { noop } from 'es-toolkit/function' @@ -9,7 +10,6 @@ import * as React from 'react' import { useCallback } from 'react' import Picker from '@/app/components/base/date-and-time-picker/date-picker' import { useLocale } from '@/context/i18n' -import { cn } from '@/utils/classnames' import { formatToLocalTime } from '@/utils/format' type Props = { @@ -30,7 +30,7 @@ const DatePicker: FC = ({ const renderDate = useCallback(({ value, handleClickTrigger, isOpen }: TriggerProps) => { return ( -
+
{value ? formatToLocalTime(value, locale, 'MMM D') : ''}
) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx index a4bf025139..a89b77e9e3 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/time-range-picker/range-selector.tsx @@ -3,13 +3,13 @@ import type { FC } from 'react' import type { PeriodParamsWithTimeRange, TimeRange } from '@/app/components/app/overview/app-chart' import type { Item } from '@/app/components/base/select' import type { I18nKeysByPrefix } from '@/types/i18n' +import { cn } from '@langgenius/dify-ui/cn' import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react' import dayjs from 'dayjs' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { SimpleSelect } from '@/app/components/base/select' -import { cn } from '@/utils/classnames' const today = dayjs() @@ -44,7 +44,7 @@ const RangeSelector: FC = ({ const renderTrigger = useCallback((item: Item | null, isOpen: boolean) => { return ( -
+
{isCustomRange ? t('filter.period.custom', { ns: 'appLog' }) : item?.name}
@@ -57,13 +57,13 @@ const RangeSelector: FC = ({ {selected && ( )} - {item.name} + {item.name} ) }, []) diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx index 17ca5d78cf..29de1a1eae 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-button.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { PopupProps } from './config-popup' +import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { useCallback, useRef, useState } from 'react' import { @@ -9,7 +10,6 @@ import { PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import { cn } from '@/utils/classnames' import ConfigPopup from './config-popup' type Props = { diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx index 138d238b47..15b81e1ad6 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/config-popup.tsx @@ -1,6 +1,7 @@ 'use client' import type { FC, JSX } from 'react' import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type' +import { cn } from '@langgenius/dify-ui/cn' import { useBoolean } from 'ahooks' import * as React from 'react' import { useCallback, useState } from 'react' @@ -9,7 +10,6 @@ import Divider from '@/app/components/base/divider' import Switch from '@/app/components/base/switch' import Tooltip from '@/app/components/base/tooltip' import Indicator from '@/app/components/header/indicator' -import { cn } from '@/utils/classnames' import ProviderConfigModal from './provider-config-modal' import ProviderPanel from './provider-panel' import TracingIcon from './tracing-icon' @@ -331,7 +331,7 @@ const ConfigPopup: FC = ({
-
+
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
{!readOnly && ( @@ -350,7 +350,7 @@ const ConfigPopup: FC = ({
-
+
{t(`${I18N_PREFIX}.tracingDescription`, { ns: 'app' })}
@@ -379,7 +379,7 @@ const ConfigPopup: FC = ({
{configuredProviderPanel()}
-
{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`, { ns: 'app' })}
+
{t(`${I18N_PREFIX}.configProviderTitle.moreProvider`, { ns: 'app' })}
{moreProviderPanel()}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx index 7c47249830..2b56ecfeea 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' +import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import Input from '@/app/components/base/input' -import { cn } from '@/utils/classnames' type Props = { className?: string diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx index 239427159c..9c42f85825 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/panel.tsx @@ -2,6 +2,7 @@ import type { FC } from 'react' import type { AliyunConfig, ArizeConfig, DatabricksConfig, LangFuseConfig, LangSmithConfig, MLflowConfig, OpikConfig, PhoenixConfig, TencentConfig, WeaveConfig } from './type' import type { TracingStatus } from '@/models/app' +import { cn } from '@langgenius/dify-ui/cn' import { RiArrowDownDoubleLine, RiEqualizer2Line, @@ -18,7 +19,6 @@ import Indicator from '@/app/components/header/indicator' import { useAppContext } from '@/context/app-context' import { usePathname } from '@/next/navigation' import { fetchTracingConfig as doFetchTracingConfig, fetchTracingStatus, updateTracingStatus } from '@/service/apps' -import { cn } from '@/utils/classnames' import ConfigButton from './config-button' import TracingIcon from './tracing-icon' import { TracingProvider } from './type' @@ -247,11 +247,11 @@ const Panel: FC = () => { >
-
{t(`${I18N_PREFIX}.title`, { ns: 'app' })}
+
{t(`${I18N_PREFIX}.title`, { ns: 'app' })}
@@ -286,12 +286,12 @@ const Panel: FC = () => { >
-
+
-
+
{t(`${I18N_PREFIX}.${enabled ? 'enabled' : 'disabled'}`, { ns: 'app' })}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx index 076a5cd7d8..ed7f4ab962 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/provider-panel.tsx @@ -1,5 +1,6 @@ 'use client' import type { FC } from 'react' +import { cn } from '@langgenius/dify-ui/cn' import { RiEqualizer2Line, } from '@remixicon/react' @@ -8,7 +9,6 @@ import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { AliyunIconBig, ArizeIconBig, DatabricksIconBig, LangfuseIconBig, LangsmithIconBig, MlflowIconBig, OpikIconBig, PhoenixIconBig, TencentIconBig, WeaveIconBig } from '@/app/components/base/icons/src/public/tracing' import { Eye as View } from '@/app/components/base/icons/src/vender/solid/general' -import { cn } from '@/utils/classnames' import { TracingProvider } from './type' const I18N_PREFIX = 'tracing' @@ -82,7 +82,7 @@ const ProviderPanel: FC = ({
- {isChosen &&
{t(`${I18N_PREFIX}.inUse`, { ns: 'app' })}
} + {isChosen &&
{t(`${I18N_PREFIX}.inUse`, { ns: 'app' })}
}
{!readOnly && (
@@ -102,7 +102,7 @@ const ProviderPanel: FC = ({
)}
-
+
{t(`${I18N_PREFIX}.${type}.description`, { ns: 'app' })}
diff --git a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx index 9bf1ddc50d..cdc0f753a6 100644 --- a/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx +++ b/web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/tracing-icon.tsx @@ -1,8 +1,8 @@ 'use client' import type { FC } from 'react' +import { cn } from '@langgenius/dify-ui/cn' import * as React from 'react' import { TracingIcon as Icon } from '@/app/components/base/icons/src/public/tracing' -import { cn } from '@/utils/classnames' type Props = { className?: string @@ -10,7 +10,7 @@ type Props = { } const sizeClassMap = { - lg: 'w-9 h-9 p-2 radius-lg', + lg: 'w-9 h-9 p-2 rounded-[10px]', md: 'w-6 h-6 p-1 rounded-lg', } diff --git a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx index 092e47278f..ba3272c1a7 100644 --- a/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx +++ b/web/app/(commonLayout)/datasets/(datasetDetailLayout)/[datasetId]/layout-main.tsx @@ -1,6 +1,7 @@ 'use client' import type { RemixiconComponentType } from '@remixicon/react' import type { FC } from 'react' +import { cn } from '@langgenius/dify-ui/cn' import { RiEqualizer2Fill, RiEqualizer2Line, @@ -24,7 +25,6 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useDocumentTitle from '@/hooks/use-document-title' import { usePathname } from '@/next/navigation' import { useDatasetDetail, useDatasetRelatedApps } from '@/service/knowledge/use-dataset' -import { cn } from '@/utils/classnames' type IAppDetailLayoutProps = { children: React.ReactNode diff --git a/web/app/(humanInputLayout)/form/[token]/form.tsx b/web/app/(humanInputLayout)/form/[token]/form.tsx index 898dab8f4a..be30585101 100644 --- a/web/app/(humanInputLayout)/form/[token]/form.tsx +++ b/web/app/(humanInputLayout)/form/[token]/form.tsx @@ -3,6 +3,7 @@ import type { ButtonProps } from '@/app/components/base/ui/button' import type { FormInputItem, UserAction } from '@/app/components/workflow/nodes/human-input/types' import type { SiteInfo } from '@/models/share' import type { HumanInputFormError } from '@/service/use-share' +import { cn } from '@langgenius/dify-ui/cn' import { RiCheckboxCircleFill, RiErrorWarningFill, @@ -22,7 +23,6 @@ import { Button } from '@/app/components/base/ui/button' import useDocumentTitle from '@/hooks/use-document-title' import { useParams } from '@/next/navigation' import { useGetHumanInputForm, useSubmitHumanInputForm } from '@/service/use-share' -import { cn } from '@/utils/classnames' export type FormData = { site: { site: SiteInfo } @@ -101,7 +101,7 @@ const FormContent = () => { return (
-
+
@@ -129,7 +129,7 @@ const FormContent = () => { return (
-
+
@@ -157,7 +157,7 @@ const FormContent = () => { return (
-
+
@@ -185,7 +185,7 @@ const FormContent = () => { return (
-
+
@@ -211,7 +211,7 @@ const FormContent = () => { return (
-
+
@@ -248,7 +248,7 @@ const FormContent = () => {
{site.title}
-
+
{contentList.map((content, index) => (
{!systemFeatures.branding.enabled && ( -
+
© {' '} {new Date().getFullYear()} diff --git a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx index 5b89084ea1..df102e609a 100644 --- a/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx +++ b/web/app/(shareLayout)/webapp-reset-password/set-password/page.tsx @@ -1,4 +1,5 @@ 'use client' +import { cn } from '@langgenius/dify-ui/cn' import { RiCheckboxCircleFill } from '@remixicon/react' import { useCountDown } from 'ahooks' import { useCallback, useState } from 'react' @@ -9,7 +10,6 @@ import { toast } from '@/app/components/base/ui/toast' import { validPassword } from '@/config' import { useRouter, useSearchParams } from '@/next/navigation' import { changeWebAppPasswordWithToken } from '@/service/common' -import { cn } from '@/utils/classnames' const ChangePasswordForm = () => { const { t } = useTranslation() diff --git a/web/app/(shareLayout)/webapp-signin/layout.tsx b/web/app/(shareLayout)/webapp-signin/layout.tsx index 21cb0e1f57..5451b45194 100644 --- a/web/app/(shareLayout)/webapp-signin/layout.tsx +++ b/web/app/(shareLayout)/webapp-signin/layout.tsx @@ -1,10 +1,10 @@ 'use client' import type { PropsWithChildren } from 'react' +import { cn } from '@langgenius/dify-ui/cn' import { useTranslation } from 'react-i18next' import { useGlobalPublicStore } from '@/context/global-public-context' import useDocumentTitle from '@/hooks/use-document-title' -import { cn } from '@/utils/classnames' export default function SignInLayout({ children }: PropsWithChildren) { const { t } = useTranslation() @@ -21,7 +21,7 @@ export default function SignInLayout({ children }: PropsWithChildren) {
{systemFeatures.branding.enabled === false && ( -
+
© {' '} {new Date().getFullYear()} diff --git a/web/app/(shareLayout)/webapp-signin/normalForm.tsx b/web/app/(shareLayout)/webapp-signin/normalForm.tsx index ed97e64806..436c7e64bb 100644 --- a/web/app/(shareLayout)/webapp-signin/normalForm.tsx +++ b/web/app/(shareLayout)/webapp-signin/normalForm.tsx @@ -1,4 +1,5 @@ 'use client' +import { cn } from '@langgenius/dify-ui/cn' import { RiContractLine, RiDoorLockLine, RiErrorWarningFill } from '@remixicon/react' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' @@ -8,7 +9,6 @@ import { IS_CE_EDITION } from '@/config' import { useGlobalPublicStore } from '@/context/global-public-context' import Link from '@/next/link' import { LicenseStatus } from '@/types/feature' -import { cn } from '@/utils/classnames' import MailAndCodeAuth from './components/mail-and-code-auth' import MailAndPasswordAuth from './components/mail-and-password-auth' import SSOAuth from './components/sso-auth' @@ -58,10 +58,10 @@ const NormalForm = () => {
- +

{t('licenseLost', { ns: 'login' })}

-

{t('licenseLostTip', { ns: 'login' })}

+

{t('licenseLostTip', { ns: 'login' })}

@@ -74,10 +74,10 @@ const NormalForm = () => {
- +

{t('licenseExpired', { ns: 'login' })}

-

{t('licenseExpiredTip', { ns: 'login' })}

+

{t('licenseExpiredTip', { ns: 'login' })}

@@ -90,10 +90,10 @@ const NormalForm = () => {
- +

{t('licenseInactive', { ns: 'login' })}

-

{t('licenseInactiveTip', { ns: 'login' })}

+

{t('licenseInactiveTip', { ns: 'login' })}

@@ -105,7 +105,7 @@ const NormalForm = () => {

{systemFeatures.branding.enabled ? t('pageTitleForE', { ns: 'login' }) : t('pageTitle', { ns: 'login' })}

-

{t('welcome', { ns: 'login' })}

+

{t('welcome', { ns: 'login' })}

@@ -122,7 +122,7 @@ const NormalForm = () => {
- {t('or', { ns: 'login' })} + {t('or', { ns: 'login' })}
)} @@ -159,7 +159,7 @@ const NormalForm = () => {

{t('noLoginMethod', { ns: 'login' })}

-

{t('noLoginMethodTip', { ns: 'login' })}

+

{t('noLoginMethodTip', { ns: 'login' })}