refactor(web): migrate rich tooltip overlays (#35675)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-04-29 14:44:39 +08:00 committed by GitHub
parent 25973c7d77
commit 0e55dcb297
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 493 additions and 341 deletions

View File

@ -385,9 +385,6 @@
}
},
"web/app/components/app/configuration/config/agent/agent-tools/index.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 9
}
@ -641,11 +638,6 @@
"count": 2
}
},
"web/app/components/app/overview/app-card.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/overview/customize/index.tsx": {
"no-restricted-imports": {
"count": 1
@ -2607,14 +2599,6 @@
"count": 1
}
},
"web/app/components/datasets/external-api/external-api-modal/index.tsx": {
"no-restricted-imports": {
"count": 2
},
"react/set-state-in-effect": {
"count": 1
}
},
"web/app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx": {
"react/set-state-in-effect": {
"count": 1
@ -2625,11 +2609,6 @@
"count": 1
}
},
"web/app/components/datasets/extra-info/statistics.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/datasets/formatted-text/flavours/type.ts": {
"ts/no-empty-object-type": {
"count": 1
@ -3299,9 +3278,6 @@
}
},
"web/app/components/plugins/plugin-detail-panel/endpoint-list.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 2
}
@ -3764,11 +3740,6 @@
"count": 1
}
},
"web/app/components/tools/mcp/detail/tool-item.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/tools/mcp/mcp-server-modal.tsx": {
"no-restricted-imports": {
"count": 1
@ -3782,11 +3753,6 @@
"count": 1
}
},
"web/app/components/tools/mcp/mcp-service-card.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/tools/mcp/modal.tsx": {
"no-restricted-imports": {
"count": 1
@ -4241,9 +4207,6 @@
}
},
"web/app/components/workflow/nodes/_base/components/prompt/editor.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 4
}
@ -5711,9 +5674,6 @@
}
},
"web/app/signin/one-more-step.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": {
"count": 1
}

View File

