diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/__tests__/item.spec.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/__tests__/item.spec.tsx index e5760310a9..896897a777 100644 --- a/web/app/components/workflow/nodes/tool/components/tool-form/__tests__/item.spec.tsx +++ b/web/app/components/workflow/nodes/tool/components/tool-form/__tests__/item.spec.tsx @@ -78,6 +78,7 @@ describe('tool/tool-form/item', () => { mockUseLanguage.mockReturnValue('en_US') }) + // Text input fields render their descriptions inline above the input. it('should render text input labels and forward props to form input item', () => { const handleChange = vi.fn() const handleManageInputField = vi.fn() @@ -121,6 +122,31 @@ describe('tool/tool-form/item', () => { }) }) + // URL fragments inside descriptions should be rendered as external links. + it('should render URLs in descriptions as external links', () => { + render( + , + ) + + const link = screen.getByRole('link', { name: 'https://docs.dify.ai/tools' }) + expect(link).toHaveAttribute('href', 'https://docs.dify.ai/tools') + expect(link).toHaveAttribute('target', '_blank') + expect(link).toHaveAttribute('rel', 'noopener noreferrer') + expect(link.parentElement).toHaveTextContent('Visit https://docs.dify.ai/tools for docs') + }) + + // Non-text fields keep their descriptions inside the tooltip and support JSON schema preview. it('should show tooltip for non-description fields and open the schema modal', () => { const objectSchema = createSchema({ name: 'tool_config', diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx index d83f445c2c..5011cf9486 100644 --- a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx +++ b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx @@ -1,5 +1,5 @@ 'use client' -import type { FC } from 'react' +import type { FC, ReactNode } from 'react' import type { ToolVarInputs } from '../../types' import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Tool } from '@/app/components/tools/types' @@ -15,6 +15,45 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi import { SchemaModal } from '@/app/components/plugins/plugin-detail-panel/tool-selector/components' import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item' +const URL_REGEX = /(https?:\/\/\S+)/g + +const renderDescriptionWithLinks = (description: string): ReactNode => { + const matches = [...description.matchAll(URL_REGEX)] + + if (!matches.length) + return description + + const parts: ReactNode[] = [] + let currentIndex = 0 + + matches.forEach((match, index) => { + const [url] = match + const start = match.index ?? 0 + + if (start > currentIndex) + parts.push(description.slice(currentIndex, start)) + + parts.push( + + {url} + , + ) + + currentIndex = start + url.length + }) + + if (currentIndex < description.length) + parts.push(description.slice(currentIndex)) + + return parts +} + type Props = { readOnly: boolean nodeId: string @@ -87,7 +126,9 @@ const ToolFormItem: FC = ({ )} {showDescription && tooltip && ( - {tooltip[language] || tooltip.en_US} + + {renderDescriptionWithLinks(tooltip[language] || tooltip.en_US)} + )}