diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 3ebbd8ebb4a..0141deb57b9 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -75,6 +75,8 @@ "idb-keyval": "^6.2.1", "jsondiffpatch": "^0.6.0", "konva": "^9.3.15", + "linkify-react": "^4.2.0", + "linkifyjs": "^4.2.0", "lodash-es": "^4.17.21", "lru-cache": "^11.0.1", "mtwist": "^1.0.2", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index b81b3c23e5d..21282bca51d 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -74,6 +74,12 @@ dependencies: konva: specifier: ^9.3.15 version: 9.3.15 + linkify-react: + specifier: ^4.2.0 + version: 4.2.0(linkifyjs@4.2.0)(react@18.3.1) + linkifyjs: + specifier: ^4.2.0 + version: 4.2.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -6714,6 +6720,20 @@ packages: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} dev: false + /linkify-react@4.2.0(linkifyjs@4.2.0)(react@18.3.1): + resolution: {integrity: sha512-dIcDGo+n4FP2FPIHDcqB7cUE+omkcEgQJpc7sNNP4+XZ9FUhFAkKjGnHMzsZM+B4yF93sK166z9K5cKTe/JpzA==} + peerDependencies: + linkifyjs: ^4.0.0 + react: '>= 15.0.0' + dependencies: + linkifyjs: 4.2.0 + react: 18.3.1 + dev: false + + /linkifyjs@4.2.0: + resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==} + dev: false + /liqe@3.8.0: resolution: {integrity: sha512-cZ1rDx4XzxONBTskSPBp7/KwJ9qbUdF8EPnY4VjKXwHF1Krz9lgnlMTh1G7kd+KtPYvUte1mhuZeQSnk7KiSBg==} engines: {node: '>=12.0'} diff --git a/invokeai/frontend/web/src/common/components/linkify.ts b/invokeai/frontend/web/src/common/components/linkify.ts new file mode 100644 index 00000000000..4ac639468f7 --- /dev/null +++ b/invokeai/frontend/web/src/common/components/linkify.ts @@ -0,0 +1,17 @@ +import type { SystemStyleObject } from '@invoke-ai/ui-library'; +import type { Opts as LinkifyOpts } from 'linkifyjs'; + +export const linkifySx: SystemStyleObject = { + a: { + fontWeight: 'semibold', + }, + 'a:hover': { + textDecoration: 'underline', + }, +}; + +export const linkifyOptions: LinkifyOpts = { + target: '_blank', + rel: 'noopener noreferrer', + validate: (value) => /^https?:\/\//.test(value), +}; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowDescription.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowDescription.tsx index d6e3817fedd..4b31c85823a 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowDescription.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowListMenu/ActiveWorkflowDescription.tsx @@ -1,6 +1,8 @@ import { Text } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { linkifyOptions, linkifySx } from 'common/components/linkify'; import { selectWorkflowDescription } from 'features/nodes/store/workflowSlice'; +import Linkify from 'linkify-react'; import { memo } from 'react'; export const ActiveWorkflowDescription = memo(() => { @@ -11,8 +13,8 @@ export const ActiveWorkflowDescription = memo(() => { } return ( - - {description} + + {description} ); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContent.tsx index 144c7a121d4..dc89d50f8a9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/HeadingElementContent.tsx @@ -1,5 +1,7 @@ import type { HeadingProps, SystemStyleObject } from '@invoke-ai/ui-library'; import { Text } from '@invoke-ai/ui-library'; +import { linkifyOptions, linkifySx } from 'common/components/linkify'; +import Linkify from 'linkify-react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,13 +11,14 @@ const headingSx: SystemStyleObject = { '&[data-is-empty="true"]': { opacity: 0.3, }, + ...linkifySx, }; export const HeadingElementContent = memo(({ content, ...rest }: { content: string } & HeadingProps) => { const { t } = useTranslation(); return ( - {content || t('workflows.builder.headingPlaceholder')} + {content || t('workflows.builder.headingPlaceholder')} ); }); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx index 63acf2918b7..fa49391c22c 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementDescriptionEditable.tsx @@ -1,10 +1,12 @@ import { FormHelperText, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; +import { linkifyOptions, linkifySx } from 'common/components/linkify'; import { useEditable } from 'common/hooks/useEditable'; import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDescription'; import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate'; import { fieldDescriptionChanged } from 'features/nodes/store/nodesSlice'; import type { NodeFieldElement } from 'features/nodes/types/workflow'; +import Linkify from 'linkify-react'; import { memo, useCallback, useRef } from 'react'; export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeFieldElement }) => { @@ -36,7 +38,11 @@ export const NodeFieldElementDescriptionEditable = memo(({ el }: { el: NodeField }); if (!editable.isEditing) { - return {editable.value}; + return ( + + {editable.value} + + ); } return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx index f028ea4da18..ecee18bfb6d 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/NodeFieldElementViewMode.tsx @@ -1,5 +1,6 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { Flex, FormControl, FormHelperText } from '@invoke-ai/ui-library'; +import { linkifyOptions, linkifySx } from 'common/components/linkify'; import { InputFieldRenderer } from 'features/nodes/components/flow/nodes/Invocation/fields/InputFieldRenderer'; import { useContainerContext } from 'features/nodes/components/sidePanel/builder/contexts'; import { NodeFieldElementLabel } from 'features/nodes/components/sidePanel/builder/NodeFieldElementLabel'; @@ -7,6 +8,7 @@ import { useInputFieldDescription } from 'features/nodes/hooks/useInputFieldDesc import { useInputFieldTemplate } from 'features/nodes/hooks/useInputFieldTemplate'; import type { NodeFieldElement } from 'features/nodes/types/workflow'; import { NODE_FIELD_CLASS_NAME } from 'features/nodes/types/workflow'; +import Linkify from 'linkify-react'; import { memo, useMemo } from 'react'; const sx: SystemStyleObject = { @@ -43,7 +45,11 @@ export const NodeFieldElementViewMode = memo(({ el }: { el: NodeFieldElement }) settings={data.settings} /> - {showDescription && _description && {_description}} + {showDescription && _description && ( + + {_description} + + )} ); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContent.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContent.tsx index d62b56de7c5..c81d2bddb36 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/builder/TextElementContent.tsx @@ -1,5 +1,7 @@ import type { SystemStyleObject, TextProps } from '@invoke-ai/ui-library'; import { Text } from '@invoke-ai/ui-library'; +import { linkifyOptions, linkifySx } from 'common/components/linkify'; +import Linkify from 'linkify-react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,13 +11,14 @@ const textSx: SystemStyleObject = { '&[data-is-empty="true"]': { opacity: 0.3, }, + ...linkifySx, }; export const TextElementContent = memo(({ content, ...rest }: { content: string } & TextProps) => { const { t } = useTranslation(); return ( - {content || t('workflows.builder.textPlaceholder')} + {content || t('workflows.builder.textPlaceholder')} ); });