@ -6,7 +6,9 @@ import type { ToolWithProvider } from '@/app/components/workflow/types'
import type { AgentTool } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Switch } from '@langgenius/dify-ui/switch'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import {
RiDeleteBinLine,
RiEqualizer2Line,
@ -23,7 +25,6 @@ import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
import AppIcon from '@/app/components/base/app-icon'
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import { CollectionType } from '@/app/components/tools/types'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
@ -154,13 +155,23 @@ const AgentTools: FC = () => {
title={(
<div className="flex items-center">
<div className="mr-1">{t('agent.tools.name', { ns: 'appDebug' })}</div>
<Tooltip
popupContent={(
<div className="w-[180px]">
{t('agent.tools.description', { ns: 'appDebug' })}
</div>
)}
/>
<Popover>
<PopoverTrigger
openOnHover
aria-label={t('agent.tools.description', { ns: 'appDebug' })}
render={(
<button
type="button"
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm p-px outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
<span aria-hidden className="i-ri-question-line h-3.5 w-3.5 text-text-quaternary hover:text-text-tertiary" />
</button>
)}
/>
<PopoverContent popupClassName="w-[180px] px-3 py-2 system-xs-regular text-text-tertiary">
{t('agent.tools.description', { ns: 'appDebug' })}
</PopoverContent>
</Popover>
</div>
)}
headerRight={(
@ -216,34 +227,59 @@ const AgentTools: FC = () => {
<span className="pr-1.5 system-xs-medium text-text-secondary">{getProviderShowName(item)}</span>
<span className="text-text-tertiary">{item.tool_label}</span>
{!item.isDeleted && !readonly && (
<Tooltip
popupContent={(
<Popover>
<span className="h-4 w-4">
<PopoverTrigger
openOnHover
aria-label={item.tool_name}
render={(
<button
type="button"
className="ml-0.5 hidden h-4 w-4 items-center justify-center rounded-sm outline-hidden group-hover:inline-flex hover:bg-state-base-hover focus-visible:inline-flex focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
data-testid="tool-info-tooltip"
>
<RiInformation2Line className="h-4 w-4 text-text-tertiary" />
</button>
)}
/>
</span>
<PopoverContent popupClassName="w-[180px] px-3 py-2 system-xs-regular">
<div className="w-[180px]">
<div className="mb-1.5 text-text-secondary">{item.tool_name}</div>
<div className="mb-1.5 text-text-tertiary">{t('toolNameUsageTip', { ns: 'tools' })}</div>
<div className="cursor-pointer text-text-accent" onClick={() => copy(item.tool_name)}>{t('copyToolName', { ns: 'tools' })}</div>
<button
type="button"
className="cursor-pointer rounded-sm text-text-accent outline-hidden hover:underline focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
onClick={() => copy(item.tool_name)}
>
{t('copyToolName', { ns: 'tools' })}
</button>
</div>
)}
>
<div className="h-4 w-4">
<div className="ml-0.5 hidden group-hover:inline-block" data-testid="tool-info-tooltip">
<RiInformation2Line className="h-4 w-4 text-text-tertiary" />
</div>
</div>
</Tooltip>
</PopoverContent>
</Popover>
)}
</div>
</div>
<div className="ml-1 flex shrink-0 items-center">
{item.isDeleted && (
<div className="mr-2 flex items-center">
<Tooltip
popupContent={t('toolRemoved', { ns: 'tools' })}
>
<div className="mr-1 cursor-pointer rounded-md p-1 hover:bg-black/5">
<AlertTriangle className="h-4 w-4 text-[#F79009]" />
</div>
</Tooltip>
<Popover>
<PopoverTrigger
openOnHover
aria-label={t('toolRemoved', { ns: 'tools' })}
render={(
<button
type="button"
className="mr-1 cursor-pointer rounded-md p-1 outline-hidden hover:bg-black/5 focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
<AlertTriangle className="h-4 w-4 text-[#F79009]" />
</button>
)}
/>
<PopoverContent popupClassName="px-3 py-2 system-xs-regular text-text-tertiary">
{t('toolRemoved', { ns: 'tools' })}
</PopoverContent>
</Popover>
<div
className="cursor-pointer rounded-md p-1 text-text-tertiary hover:text-text-destructive"
onClick={() => {
@ -263,19 +299,25 @@ const AgentTools: FC = () => {
{!item.isDeleted && !readonly && (
<div className="mr-2 hidden items-center gap-1 group-hover:flex">
{!item.notAuthor && (
<Tooltip
popupContent={t('setBuiltInTools.infoAndSetting', { ns: 'tools' })}
needsDelay={false}
>
<div
className="cursor-pointer rounded-md p-1 hover:bg-black/5"
onClick={() => {
setCurrentTool(item)
setIsShowSettingTool(true)
}}
>
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</div>
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
className="cursor-pointer rounded-md p-1 outline-hidden hover:bg-black/5 focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
aria-label={t('setBuiltInTools.infoAndSetting', { ns: 'tools' })}
onClick={() => {
setCurrentTool(item)
setIsShowSettingTool(true)
}}
>
<RiEqualizer2Line className="h-4 w-4 text-text-tertiary" />
</button>
)}
/>
<TooltipContent>
{t('setBuiltInTools.infoAndSetting', { ns: 'tools' })}
</TooltipContent>
</Tooltip>
)}
<div

View File

@ -270,6 +270,7 @@ describe('AppCard', () => {
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'overview.appInfo.enableTooltip.description' }))
fireEvent.click(screen.getByText('overview.appInfo.enableTooltip.learnMore'))
expect(mockWindowOpen).toHaveBeenCalledWith('https://docs.example.com/use-dify/nodes/user-input', '_blank')

View File

@ -2,6 +2,7 @@
import type { ConfigParams } from './settings'
import type { AppDetailResponse } from '@/models/app'
import type { AppSSO } from '@/types/app'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Switch } from '@langgenius/dify-ui/switch'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
@ -9,7 +10,6 @@ import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppBasic from '@/app/components/app-sidebar/basic'
import { useStore as useAppStore } from '@/app/components/app/store'
import Tooltip from '@/app/components/base/tooltip'
import SecretKeyButton from '@/app/components/develop/secret-key/secret-key-button'
import Indicator from '@/app/components/header/indicator'
import { useAppContext } from '@/context/app-context'
@ -179,6 +179,31 @@ function AppCard({
triggerModeDisabled,
])
const missingStartNodeContent = cardState.appUnpublished || cardState.missingStartNode
? (
<>
<div className="mb-1 text-xs font-normal text-text-secondary">
{t('overview.appInfo.enableTooltip.description', { ns: 'appOverview' })}
</div>
<button
type="button"
className="cursor-pointer rounded-sm text-xs font-normal text-text-accent outline-hidden hover:underline focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
onClick={() => window.open(docLink('/use-dify/nodes/user-input'), '_blank')}
>
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
</button>
</>
)
: ''
const statusPopoverContent = cardState.toggleDisabled
? (
triggerModeDisabled && triggerModeMessage
? triggerModeMessage
: missingStartNodeContent
)
: ''
return (
<div
className={`${isInPanel ? 'border-t border-l-[0.5px]' : 'border-[0.5px] shadow-xs'} w-full max-w-full rounded-xl border-effects-highlight ${className ?? ''} ${cardState.isMinimalState ? 'h-12' : ''}`}
@ -187,13 +212,19 @@ function AppCard({
{triggerModeDisabled && (
triggerModeMessage
? (
<Tooltip
popupContent={triggerModeMessage}
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
position="right"
>
<div className="absolute inset-0 z-10 cursor-not-allowed rounded-xl" aria-hidden="true" />
</Tooltip>
<Popover>
<PopoverTrigger
openOnHover
aria-label={typeof triggerModeMessage === 'string' ? triggerModeMessage : basicName}
render={<button type="button" className="absolute inset-0 z-10 cursor-not-allowed rounded-xl outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-hover" />}
/>
<PopoverContent
placement="right"
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
>
{triggerModeMessage}
</PopoverContent>
</Popover>
)
: <div className="absolute inset-0 z-10 cursor-not-allowed rounded-xl" aria-hidden="true" />
)}
@ -219,38 +250,31 @@ function AppCard({
: t('overview.status.disable', { ns: 'appOverview' })}
</div>
</div>
<Tooltip
popupContent={
cardState.toggleDisabled
? (
triggerModeDisabled && triggerModeMessage
? triggerModeMessage
: (cardState.appUnpublished || cardState.missingStartNode)
? (
<>
<div className="mb-1 text-xs font-normal text-text-secondary">
{t('overview.appInfo.enableTooltip.description', { ns: 'appOverview' })}
</div>
<div
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
onClick={() => window.open(docLink('/use-dify/nodes/user-input'), '_blank')}
>
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
</div>
</>
)
: ''
)
: ''
}
position="right"
popupClassName="w-58 max-w-60 rounded-xl bg-components-panel-bg px-3.5 py-3 shadow-lg"
offset={24}
>
<div>
<Switch checked={cardState.runningStatus} onCheckedChange={onChangeStatus} disabled={cardState.toggleDisabled} />
</div>
</Tooltip>
{cardState.toggleDisabled && statusPopoverContent
? (
<Popover>
<PopoverTrigger
openOnHover
nativeButton={false}
aria-label={typeof statusPopoverContent === 'string' ? statusPopoverContent : t('overview.appInfo.enableTooltip.description', { ns: 'appOverview' })}
render={(
<div>
<Switch checked={cardState.runningStatus} onCheckedChange={onChangeStatus} disabled={cardState.toggleDisabled} />
</div>
)}
/>
<PopoverContent
placement="right"
sideOffset={24}
popupClassName="w-58 max-w-60 rounded-xl bg-components-panel-bg px-3.5 py-3 shadow-lg"
>
{statusPopoverContent}
</PopoverContent>
</Popover>
)
: (
<Switch checked={cardState.runningStatus} onCheckedChange={onChangeStatus} disabled={cardState.toggleDisabled} />
)}
</div>
{!cardState.isMinimalState && (
<AppCardUrlSection

View File

@ -10,13 +10,13 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import { RiBook2Line, RiCloseLine, RiInformation2Line, RiLock2Fill } from '@remixicon/react'
import { memo, useEffect, useState } from 'react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { PortalToFollowElem, PortalToFollowElemContent } from '@/app/components/base/portal-to-follow-elem'
import Tooltip from '@/app/components/base/tooltip'
import { createExternalAPI } from '@/service/datasets'
import Form from './Form'
@ -57,15 +57,20 @@ const formSchemas: FormSchema[] = [
required: true,
},
]
const emptyExternalAPIFormData: CreateExternalAPIReq = {
name: '',
settings: {
endpoint: '',
api_key: '',
},
}
const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCancel, datasetBindings, isEditMode, onEdit }) => {
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const [showConfirm, setShowConfirm] = useState(false)
const [formData, setFormData] = useState<CreateExternalAPIReq>({ name: '', settings: { endpoint: '', api_key: '' } })
useEffect(() => {
if (isEditMode && data)
setFormData(data)
}, [isEditMode, data])
const [formData, setFormData] = useState<CreateExternalAPIReq>(() => isEditMode && data ? data : emptyExternalAPIFormData)
const hasEmptyInputs = Object.values(formData).some(value => typeof value === 'string' ? value.trim() === '' : Object.values(value).some(v => v.trim() === ''))
const handleDataChange = (val: CreateExternalAPIReq) => {
setFormData(val)
@ -108,106 +113,121 @@ const AddExternalAPIModal: FC<AddExternalAPIModalProps> = ({ data, onSave, onCan
}
}
return (
<PortalToFollowElem open>
<PortalToFollowElemContent className="z-60 h-full w-full">
<div className="fixed inset-0 flex items-center justify-center bg-black/25">
<div className="shadows-shadow-xl relative flex w-[480px] flex-col items-start rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg">
<div className="flex flex-col items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<div className="grow self-stretch title-2xl-semi-bold text-text-primary">
{isEditMode ? t('editExternalAPIFormTitle', { ns: 'dataset' }) : t('createExternalAPI', { ns: 'dataset' })}
</div>
{isEditMode && (datasetBindings?.length ?? 0) > 0 && (
<div className="flex items-center system-xs-regular text-text-tertiary">
{t('editExternalAPIFormWarning.front', { ns: 'dataset' })}
<span className="flex cursor-pointer items-center text-text-accent">
&nbsp;
{datasetBindings?.length}
{' '}
{t('editExternalAPIFormWarning.end', { ns: 'dataset' })}
<Dialog
open
disablePointerDismissal
onOpenChange={(open) => {
if (!open)
onCancel()
}}
>
<DialogContent className="w-[480px]! max-w-none! overflow-visible! rounded-2xl! border-[0.5px]! border-components-panel-border! bg-components-panel-bg! p-0! shadow-xl!">
<div className="relative flex w-full flex-col items-start">
<div className="flex flex-col items-start gap-2 self-stretch pt-6 pr-14 pb-3 pl-6">
<DialogTitle className="grow self-stretch title-2xl-semi-bold text-text-primary">
{isEditMode ? t('editExternalAPIFormTitle', { ns: 'dataset' }) : t('createExternalAPI', { ns: 'dataset' })}
</DialogTitle>
{isEditMode && (datasetBindings?.length ?? 0) > 0 && (
<div className="flex items-center system-xs-regular text-text-tertiary">
{t('editExternalAPIFormWarning.front', { ns: 'dataset' })}
<span className="flex cursor-pointer items-center text-text-accent">
&nbsp;
{datasetBindings?.length}
{' '}
{t('editExternalAPIFormWarning.end', { ns: 'dataset' })}
&nbsp;
<Tooltip
popupClassName="flex items-center self-stretch w-[320px]"
popupContent={(
<div className="p-1">
<div className="flex items-start self-stretch pt-1 pr-3 pb-0.5 pl-2">
<div className="system-xs-medium-uppercase text-text-tertiary">{`${datasetBindings?.length} ${t('editExternalAPITooltipTitle', { ns: 'dataset' })}`}</div>
</div>
{datasetBindings?.map(binding => (
<div key={binding.id} className="flex items-center gap-1 self-stretch px-2 py-1">
<RiBook2Line className="h-4 w-4 text-text-secondary" />
<div className="system-sm-medium text-text-secondary">{binding.name}</div>
</div>
))}
</div>
<Popover>
<PopoverTrigger
openOnHover
aria-label={t('editExternalAPIFormWarning.end', { ns: 'dataset' })}
render={(
<button
type="button"
className="flex h-3.5 w-3.5 items-center justify-center rounded-sm outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
<RiInformation2Line className="h-3.5 w-3.5" />
</button>
)}
asChild={false}
position="bottom"
/>
<PopoverContent
placement="bottom"
popupClassName="flex w-[320px] items-center self-stretch px-3 py-2"
>
<RiInformation2Line className="h-3.5 w-3.5" />
</Tooltip>
</span>
</div>
)}
</div>
<ActionButton className="absolute top-5 right-5" onClick={onCancel}>
<RiCloseLine className="h-[18px] w-[18px] shrink-0 text-text-tertiary" />
</ActionButton>
<Form value={formData} onChange={handleDataChange} formSchemas={formSchemas} className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3" />
<div className="flex items-center justify-end gap-2 self-stretch p-6 pt-5">
<Button type="button" variant="secondary" onClick={onCancel}>
{t('externalAPIForm.cancel', { ns: 'dataset' })}
</Button>
<Button
type="submit"
variant="primary"
onClick={() => {
if (isEditMode && (datasetBindings?.length ?? 0) > 0)
setShowConfirm(true)
else if (isEditMode && onEdit)
onEdit(formData)
else
handleSave()
}}
disabled={hasEmptyInputs || loading}
>
{t('externalAPIForm.save', { ns: 'dataset' })}
</Button>
</div>
<div className="flex items-center justify-center gap-1 self-stretch rounded-b-2xl border-t-[0.5px] border-divider-subtle
bg-background-soft px-2 py-3 system-xs-regular text-text-tertiary"
>
<RiLock2Fill className="h-3 w-3 text-text-quaternary" />
{t('externalAPIForm.encrypted.front', { ns: 'dataset' })}
<a className="text-text-accent" target="_blank" rel="noopener noreferrer" href="https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html">
PKCS1_OAEP
</a>
{t('externalAPIForm.encrypted.end', { ns: 'dataset' })}
</div>
</div>
<AlertDialog
open={showConfirm && (datasetBindings?.length ?? 0) > 0}
onOpenChange={open => !open && setShowConfirm(false)}
>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
Warning
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{`${t('editExternalAPIConfirmWarningContent.front', { ns: 'dataset' })} ${datasetBindings?.length} ${t('editExternalAPIConfirmWarningContent.end', { ns: 'dataset' })}`}
</AlertDialogDescription>
<div className="p-1">
<div className="flex items-start self-stretch pt-1 pr-3 pb-0.5 pl-2">
<div className="system-xs-medium-uppercase text-text-tertiary">{`${datasetBindings?.length} ${t('editExternalAPITooltipTitle', { ns: 'dataset' })}`}</div>
</div>
{datasetBindings?.map(binding => (
<div key={binding.id} className="flex items-center gap-1 self-stretch px-2 py-1">
<RiBook2Line className="h-4 w-4 text-text-secondary" />
<div className="system-sm-medium text-text-secondary">{binding.name}</div>
</div>
))}
</div>
</PopoverContent>
</Popover>
</span>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={handleSave}>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
)}
</div>
<ActionButton className="absolute top-5 right-5" onClick={onCancel}>
<RiCloseLine className="h-[18px] w-[18px] shrink-0 text-text-tertiary" />
</ActionButton>
<Form value={formData} onChange={handleDataChange} formSchemas={formSchemas} className="flex flex-col items-start justify-center gap-4 self-stretch px-6 py-3" />
<div className="flex items-center justify-end gap-2 self-stretch p-6 pt-5">
<Button type="button" variant="secondary" onClick={onCancel}>
{t('externalAPIForm.cancel', { ns: 'dataset' })}
</Button>
<Button
type="submit"
variant="primary"
onClick={() => {
if (isEditMode && (datasetBindings?.length ?? 0) > 0)
setShowConfirm(true)
else if (isEditMode && onEdit)
onEdit(formData)
else
handleSave()
}}
disabled={hasEmptyInputs || loading}
>
{t('externalAPIForm.save', { ns: 'dataset' })}
</Button>
</div>
<div className="flex items-center justify-center gap-1 self-stretch rounded-b-2xl border-t-[0.5px] border-divider-subtle
bg-background-soft px-2 py-3 system-xs-regular text-text-tertiary"
>
<RiLock2Fill className="h-3 w-3 text-text-quaternary" />
{t('externalAPIForm.encrypted.front', { ns: 'dataset' })}
<a className="text-text-accent" target="_blank" rel="noopener noreferrer" href="https://pycryptodome.readthedocs.io/en/latest/src/cipher/oaep.html">
PKCS1_OAEP
</a>
{t('externalAPIForm.encrypted.end', { ns: 'dataset' })}
</div>
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>
<AlertDialog
open={showConfirm && (datasetBindings?.length ?? 0) > 0}
onOpenChange={open => !open && setShowConfirm(false)}
>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
Warning
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{`${t('editExternalAPIConfirmWarningContent.front', { ns: 'dataset' })} ${datasetBindings?.length} ${t('editExternalAPIConfirmWarningContent.end', { ns: 'dataset' })}`}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
<AlertDialogConfirmButton onClick={handleSave}>
{t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
</DialogContent>
</Dialog>
)
}
export default memo(AddExternalAPIModal)

View File

@ -1,10 +1,10 @@
import type { RelatedAppResponse } from '@/models/datasets'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { RiInformation2Line } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import LinkedAppsPanel from '@/app/components/base/linked-apps-panel'
import Tooltip from '@/app/components/base/tooltip'
import NoLinkedAppsPanel from '../no-linked-apps-panel'
type StatisticsProps = {
@ -40,26 +40,34 @@ const Statistics = ({
<div className="system-md-semibold-uppercase text-text-secondary">
{relatedAppsTotal ?? '--'}
</div>
<Tooltip
position="top-start"
noDecoration
needsDelay
popupContent={
hasRelatedApps
<Popover>
<PopoverTrigger
openOnHover
aria-label={t('datasetMenus.relatedApp', { ns: 'common' })}
render={(
<button
type="button"
className="flex cursor-pointer items-center gap-x-0.5 rounded-sm system-2xs-medium-uppercase text-text-tertiary outline-hidden hover:text-text-secondary focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
<span>{t('datasetMenus.relatedApp', { ns: 'common' })}</span>
<RiInformation2Line className="size-3" />
</button>
)}
/>
<PopoverContent
placement="top-start"
popupClassName="border-0 bg-transparent p-0 shadow-none"
>
{hasRelatedApps
? (
<LinkedAppsPanel
relatedApps={relatedApps.data}
isMobile={!expand}
/>
)
: <NoLinkedAppsPanel />
}
>
<div className="flex cursor-pointer items-center gap-x-0.5 system-2xs-medium-uppercase text-text-tertiary">
<span>{t('datasetMenus.relatedApp', { ns: 'common' })}</span>
<RiInformation2Line className="size-3" />
</div>
</Tooltip>
: <NoLinkedAppsPanel />}
</PopoverContent>
</Popover>
</div>
</div>
)

View File

@ -76,6 +76,8 @@ const createPluginDetail = (): PluginDetail => ({
})
describe('EndpointList', () => {
const getAddButton = () => screen.getByRole('button', { name: 'plugin.detailPanel.endpointModalTitle' })
beforeEach(() => {
vi.clearAllMocks()
mockEndpointListData = { endpoints: mockEndpoints }
@ -112,7 +114,7 @@ describe('EndpointList', () => {
it('should render add button', () => {
render(<EndpointList detail={createPluginDetail()} />)
expect(screen.getAllByRole('button').length).toBeGreaterThan(0)
expect(getAddButton()).toBeInTheDocument()
})
})
@ -120,8 +122,7 @@ describe('EndpointList', () => {
it('should show modal when add button clicked', () => {
render(<EndpointList detail={createPluginDetail()} />)
const addButton = screen.getAllByRole('button')[0]
fireEvent.click(addButton!)
fireEvent.click(getAddButton())
expect(screen.getByTestId('endpoint-modal'))!.toBeInTheDocument()
})
@ -129,8 +130,7 @@ describe('EndpointList', () => {
it('should hide modal when cancel clicked', () => {
render(<EndpointList detail={createPluginDetail()} />)
const addButton = screen.getAllByRole('button')[0]
fireEvent.click(addButton!)
fireEvent.click(getAddButton())
expect(screen.getByTestId('endpoint-modal'))!.toBeInTheDocument()
fireEvent.click(screen.getByTestId('modal-cancel'))
@ -140,8 +140,7 @@ describe('EndpointList', () => {
it('should call createEndpoint when save clicked', () => {
render(<EndpointList detail={createPluginDetail()} />)
const addButton = screen.getAllByRole('button')[0]
fireEvent.click(addButton!)
fireEvent.click(getAddButton())
fireEvent.click(screen.getByTestId('modal-save'))
expect(mockCreateEndpoint).toHaveBeenCalled()
@ -176,8 +175,7 @@ describe('EndpointList', () => {
it('should invalidate endpoint list after successful create', () => {
render(<EndpointList detail={createPluginDetail()} />)
const addButton = screen.getAllByRole('button')[0]
fireEvent.click(addButton!)
fireEvent.click(getAddButton())
fireEvent.click(screen.getByTestId('modal-save'))
expect(mockInvalidateEndpointList).toHaveBeenCalledWith('test-plugin')
@ -186,8 +184,7 @@ describe('EndpointList', () => {
it('should pass correct params to createEndpoint', () => {
render(<EndpointList detail={createPluginDetail()} />)
const addButton = screen.getAllByRole('button')[0]
fireEvent.click(addButton!)
fireEvent.click(getAddButton())
fireEvent.click(screen.getByTestId('modal-save'))
expect(mockCreateEndpoint).toHaveBeenCalledWith({

View File

@ -1,5 +1,6 @@
import type { PluginDetail } from '@/app/components/plugins/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiAddLine,
@ -11,7 +12,6 @@ import * as React from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useDocLink } from '@/context/i18n'
import {
@ -67,10 +67,23 @@ const EndpointList = ({ detail }: Props) => {
<div className="mb-1 flex h-6 items-center justify-between system-sm-semibold-uppercase text-text-secondary">
<div className="flex items-center gap-0.5">
{t('detailPanel.endpoints', { ns: 'plugin' })}
<Tooltip
position="right"
popupClassName="w-[240px] p-4 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border"
popupContent={(
<Popover>
<PopoverTrigger
openOnHover
aria-label={t('detailPanel.endpointsTip', { ns: 'plugin' })}
render={(
<button
type="button"
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm p-px outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
<span aria-hidden className="i-ri-question-line h-3.5 w-3.5 text-text-quaternary hover:text-text-tertiary" />
</button>
)}
/>
<PopoverContent
placement="right"
popupClassName="w-[240px] p-4 rounded-xl bg-components-panel-bg-blur border-[0.5px] border-components-panel-border"
>
<div className="flex flex-col gap-2">
<div className="flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle">
<RiApps2AddLine className="h-4 w-4 text-text-tertiary" />
@ -80,17 +93,19 @@ const EndpointList = ({ detail }: Props) => {
href={docLink('/develop-plugin/getting-started/getting-started-dify-plugin')}
target="_blank"
rel="noopener noreferrer"
className="inline-flex cursor-pointer items-center gap-1 system-xs-regular text-text-accent"
>
<div className="inline-flex cursor-pointer items-center gap-1 system-xs-regular text-text-accent">
<RiBookOpenLine className="h-3 w-3" />
{t('detailPanel.endpointsDocLink', { ns: 'plugin' })}
</div>
<RiBookOpenLine className="h-3 w-3" />
{t('detailPanel.endpointsDocLink', { ns: 'plugin' })}
</a>
</div>
)}
/>
</PopoverContent>
</Popover>
</div>
<ActionButton onClick={showEndpointModal}>
<ActionButton
aria-label={t('detailPanel.endpointModalTitle', { ns: 'plugin' })}
onClick={showEndpointModal}
>
<RiAddLine className="h-4 w-4" />
</ActionButton>
</div>

View File

@ -1,9 +1,9 @@
'use client'
import type { Tool } from '@/app/components/tools/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { useLocale } from '@/context/i18n'
import { getLanguage } from '@/i18n-config/language'
@ -51,25 +51,31 @@ const MCPToolItem = ({
}
return (
<Tooltip
key={tool.name}
position="left"
popupClassName="p-0! px-4! py-3.5! w-[360px]! border-[0.5px]! border-components-panel-border! rounded-xl! shadow-lg!"
popupContent={(
<Popover key={tool.name}>
<PopoverTrigger
openOnHover
aria-label={tool.label[language]}
render={(
<button
type="button"
className={cn('bg-components-panel-item-bg w-full cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 text-left shadow-xs outline-hidden hover:bg-components-panel-on-panel-item-bg-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover')}
>
<div className="pb-0.5 system-md-semibold text-text-secondary">{tool.label[language]}</div>
<div className="line-clamp-2 system-xs-regular text-text-tertiary" title={tool.description[language]}>{tool.description[language]}</div>
</button>
)}
/>
<PopoverContent
placement="left"
popupClassName="w-[360px]! rounded-xl! border-[0.5px]! border-components-panel-border! px-4! py-3.5! shadow-lg!"
>
<div>
<div className="mb-1 title-xs-semi-bold text-text-primary">{tool.label[language]}</div>
<div className="body-xs-regular text-text-secondary">{tool.description[language]}</div>
{renderParameters()}
</div>
)}
>
<div
className={cn('bg-components-panel-item-bg cursor-pointer rounded-xl border-[0.5px] border-components-panel-border-subtle px-4 py-3 shadow-xs hover:bg-components-panel-on-panel-item-bg-hover')}
>
<div className="pb-0.5 system-md-semibold text-text-secondary">{tool.label[language]}</div>
<div className="line-clamp-2 system-xs-regular text-text-tertiary" title={tool.description[language]}>{tool.description[language]}</div>
</div>
</Tooltip>
</PopoverContent>
</Popover>
)
}
export default MCPToolItem

View File

@ -15,14 +15,15 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Switch } from '@langgenius/dify-ui/switch'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiEditLine, RiLoopLeftLine } from '@remixicon/react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Divider from '@/app/components/base/divider'
import { Mcp } from '@/app/components/base/icons/src/vender/other'
import Tooltip from '@/app/components/base/tooltip'
import Indicator from '@/app/components/header/indicator'
import MCPServerModal from '@/app/components/tools/mcp/mcp-server-modal'
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
@ -81,13 +82,22 @@ const ServerURLSection: FC<ServerURLSectionProps> = ({
<CopyFeedback content={serverURL} className="size-6!" />
<Divider type="vertical" className="mx-0.5! h-3.5! shrink-0" />
{isCurrentWorkspaceManager && (
<Tooltip popupContent={t('overview.appInfo.regenerate', { ns: 'appOverview' }) || ''}>
<div
className="cursor-pointer rounded-md p-1 hover:bg-state-base-hover"
onClick={onRegenerate}
>
<RiLoopLeftLine className={cn('h-4 w-4 text-text-tertiary hover:text-text-secondary', genLoading && 'animate-spin')} />
</div>
<Tooltip>
<TooltipTrigger
render={(
<button
type="button"
className="cursor-pointer rounded-md p-1 outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
aria-label={t('overview.appInfo.regenerate', { ns: 'appOverview' }) || ''}
onClick={onRegenerate}
>
<RiLoopLeftLine className={cn('h-4 w-4 text-text-tertiary hover:text-text-secondary', genLoading && 'animate-spin')} />
</button>
)}
/>
<TooltipContent>
{t('overview.appInfo.regenerate', { ns: 'appOverview' })}
</TooltipContent>
</Tooltip>
)}
</>
@ -104,13 +114,19 @@ type TriggerModeOverlayProps = {
const TriggerModeOverlay: FC<TriggerModeOverlayProps> = ({ triggerModeMessage }) => {
if (triggerModeMessage) {
return (
<Tooltip
popupContent={triggerModeMessage}
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
position="right"
>
<div className="absolute inset-0 z-10 cursor-not-allowed rounded-xl" aria-hidden="true"></div>
</Tooltip>
<Popover>
<PopoverTrigger
openOnHover
aria-label={typeof triggerModeMessage === 'string' ? triggerModeMessage : 'Disabled'}
render={<button type="button" className="absolute inset-0 z-10 cursor-not-allowed rounded-xl outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-hover" />}
/>
<PopoverContent
placement="right"
popupClassName="max-w-64 rounded-xl bg-components-panel-bg px-3 py-2 text-xs text-text-secondary shadow-lg"
>
{triggerModeMessage}
</PopoverContent>
</Popover>
)
}
return <div className="absolute inset-0 z-10 cursor-not-allowed rounded-xl" aria-hidden="true"></div>
@ -146,12 +162,13 @@ function getTooltipContent({
<div className="mb-1 text-xs font-normal text-text-secondary">
{t('overview.appInfo.enableTooltip.description', { ns: 'appOverview' })}
</div>
<div
className="cursor-pointer text-xs font-normal text-text-accent hover:underline"
<button
type="button"
className="cursor-pointer rounded-sm text-xs font-normal text-text-accent outline-hidden hover:underline focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
onClick={() => window.open(docLink('/use-dify/nodes/user-input'), '_blank')}
>
{t('overview.appInfo.enableTooltip.learnMore', { ns: 'appOverview' })}
</div>
</button>
</>
)
}
@ -316,16 +333,31 @@ const MCPServiceCard: FC<IAppCardProps> = ({
</div>
</div>
<StatusIndicator serverActivated={serverActivated} />
<Tooltip
popupContent={tooltipContent}
position="right"
popupClassName="w-58 max-w-60 rounded-xl bg-components-panel-bg px-3.5 py-3 shadow-lg"
offset={24}
>
<div>
<Switch checked={activated} onCheckedChange={onChangeStatus} disabled={toggleDisabled} />
</div>
</Tooltip>
{toggleDisabled && tooltipContent
? (
<Popover>
<PopoverTrigger
openOnHover
nativeButton={false}
aria-label={typeof tooltipContent === 'string' ? tooltipContent : t('overview.appInfo.enableTooltip.description', { ns: 'appOverview' })}
render={(
<div>
<Switch checked={activated} onCheckedChange={onChangeStatus} disabled={toggleDisabled} />
</div>
)}
/>
<PopoverContent
placement="right"
sideOffset={24}
popupClassName="w-58 max-w-60 rounded-xl bg-components-panel-bg px-3.5 py-3 shadow-lg"
>
{tooltipContent}
</PopoverContent>
</Popover>
)
: (
<Switch checked={activated} onCheckedChange={onChangeStatus} disabled={toggleDisabled} />
)}
</div>
{!isMinimalState && (
<ServerURLSection

View File

@ -7,7 +7,9 @@ import type {
Variable,
} from '../../../../types'
import { cn } from '@langgenius/dify-ui/cn'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Switch } from '@langgenius/dify-ui/switch'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import {
RiDeleteBinLine,
} from '@remixicon/react'
@ -26,7 +28,6 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
import { Jinja } from '@/app/components/base/icons/src/vender/workflow'
import PromptEditor from '@/app/components/base/prompt-editor'
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
import Tooltip from '@/app/components/base/tooltip'
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars'
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
@ -165,7 +166,25 @@ const Editor: FC<Props> = ({
{' '}
{required && <span className="text-text-destructive">*</span>}
</div>
{!!titleTooltip && <Tooltip popupContent={titleTooltip} />}
{!!titleTooltip && (
<Popover>
<PopoverTrigger
openOnHover
aria-label={typeof titleTooltip === 'string' ? titleTooltip : typeof title === 'string' ? title : 'Help'}
render={(
<button
type="button"
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm p-px outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
<span aria-hidden className="i-ri-question-line h-3.5 w-3.5 text-text-quaternary hover:text-text-tertiary" />
</button>
)}
/>
<PopoverContent popupClassName="max-w-[300px] px-3 py-2 system-xs-regular text-text-tertiary">
{titleTooltip}
</PopoverContent>
</Popover>
)}
</div>
<div className="flex items-center">
<div className="text-xs leading-[18px] font-medium text-text-tertiary">{value?.length || 0}</div>
@ -184,34 +203,48 @@ const Editor: FC<Props> = ({
{/* Operations */}
<div className="flex items-center space-x-[2px]">
{isSupportJinja && (
<Tooltip
popupContent={(
<div>
<div>{t('common.enableJinja', { ns: 'workflow' })}</div>
<a className="text-text-accent" target="_blank" href="https://jinja.palletsprojects.com/en/2.10.x/">{t('common.learnMore', { ns: 'workflow' })}</a>
</div>
)}
>
<div className={cn(editionType === EditionType.jinja2 && 'border-components-button-ghost-bg-hover bg-components-button-ghost-bg-hover', 'flex h-[22px] items-center space-x-0.5 rounded-[5px] border border-transparent px-1.5 hover:border-components-button-ghost-bg-hover')}>
<Jinja className="h-3 w-6 text-text-quaternary" />
<Switch
size="sm"
checked={editionType === EditionType.jinja2}
onCheckedChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}
<div className={cn(editionType === EditionType.jinja2 && 'border-components-button-ghost-bg-hover bg-components-button-ghost-bg-hover', 'flex h-[22px] items-center space-x-0.5 rounded-[5px] border border-transparent px-1.5 hover:border-components-button-ghost-bg-hover')}>
<Popover>
<PopoverTrigger
openOnHover
aria-label={t('common.enableJinja', { ns: 'workflow' })}
render={(
<button
type="button"
className="flex h-4 w-7 items-center justify-center rounded-sm outline-hidden hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
<Jinja className="h-3 w-6 text-text-quaternary" />
</button>
)}
/>
</div>
</Tooltip>
<PopoverContent popupClassName="px-3 py-2 system-xs-regular text-text-tertiary">
<div>
<div>{t('common.enableJinja', { ns: 'workflow' })}</div>
<a className="text-text-accent hover:underline" target="_blank" rel="noopener noreferrer" href="https://jinja.palletsprojects.com/en/2.10.x/">{t('common.learnMore', { ns: 'workflow' })}</a>
</div>
</PopoverContent>
</Popover>
<Switch
size="sm"
checked={editionType === EditionType.jinja2}
onCheckedChange={(checked) => {
onEditionTypeChange?.(checked ? EditionType.jinja2 : EditionType.basic)
}}
/>
</div>
)}
{!readOnly && (
<Tooltip
popupContent={`${t('common.insertVarTip', { ns: 'workflow' })}`}
>
<ActionButton onClick={handleInsertVariable}>
<Variable02 className="h-4 w-4" />
</ActionButton>
<Tooltip>
<TooltipTrigger
render={(
<ActionButton onClick={handleInsertVariable}>
<Variable02 className="h-4 w-4" />
</ActionButton>
)}
/>
<TooltipContent>
{t('common.insertVarTip', { ns: 'workflow' })}
</TooltipContent>
</Tooltip>
)}
{showRemove && (

View File

@ -2,11 +2,11 @@
import type { Reducer } from 'react'
import type { LanguagesSupported } from '@/i18n-config/language'
import { Button } from '@langgenius/dify-ui/button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { toast } from '@langgenius/dify-ui/toast'
import { useReducer } from 'react'
import { useTranslation } from 'react-i18next'
import Tooltip from '@/app/components/base/tooltip'
import { LICENSE_LINK } from '@/constants/link'
import { languages } from '@/i18n-config/language'
import Link from '@/next/link'
@ -94,21 +94,35 @@ const OneMoreStep = () => {
<div className="mx-auto mt-6 w-full">
<div className="relative">
<div className="mb-5">
<label className="my-2 flex items-center justify-between system-md-semibold text-text-secondary">
{t('invitationCode', { ns: 'login' })}
<Tooltip
popupContent={(
<div className="w-[256px] text-xs font-medium">
<div className="my-2 flex items-center justify-between system-md-semibold text-text-secondary">
<label htmlFor="invitation_code">
{t('invitationCode', { ns: 'login' })}
</label>
<Popover>
<PopoverTrigger
openOnHover
render={(
<button
type="button"
className="cursor-pointer rounded-sm text-text-accent-secondary outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-hover"
>
{t('dontHave', { ns: 'login' })}
</button>
)}
/>
<PopoverContent
placement="top"
popupClassName="w-[256px] px-3 py-2 text-xs font-medium text-text-tertiary"
>
<div>
<div className="font-medium">{t('sendUsMail', { ns: 'login' })}</div>
<div className="cursor-pointer text-xs font-medium text-text-accent-secondary">
<a href="mailto:request-invitation@langgenius.ai">request-invitation@langgenius.ai</a>
</div>
</div>
)}
>
<span className="cursor-pointer text-text-accent-secondary">{t('dontHave', { ns: 'login' })}</span>
</Tooltip>
</label>
</PopoverContent>
</Popover>
</div>
<div className="mt-1">
<Input
id="invitation_code"