refactor(web): migrate shared icon commands (#40620)

This commit is contained in:
yyh 2026-08-13 02:29:36 +00:00 committed by GitHub
parent 3925b5fa1b
commit 312bc4912d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 270 additions and 275 deletions

View File

@ -424,11 +424,6 @@
"count": 1
}
},
"web/app/components/app/configuration/debug/__tests__/index.spec.tsx": {
"typescript/no-explicit-any": {
"count": 1
}
},
"web/app/components/app/configuration/debug/chat-user-input.tsx": {
"jsx_a11y/no-autofocus": {
"count": 2

View File

@ -14,6 +14,7 @@ import {
DropdownMenuContent,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
@ -31,7 +32,6 @@ import { useInvalid } from '@/service/use-base'
import { useExportPipelineDSL } from '@/service/use-pipeline'
import { downloadBlob } from '@/utils/download'
import { getDatasetACLCapabilities } from '@/utils/permission'
import ActionButton from '../../base/action-button'
import RenameDatasetModal from '../../datasets/rename-modal'
import Menu from './menu'
@ -174,15 +174,15 @@ const DropDown = ({ expand, triggerClassName }: DropDownProps) => {
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
size={expand ? 'l' : 'm'}
size={expand ? 'lg' : 'md'}
className={cn('data-popup-open:bg-state-base-hover', triggerClassName)}
/>
>
<span aria-hidden className="i-ri-more-fill size-4" />
</IconButton>
}
>
<span aria-hidden className="i-ri-more-fill size-4" />
</DropdownMenuTrigger>
/>
<DropdownMenuContent
placement={expand ? 'bottom-end' : 'right-start'}
sideOffset={4}

View File

@ -1,9 +1,10 @@
'use client'
import { Button, buttonVariants } from '@langgenius/dify-ui/button'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useTranslation } from 'react-i18next'
import CopyFeedback from '@/app/components/base/copy-feedback'
import ShareQRCode from '@/app/components/base/qrcode'
import ActionButton from '../../../base/action-button'
import { AccessPointEndpoint } from './access-point-card'
type AccessPointUrlProps = {
@ -43,6 +44,7 @@ export function AccessPointUrl({
unavailableLabel,
value,
}: AccessPointUrlProps) {
const { t } = useTranslation()
const detailsAvailable = !loading && !unavailable
const disabledActions = (
@ -90,9 +92,8 @@ export function AccessPointUrl({
<ShareQRCode content={value} />
))}
{showRegenerate && (
<ActionButton
className="size-6 px-0"
aria-label={regenerateLabel}
<IconButton
aria-label={regenerateLabel || t(($) => $['operation.regenerate'], { ns: 'common' })}
disabled={regenerateDisabled || regenerating}
onClick={onRegenerate}
>
@ -100,7 +101,7 @@ export function AccessPointUrl({
aria-hidden
className={`i-ri-loop-left-line size-4 ${regenerating ? 'animate-spin' : ''}`}
/>
</ActionButton>
</IconButton>
)}
{showOpen && (
<>

View File

@ -5,6 +5,7 @@ import type { AnnotationItem, AnnotationItemBasic } from './type'
import type { AnnotationReplyConfig } from '@/models/debug'
import type { App } from '@/types/app'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { Switch } from '@langgenius/dify-ui/switch'
import { toast } from '@langgenius/dify-ui/toast'
@ -13,7 +14,6 @@ import { useDebounce } from 'ahooks'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
import Loading from '@/app/components/base/loading'
@ -202,7 +202,7 @@ const Annotation: FC<Props> = (props) => {
{annotationConfig?.enabled && (
<div className="flex items-center pr-1 pl-1.5">
<div className="mr-1 h-3.5 w-px shrink-0 bg-divider-subtle"></div>
<ActionButton
<IconButton
aria-label={t(($) => $['initSetup.configTitle'], {
ns: 'appAnnotation',
})}
@ -212,7 +212,7 @@ const Annotation: FC<Props> = (props) => {
aria-hidden="true"
className="size-4 text-text-tertiary"
/>
</ActionButton>
</IconButton>
</div>
)}
</div>

View File

@ -2,9 +2,9 @@
import type { AnnotationItem } from './type'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { CheckboxGroup } from '@langgenius/dify-ui/checkbox-group'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import useTimestamp from '@/hooks/use-timestamp'
import BatchAction from './batch-action'
import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
@ -54,18 +54,18 @@ function AnnotationTableRow({
<td className="p-3 pr-2">{item.hit_count}</td>
<td className="w-24 p-3 pr-2" onClick={(e) => e.stopPropagation()}>
<div className="flex space-x-1 text-text-tertiary">
<ActionButton
<IconButton
aria-label={t(($) => $['feature.annotation.edit'], { ns: 'appDebug' })}
onClick={() => onView(item)}
>
<span aria-hidden className="i-ri-edit-line size-4" />
</ActionButton>
<ActionButton
</IconButton>
<IconButton
aria-label={t(($) => $['feature.annotation.remove'], { ns: 'appDebug' })}
onClick={() => onRemoveClick(item.id)}
>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</ActionButton>
</IconButton>
</div>
</td>
</tr>

View File

@ -12,11 +12,11 @@ import {
DrawerPortal,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Loading from '@/app/components/base/loading'
import TabSlider from '@/app/components/base/tab-slider-plain'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
@ -189,9 +189,12 @@ const SettingBuiltInTool: FC<Props> = ({
{/* header */}
<div className="relative border-b border-divider-subtle p-4 pb-3">
<div className="absolute top-3 right-3">
<ActionButton onClick={onHide}>
<RiCloseLine className="size-4" />
</ActionButton>
<IconButton
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={onHide}
>
<RiCloseLine aria-hidden="true" className="size-4" />
</IconButton>
</div>
{showBackButton && (
<div

View File

@ -194,7 +194,7 @@ describe('dataset-config/card-item', () => {
})
})
it('should call onRemove and toggle destructive state on hover', async () => {
it('should call onRemove from its named icon command', async () => {
const user = userEvent.setup()
const dataset = createDataset()
const { onRemove } = renderItem(dataset)
@ -205,15 +205,6 @@ describe('dataset-config/card-item', () => {
hidden: true,
})
expect(deleteButton.className).not.toContain('action-btn-destructive')
fireEvent.mouseEnter(deleteButton)
expect(deleteButton.className).toContain('action-btn-destructive')
expect(card.className).toContain('border-state-destructive-border')
fireEvent.mouseLeave(deleteButton)
expect(deleteButton.className).not.toContain('action-btn-destructive')
await user.click(deleteButton)
expect(onRemove).toHaveBeenCalledWith(dataset.id)
})

View File

@ -10,11 +10,10 @@ import {
DrawerPortal,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { RiDeleteBinLine, RiEditLine } from '@remixicon/react'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
@ -73,32 +72,29 @@ const Item: FC<ItemProps> = ({ config, onSave, onRemove, readonly = false, edita
</div>
<div className="ml-2 hidden shrink-0 items-center space-x-1 group-hover:flex">
{editable && !readonly && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.edit'], { ns: 'common' })}
onClick={(e) => {
e.stopPropagation()
setShowSettingsModal(true)
}}
>
<RiEditLine aria-hidden="true" className="size-4 shrink-0 text-text-tertiary" />
</ActionButton>
<span
aria-hidden="true"
className="i-ri-edit-line size-4 shrink-0 text-text-tertiary"
/>
</IconButton>
)}
{!readonly && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.remove'], { ns: 'common' })}
tone="destructive"
onClick={() => onRemove(config.id)}
state={isDeleting ? ActionButtonState.Destructive : ActionButtonState.Default}
onMouseEnter={() => setIsDeleting(true)}
onMouseLeave={() => setIsDeleting(false)}
>
<RiDeleteBinLine
aria-hidden="true"
className={cn(
'size-4 shrink-0 text-text-tertiary',
isDeleting && 'text-text-destructive',
)}
/>
</ActionButton>
<span aria-hidden="true" className="i-ri-delete-bin-line size-4 shrink-0" />
</IconButton>
)}
</div>
{!!config.indexing_technique && (

View File

@ -1,5 +1,6 @@
import type { ComponentProps } from 'react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import * as React from 'react'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import ConfigContext from '@/context/debug-configuration'
@ -484,10 +485,10 @@ const renderDebug = (
}
render(
React.createElement(ConfigContext.Provider, {
value: createContextValue(options.contextValue),
children: <Debug {...props} />,
}),
// oxlint-disable-next-line eslint-react/no-context-provider -- use-context-selector contexts are not React 19 context components.
<ConfigContext.Provider value={createContextValue(options.contextValue)}>
<Debug {...props} />
</ConfigContext.Provider>,
)
return { onSetting, notify: mockState.mockToastCall, props }
@ -854,7 +855,7 @@ describe('Debug', () => {
await waitFor(() => expect(mockState.mockSendCompletionMessage).toHaveBeenCalledTimes(1))
const [, requestData, handlers] = (mockState.mockSendCompletionMessage.mock.calls[0] ??
[]) as [unknown, any, { onNotifyError: (message: string) => void }]
[]) as [unknown, unknown, { onNotifyError: (message: string) => void }]
expect(requestData).toMatchObject({
inputs: { question: 'hello' },
model_config: {
@ -1083,7 +1084,9 @@ describe('Debug', () => {
})
})
it('should emit restart event when refresh is clicked in multiple-model mode', () => {
it('should emit restart event when refresh is clicked in multiple-model mode', async () => {
const user = userEvent.setup()
renderDebug({
props: {
debugWithMultipleModel: true,
@ -1093,7 +1096,7 @@ describe('Debug', () => {
},
})
fireEvent.click(screen.getAllByTestId('action-button')[0]!)
await user.click(screen.getByRole('button', { name: 'common.operation.refresh' }))
expect(mockState.mockEventEmitterEmit).toHaveBeenCalledWith({
type: APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
})

View File

@ -7,9 +7,9 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { ModelStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useDebugConfigurationContext } from '@/context/debug-configuration'
import { useProviderContext } from '@/context/provider-context'
@ -80,12 +80,12 @@ const DebugItem: FC<DebugItemProps> = ({ modelAndParameter, className, style })
<DropdownMenu>
<DropdownMenuTrigger
render={
<ActionButton
className="focus-visible:ring-2 focus-visible:ring-state-accent-solid data-popup-open:bg-state-base-hover"
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
className="data-popup-open:bg-state-base-hover"
>
<span aria-hidden className="i-ri-more-fill size-4 text-text-tertiary" />
</ActionButton>
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-[160px]">

View File

@ -6,6 +6,7 @@ import type { ModelParameterModalProps } from '@/app/components/header/account-s
import type { Inputs } from '@/models/debug'
import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiAddLine, RiEqualizer2Line, RiSparklingFill } from '@remixicon/react'
import { useBoolean } from 'ahooks'
@ -442,12 +443,12 @@ const Debug: FC<IDebug> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['operation.refresh'], { ns: 'common' })}
onClick={clearConversation}
>
<RefreshCcw01 aria-hidden="true" className="size-4" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>

View File

@ -26,6 +26,7 @@ import {
DrawerPortal,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
@ -43,7 +44,6 @@ import { useShallow } from 'zustand/react/shallow'
import ModelInfo from '@/app/components/app/log/model-info'
import { useStore as useAppStore } from '@/app/components/app/store'
import TextGeneration from '@/app/components/app/text-generate/item'
import ActionButton from '@/app/components/base/action-button'
import AgentLogModal from '@/app/components/base/agent-log-modal'
import Chat from '@/app/components/base/chat/chat'
import CopyIcon from '@/app/components/base/copy-icon'
@ -475,13 +475,13 @@ function DetailPanel({ appDetail, detail, onClose, onFeedback }: IDetailPanel) {
<div className="flex grow flex-wrap items-center justify-end gap-y-1">
{!isAdvanced && <ModelInfo model={detail.model_config.model} />}
</div>
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
onClick={onClose}
>
<RiCloseLine className="size-4 text-text-tertiary" />
</ActionButton>
</IconButton>
</div>
{/* Panel Body */}
<div className="shrink-0 px-1 pt-1">

View File

@ -1,5 +1,6 @@
import type { SiteInfo } from '@/models/share'
import { fireEvent, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { act } from 'react'
@ -40,12 +41,7 @@ const baseProps = {
className: 'custom-modal',
}
const getCopyButton = () => {
const buttons = screen.getAllByRole('button')
const actionButton = buttons.find((button) => button.className.includes('action-btn'))
expect(actionButton).toBeDefined()
return actionButton!
}
const getCopyButton = () => screen.getByRole('button', { name: /copy/i })
describe('Embedded', () => {
beforeAll(() => {
@ -75,6 +71,8 @@ describe('Embedded', () => {
})
it('copies iframe snippet', async () => {
const user = userEvent.setup()
await act(async () => {
render(<Embedded {...baseProps} />)
})
@ -88,11 +86,8 @@ describe('Embedded', () => {
).toBeInTheDocument()
})
const actionButton = getCopyButton()
const innerDiv = actionButton.querySelector('div')
await act(async () => {
fireEvent.click(innerDiv ?? actionButton)
})
const copyButton = getCopyButton()
await user.click(copyButton)
await waitFor(() => {
expect(mockedCopy).toHaveBeenCalledWith(expect.stringContaining('/chatbot/token'))
@ -192,6 +187,8 @@ describe('Embedded', () => {
})
it('copies script content when scripts option is selected', async () => {
const user = userEvent.setup()
await act(async () => {
render(<Embedded {...baseProps} />)
})
@ -207,11 +204,8 @@ describe('Embedded', () => {
expect(codeBlock?.textContent ?? '').toContain('background-color: #000000')
})
const actionButton = getCopyButton()
const innerDiv = actionButton.querySelector('div')
await act(async () => {
fireEvent.click(innerDiv ?? actionButton)
})
const copyButton = getCopyButton()
await user.click(copyButton)
await waitFor(() => {
expect(mockedCopy).toHaveBeenCalledWith(expect.stringContaining("token: 'token'"))
@ -219,6 +213,8 @@ describe('Embedded', () => {
})
it('copies chrome plugin URL (without prefix) when chromePlugin option is selected', async () => {
const user = userEvent.setup()
await act(async () => {
render(<Embedded {...baseProps} />)
})
@ -233,11 +229,8 @@ describe('Embedded', () => {
expect(codeBlock?.textContent ?? '').toContain('ChatBot URL:')
})
const actionButton = getCopyButton()
const innerDiv = actionButton.querySelector('div')
await act(async () => {
fireEvent.click(innerDiv ?? actionButton)
})
const copyButton = getCopyButton()
await user.click(copyButton)
await waitFor(() => {
expect(mockedCopy).toHaveBeenCalledWith(expect.stringContaining('/chatbot/token'))

View File

@ -7,12 +7,12 @@ import type {
import type { SiteInfo } from '@/models/share'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useSuspenseQuery } from '@tanstack/react-query'
import copy from 'copy-to-clipboard'
import { Suspense, use, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { createTheme } from '@/app/components/base/chat/embedded-chatbot/theme/theme'
import { InputVarType } from '@/app/components/workflow/types'
import { userProfileQueryOptions } from '@/features/account-profile/client'
@ -305,7 +305,7 @@ const EmbeddedContent = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={
(copiedOption === option
? t(($) => $[`${prefixEmbedded}.copied`], { ns: 'appOverview' })
@ -313,13 +313,12 @@ const EmbeddedContent = ({
}
onClick={() => void onClickCopy()}
>
{copiedOption === option && (
{copiedOption === option ? (
<span aria-hidden="true" className="i-ri-clipboard-fill size-4" />
)}
{copiedOption !== option && (
) : (
<span aria-hidden="true" className="i-ri-clipboard-line size-4" />
)}
</ActionButton>
</IconButton>
}
/>
<TooltipContent>

View File

@ -3,6 +3,7 @@ import type { FC } from 'react'
import type { FeedbackType } from '@/app/components/base/chat/chat/type'
import type { WorkflowProcess } from '@/app/components/base/chat/types'
import type { AppSourceType } from '@/service/share'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import {
RiBookmark3Line,
@ -78,35 +79,30 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
appSourceType !== AppSourceTypeEnum.installedApp &&
!isResponding && (
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs">
<ActionButton
<IconButton
aria-label={t(($) => $['operation.log'], { ns: 'common' })}
disabled={isError || !messageId}
title={t(($) => $['operation.log'], { ns: 'common' })}
onClick={onOpenLogModal}
>
<RiFileList3Line className="size-4" />
</ActionButton>
<RiFileList3Line aria-hidden="true" className="size-4" />
</IconButton>
</div>
)}
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs">
{moreLikeThis && !isTryApp && (
<ActionButton
<IconButton
aria-label={t(($) => $['feature.moreLikeThis.title'], { ns: 'appDebug' })}
state={
depth === MAX_GENERATION_DEPTH
? ActionButtonState.Disabled
: ActionButtonState.Default
}
disabled={depth === MAX_GENERATION_DEPTH}
title={t(($) => $['feature.moreLikeThis.title'], { ns: 'appDebug' })}
onClick={onMoreLikeThis}
>
<RiSparklingLine className="size-4" />
</ActionButton>
<RiSparklingLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{isShowTextToSpeech && !isTryApp && <NewAudioButton id={messageId!} voice={voice} />}
{showCopyAction && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.copy'], { ns: 'common' })}
disabled={isError || !messageId}
title={t(($) => $['operation.copy'], { ns: 'common' })}
@ -117,20 +113,20 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
toast.success(t(($) => $['actionMsg.copySuccessfully'], { ns: 'common' }))
}}
>
<RiClipboardLine className="size-4" />
</ActionButton>
<RiClipboardLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{isInWebApp && isError && (
<ActionButton
<IconButton
aria-label={t(($) => $['generation.batchFailed.retry'], { ns: 'share' })}
title={t(($) => $['generation.batchFailed.retry'], { ns: 'share' })}
onClick={onRetry}
>
<RiResetLeftLine className="size-4" />
</ActionButton>
<RiResetLeftLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{isInWebApp && !isWorkflow && !isTryApp && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.save'], { ns: 'common' })}
disabled={isError || !messageId}
title={t(($) => $['operation.save'], { ns: 'common' })}
@ -138,8 +134,8 @@ const GenerationActionGroups: FC<GenerationActionGroupsProps> = ({
onSave?.(messageId as string)
}}
>
<RiBookmark3Line className="size-4" />
</ActionButton>
<RiBookmark3Line aria-hidden="true" className="size-4" />
</IconButton>
)}
</div>
{(supportFeedback || isInWebApp) && !isWorkflow && !isTryApp && !isError && messageId && (

View File

@ -2,12 +2,12 @@
import type { FC } from 'react'
import type { SavedMessage } from '@/models/debug'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import { RiClipboardLine, RiDeleteBinLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { Markdown } from '@/app/components/base/markdown'
import NewAudioButton from '@/app/components/base/new-audio-button'
import NoData from './no-data'
@ -48,7 +48,7 @@ const SavedItems: FC<ISavedItemsProps> = ({
<div className="absolute right-2 bottom-1">
<div className="ml-1 flex items-center gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs">
{isShowTextToSpeech && <NewAudioButton value={answer} />}
<ActionButton
<IconButton
aria-label={t(($) => $['operation.copy'], { ns: 'common' })}
onClick={() => {
copy(answer)
@ -56,15 +56,15 @@ const SavedItems: FC<ISavedItemsProps> = ({
}}
>
<RiClipboardLine aria-hidden="true" className="size-4" />
</ActionButton>
<ActionButton
</IconButton>
<IconButton
aria-label={t(($) => $['operation.delete'], { ns: 'common' })}
onClick={() => {
onRemove(id)
}}
>
<RiDeleteBinLine aria-hidden="true" className="size-4" />
</ActionButton>
</IconButton>
</div>
</div>
</div>

View File

@ -8,9 +8,9 @@ import {
AlertDialogDescription,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
@ -78,9 +78,14 @@ const HeaderInMobile = () => {
return (
<>
<div className="flex shrink-0 items-center gap-1 bg-mask-top2bottom-gray-50-to-transparent px-2 py-3">
<ActionButton size="l" className="shrink-0" onClick={() => setShowSidebar(true)}>
<div className="i-ri-menu-line h-4.5 w-4.5" />
</ActionButton>
<IconButton
aria-label={t(($) => $['sidebar.expandSidebar'], { ns: 'layout' })}
size="lg"
className="shrink-0"
onClick={() => setShowSidebar(true)}
>
<div aria-hidden="true" className="i-ri-menu-line h-4.5 w-4.5" />
</IconButton>
<div className="flex grow items-center justify-center">
{!currentConversationId && (
<>

View File

@ -65,17 +65,17 @@ describe('MobileOperationDropdown Component', () => {
})
})
it('applies hover state to ActionButton when open', async () => {
it('exposes popup-open state on the trigger', async () => {
const user = userEvent.setup()
render(<MobileOperationDropdown {...defaultProps} />)
const trigger = screen.getByRole('button', { name: 'common.operation.more' })
// closed state
expect(trigger).not.toHaveClass('action-btn-hover')
expect(trigger).not.toHaveAttribute('data-popup-open')
// open state
await user.click(trigger)
expect(trigger).toHaveClass('action-btn-hover')
expect(trigger).toHaveAttribute('data-popup-open')
})
it('closes the menu after clicking an action', async () => {

View File

@ -9,11 +9,11 @@ import {
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiEditBoxLine, RiLayoutRight2Line, RiResetLeftLine } from '@remixicon/react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
@ -89,14 +89,14 @@ const Header = () => {
!isSidebarCollapsed && 'user-select-none opacity-0',
)}
>
<ActionButton
<IconButton
aria-label={t(($) => $['sidebar.expandSidebar'], { ns: 'layout' })}
className={cn(!isSidebarCollapsed && 'cursor-default')}
size="l"
size="lg"
onClick={() => handleSidebarCollapse(false)}
>
<RiLayoutRight2Line aria-hidden="true" className="h-4.5 w-4.5" />
</ActionButton>
</IconButton>
<div className="mr-1 shrink-0">
<AppIcon
size="large"
@ -134,19 +134,14 @@ const Header = () => {
disabled={!!currentConversationId}
render={
<div>
<ActionButton
<IconButton
aria-label={t(($) => $['chat.newChatTip'], { ns: 'share' })}
size="l"
state={
!currentConversationId || isResponding
? ActionButtonState.Disabled
: ActionButtonState.Default
}
size="lg"
disabled={!currentConversationId || isResponding}
onClick={handleNewConversation}
>
<RiEditBoxLine aria-hidden="true" className="h-4.5 w-4.5" />
</ActionButton>
</IconButton>
</div>
}
/>
@ -159,13 +154,13 @@ const Header = () => {
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['chat.resetChat'], { ns: 'share' })}
size="l"
size="lg"
onClick={handleNewConversation}
>
<RiResetLeftLine aria-hidden="true" className="h-4.5 w-4.5" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{t(($) => $['chat.resetChat'], { ns: 'share' })}</TooltipContent>

View File

@ -4,9 +4,9 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
type Props = Readonly<{
handleResetChat: () => void
@ -29,16 +29,15 @@ const MobileOperationDropdown = ({
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
render={(props, state) => (
<ActionButton
{...props}
render={
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
size="l"
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<div className="i-ri-more-fill h-4.5 w-4.5" aria-hidden="true" />
</ActionButton>
)}
</IconButton>
}
/>
<DropdownMenuContent placement="bottom-end" sideOffset={4} popupClassName="min-w-[160px]">
<DropdownMenuItem

View File

@ -98,16 +98,16 @@ describe('ViewFormDropdown', () => {
expect(screen.getByText('Num Form')).toBeInTheDocument()
})
it('applies correct state to ActionButton when open', async () => {
it('exposes popup-open state on the trigger', async () => {
const user = userEvent.setup()
render(<ViewFormDropdown />)
const trigger = screen.getByRole('button', { name: 'share.chat.viewChatSettings' })
// closed state
expect(trigger).not.toHaveClass('action-btn-hover')
expect(trigger).not.toHaveAttribute('data-popup-open')
// open state
await user.click(trigger)
expect(trigger).toHaveClass('action-btn-hover')
expect(trigger).toHaveAttribute('data-popup-open')
})
})

View File

@ -1,25 +1,22 @@
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { RiChatSettingsLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
import { Message3Fill } from '@/app/components/base/icons/src/public/other'
const ViewFormDropdown = () => {
const { t } = useTranslation()
return (
<Popover>
<PopoverTrigger
render={(props, state) => (
<ActionButton
{...props}
render={
<IconButton
aria-label={t(($) => $['chat.viewChatSettings'], { ns: 'share' })}
size="l"
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<RiChatSettingsLine aria-hidden="true" className="h-4.5 w-4.5" />
</ActionButton>
)}
<span aria-hidden="true" className="i-ri-chat-settings-line h-4.5 w-4.5" />
</IconButton>
}
/>
<PopoverContent
placement="bottom-end"
@ -29,7 +26,7 @@ const ViewFormDropdown = () => {
>
<div className="w-100 rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg backdrop-blur-xs">
<div className="flex items-center gap-3 rounded-t-2xl border-b border-divider-subtle px-6 py-4">
<Message3Fill className="size-6 shrink-0" />
<span aria-hidden className="i-custom-public-other-message-3-fill size-6 shrink-0" />
<div className="grow system-xl-semibold text-text-secondary">
{t(($) => $['chat.chatSettingsTitle'], { ns: 'share' })}
</div>

View File

@ -10,10 +10,10 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import AppIcon from '@/app/components/base/app-icon'
import List from '@/app/components/base/chat/chat-with-history/sidebar/list'
import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
@ -106,22 +106,22 @@ const Sidebar = ({ isPanel }: Props) => {
{appData?.site.title}
</div>
{!isMobile && isSidebarCollapsed && (
<ActionButton
<IconButton
aria-label={t(($) => $['sidebar.expandSidebar'], { ns: 'layout' })}
size="l"
size="lg"
onClick={() => handleSidebarCollapse(false)}
>
<span aria-hidden className="i-ri-expand-right-line h-4.5 w-4.5" />
</ActionButton>
</IconButton>
)}
{!isMobile && !isSidebarCollapsed && (
<ActionButton
<IconButton
aria-label={t(($) => $['sidebar.collapseSidebar'], { ns: 'layout' })}
size="l"
size="lg"
onClick={() => handleSidebarCollapse(true)}
>
<span aria-hidden className="i-ri-layout-left-2-line h-4.5 w-4.5" />
</ActionButton>
</IconButton>
)}
</div>
<div className="shrink-0 px-3 py-4">

View File

@ -9,6 +9,7 @@ import {
DialogDescription,
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
@ -398,7 +399,7 @@ function Operation({
<NewAudioButton id={id} value={content} voice={config?.text_to_speech?.voice} />
)}
{hasPublicContent && !humanInputFormDataList?.length && (
<ActionButton
<IconButton
aria-label={copyLabel}
onClick={() => {
copy(content)
@ -406,12 +407,12 @@ function Operation({
}}
>
<span aria-hidden="true" className="i-ri-clipboard-line size-4" />
</ActionButton>
</IconButton>
)}
{(!noChatInput || showRegenerate) && (
<ActionButton aria-label={regenerateLabel} onClick={() => onRegenerate?.(item)}>
<IconButton aria-label={regenerateLabel} onClick={() => onRegenerate?.(item)}>
<span aria-hidden="true" className="i-ri-reset-left-line size-4" />
</ActionButton>
</IconButton>
)}
{shouldShowAnnotationAction && (
<AnnotationCtrlButton

View File

@ -4,9 +4,9 @@ import type { EnableType } from '../../types'
import type { FileUpload } from '@/app/components/base/features/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { FileUploaderInChatInput } from '@/app/components/base/file-uploader'
type OperationProps = {
@ -43,15 +43,15 @@ const Operation: FC<OperationProps> = ({
<FileUploaderInChatInput readonly={readonly} fileConfig={fileConfig} />
)}
{speechToTextConfig?.enabled && onShowVoiceInput && (
<ActionButton
className="shrink-0 outline-hidden focus-visible:inset-ring-2 focus-visible:inset-ring-state-accent-solid"
size="l"
<IconButton
className="shrink-0"
size="lg"
aria-label={t(($) => $['voiceInput.start'], { ns: 'common' })}
disabled={readonly}
onClick={onShowVoiceInput}
>
<span className="i-ri-mic-line size-5" aria-hidden="true" />
</ActionButton>
</IconButton>
)}
</div>
<Button

View File

@ -1,13 +1,15 @@
import type { FC } from 'react'
import type { IChatItem } from '@/app/components/base/chat/chat/type'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { RiFileList3Line } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import { useStore as useAppStore } from '@/app/components/app/store'
import ActionButton from '@/app/components/base/action-button'
type LogProps = {
logItem: IChatItem
}
const Log: FC<LogProps> = ({ logItem }) => {
const { t } = useTranslation()
const setCurrentLogItem = useAppStore((s) => s.setCurrentLogItem)
const setShowPromptLogModal = useAppStore((s) => s.setShowPromptLogModal)
const setShowAgentLogModal = useAppStore((s) => s.setShowAgentLogModal)
@ -27,9 +29,9 @@ const Log: FC<LogProps> = ({ logItem }) => {
else setShowPromptLogModal(true)
}}
>
<ActionButton>
<RiFileList3Line className="size-4" />
</ActionButton>
<IconButton aria-label={t(($) => $['operation.log'], { ns: 'common' })}>
<RiFileList3Line aria-hidden="true" className="size-4" />
</IconButton>
</div>
)
}

View File

@ -3,6 +3,7 @@ import type { Theme } from '../embedded-chatbot/theme/theme'
import type { ChatItem } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import copy from 'copy-to-clipboard'
import { memo, useCallback, useEffect, useRef, useState } from 'react'
@ -11,7 +12,6 @@ import Textarea from 'react-textarea-autosize'
import { FileList } from '@/app/components/base/file-uploader'
import { User } from '@/app/components/base/icons/src/public/avatar'
import { Markdown } from '@/app/components/base/markdown'
import ActionButton from '../../action-button'
import { CssTransform } from '../embedded-chatbot/theme/utils'
import ContentSwitch from './content-switch'
import { useChatContext } from './context'
@ -158,7 +158,7 @@ const Question: FC<QuestionProps> = ({
className="absolute hidden gap-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-md backdrop-blur-xs group-hover:flex"
style={{ right: contentWidth + 8 }}
>
<ActionButton
<IconButton
aria-label={copyLabel}
onClick={() => {
copy(content)
@ -166,11 +166,11 @@ const Question: FC<QuestionProps> = ({
}}
>
<div className="i-ri-clipboard-line size-4" aria-hidden="true" />
</ActionButton>
</IconButton>
{enableEdit && (
<ActionButton aria-label={editLabel} onClick={handleEdit}>
<IconButton aria-label={editLabel} onClick={handleEdit}>
<div className="i-ri-edit-line size-4" aria-hidden="true" />
</ActionButton>
</IconButton>
)}
</div>
</div>

View File

@ -1,12 +1,12 @@
import type { FC } from 'react'
import type { Theme } from '../theme/theme'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
import Divider from '@/app/components/base/divider'
import { DifyLogo } from '@/app/components/base/logo/dify-logo'
@ -115,8 +115,8 @@ const Header: FC<IHeaderProps> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={
expanded
? t(($) => $['chat.collapse'], { ns: 'share' })
@ -132,7 +132,7 @@ const Header: FC<IHeaderProps> = ({
) : (
<div className="i-ri-expand-diagonal-2-line h-4.5 w-4.5" aria-hidden="true" />
)}
</ActionButton>
</IconButton>
}
/>
<TooltipContent>
@ -146,13 +146,13 @@ const Header: FC<IHeaderProps> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={t(($) => $['chat.resetChat'], { ns: 'share' })}
onClick={onCreateNewChat}
>
<div className="i-ri-reset-left-line h-4.5 w-4.5" aria-hidden="true" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{t(($) => $['chat.resetChat'], { ns: 'share' })}</TooltipContent>
@ -185,8 +185,8 @@ const Header: FC<IHeaderProps> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={
expanded
? t(($) => $['chat.collapse'], { ns: 'share' })
@ -211,7 +211,7 @@ const Header: FC<IHeaderProps> = ({
aria-hidden="true"
/>
)}
</ActionButton>
</IconButton>
}
/>
<TooltipContent>
@ -225,8 +225,8 @@ const Header: FC<IHeaderProps> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={t(($) => $['chat.resetChat'], { ns: 'share' })}
onClick={onCreateNewChat}
>
@ -234,7 +234,7 @@ const Header: FC<IHeaderProps> = ({
className={cn('i-ri-reset-left-line h-4.5 w-4.5', theme?.colorPathOnHeader)}
aria-hidden="true"
/>
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{t(($) => $['chat.resetChat'], { ns: 'share' })}</TooltipContent>

View File

@ -1,8 +1,8 @@
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
type Props = Readonly<{
@ -14,19 +14,18 @@ const ViewFormDropdown = ({ iconColor }: Props) => {
return (
<Popover>
<PopoverTrigger
render={(props, state) => (
<ActionButton
{...props}
render={
<IconButton
aria-label={t(($) => $['chat.viewChatSettings'], { ns: 'share' })}
size="l"
state={state.open ? ActionButtonState.Hover : ActionButtonState.Default}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<span
aria-hidden
className={cn('i-ri-chat-settings-line h-4.5 w-4.5 shrink-0', iconColor)}
/>
</ActionButton>
)}
</IconButton>
}
/>
<PopoverContent
placement="bottom-end"

View File

@ -1,10 +1,10 @@
'use client'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiClipboardFill, RiClipboardLine } from '@remixicon/react'
import { useClipboard } from 'foxact/use-clipboard'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import copyStyle from './style.module.css'
type Props = Readonly<{
@ -35,10 +35,13 @@ const CopyFeedback = ({ content }: Props) => {
<Tooltip>
<TooltipTrigger
render={
<ActionButton aria-label={safeText} onClick={handleCopy}>
{copied && <RiClipboardFill className="size-4" aria-hidden="true" />}
{!copied && <RiClipboardLine className="size-4" aria-hidden="true" />}
</ActionButton>
<IconButton aria-label={safeText} onClick={handleCopy}>
{copied ? (
<RiClipboardFill className="size-4" aria-hidden="true" />
) : (
<RiClipboardLine className="size-4" aria-hidden="true" />
)}
</IconButton>
}
/>
<TooltipContent>{safeText}</TooltipContent>

View File

@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiEditLine, RiFileEditLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { addAnnotation } from '@/service/annotation'
@ -52,12 +52,12 @@ const AnnotationCtrlButton: FC<Props> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['feature.annotation.edit'], { ns: 'appDebug' })}
onClick={onEdit}
>
<RiEditLine aria-hidden className="size-4" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>
@ -69,12 +69,12 @@ const AnnotationCtrlButton: FC<Props> = ({
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['feature.annotation.add'], { ns: 'appDebug' })}
onClick={handleAdd}
>
<RiFileEditLine aria-hidden className="size-4" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>

View File

@ -1,10 +1,10 @@
import type { FileEntity } from '../types'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { ProgressCircle } from '@langgenius/dify-ui/progress'
import { RiDeleteBinLine, RiDownloadLine, RiEyeLine } from '@remixicon/react'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { PreviewMode } from '@/app/components/base/features/types'
import { ReplayLine } from '@/app/components/base/icons/src/vender/other'
import ImagePreview from '@/app/components/base/image-uploader/image-preview'
@ -77,29 +77,41 @@ const FileInAttachmentItem = ({
/>
)}
{progress === -1 && (
<ActionButton className="mr-1" onClick={() => onReUpload?.(id)}>
<ReplayLine className="size-4 text-text-tertiary" />
</ActionButton>
<IconButton
aria-label={`${t(($) => $['operation.retry'], { ns: 'common' })} ${name}`}
className="mr-1"
onClick={() => onReUpload?.(id)}
>
<ReplayLine aria-hidden="true" className="size-4 text-text-tertiary" />
</IconButton>
)}
{showDeleteAction && (
<ActionButton onClick={() => onRemove?.(id)}>
<RiDeleteBinLine className="size-4" />
</ActionButton>
<IconButton
aria-label={`${t(($) => $['operation.remove'], { ns: 'common' })} ${name}`}
onClick={() => onRemove?.(id)}
>
<RiDeleteBinLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{canPreview && isImageFile && (
<ActionButton className="mr-1" onClick={() => setImagePreviewUrl(url || '')}>
<RiEyeLine className="size-4" />
</ActionButton>
<IconButton
aria-label={`${t(($) => $['operation.view'], { ns: 'common' })} ${name}`}
className="mr-1"
onClick={() => setImagePreviewUrl(url || '')}
>
<RiEyeLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{showDownloadAction && (
<ActionButton
<IconButton
aria-label={`${t(($) => $['operation.download'], { ns: 'common' })} ${name}`}
onClick={(e) => {
e.stopPropagation()
downloadUrl({ url: url || base64Url || '', fileName: name, target: '_blank' })
}}
>
<RiDownloadLine className="size-4" />
</ActionButton>
<RiDownloadLine aria-hidden="true" className="size-4" />
</IconButton>
)}
</div>
</div>

View File

@ -1,10 +1,10 @@
import type { FileEntity } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { ProgressCircle } from '@langgenius/dify-ui/progress'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import AudioPreview from '@/app/components/base/file-uploader/audio-preview'
import PdfPreview from '@/app/components/base/file-uploader/dynamic-pdf-preview'
import VideoPreview from '@/app/components/base/file-uploader/video-preview'
@ -83,9 +83,9 @@ const FileItem = ({
{!!file.size && formatFileSize(file.size)}
</div>
{showDownloadAction && download_url && (
<ActionButton
<IconButton
aria-label={t(($) => $['operation.download'], { ns: 'common' })}
size="m"
size="md"
className="absolute -top-1 -right-1 hidden group-hover/file-item:flex"
onClick={(e) => {
e.stopPropagation()
@ -93,7 +93,7 @@ const FileItem = ({
}}
>
<span className="i-ri-download-line size-3.5 text-text-tertiary" aria-hidden="true" />
</ActionButton>
</IconButton>
)}
{progress >= 0 && !fileIsUploaded(file) && (
<ProgressCircle

View File

@ -1,11 +1,11 @@
'use client'
import type { InputProps } from '../input'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useClipboard } from 'foxact/use-clipboard'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '../action-button'
type InputWithCopyProps = {
showCopyButton?: boolean
@ -55,12 +55,12 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>(
<Tooltip>
<TooltipTrigger
render={
<ActionButton
<IconButton
size="xs"
variant="ghost"
aria-label={safeTooltipText}
onClick={handleCopy}
onMouseLeave={reset}
className="hover:bg-components-button-ghost-bg-hover"
>
{copied ? (
<span
@ -73,7 +73,7 @@ const InputWithCopy = React.forwardRef<HTMLInputElement, InputWithCopyProps>(
aria-hidden="true"
/>
)}
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{safeTooltipText}</TooltipContent>

View File

@ -5,6 +5,7 @@ import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/
import type { Type } from '@/app/components/workflow/nodes/llm/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef } from 'react'
@ -15,7 +16,6 @@ import {
isParagraphFormInput,
isSelectFormInput,
} from '@/app/components/workflow/nodes/human-input/types'
import ActionButton from '../../../action-button'
import { VariableX } from '../../../icons/src/vender/workflow'
import InputField from './input-field'
import VariableBlock from './variable-block'
@ -166,18 +166,18 @@ const HITLInputComponentUI: FC<HITLInputComponentUIProps> = ({
{!readonly && (
<div className="hidden h-full shrink-0 items-center space-x-1 group-hover:flex">
<div className="flex h-full items-center" ref={editBtnRef}>
<ActionButton size="s" aria-label={t(($) => $['operation.edit'], { ns: 'common' })}>
<IconButton size="sm" aria-label={t(($) => $['operation.edit'], { ns: 'common' })}>
<span className="i-ri-edit-line size-4 text-text-tertiary" aria-hidden="true" />
</ActionButton>
</IconButton>
</div>
<div className="flex h-full items-center" ref={removeBtnRef}>
<ActionButton size="s" aria-label={t(($) => $['operation.remove'], { ns: 'common' })}>
<IconButton size="sm" aria-label={t(($) => $['operation.remove'], { ns: 'common' })}>
<span
className="i-ri-delete-bin-line size-4 text-text-tertiary"
aria-hidden="true"
/>
</ActionButton>
</IconButton>
</div>
</div>
)}

View File

@ -1,10 +1,10 @@
'use client'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { QRCodeCanvas as QRCode } from 'qrcode.react'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import { downloadUrl } from '@/utils/download'
type Props = Readonly<{
@ -52,9 +52,9 @@ const ShareQRCode = ({ content }: Props) => {
<div className="relative size-6">
<TooltipTrigger
render={
<ActionButton aria-label={safeTooltipText} onClick={toggleQRCode}>
<IconButton aria-label={safeTooltipText} onClick={toggleQRCode}>
<span className="i-ri-qr-code-line size-4" aria-hidden="true" />
</ActionButton>
</IconButton>
}
/>
{isShow && (

View File

@ -8,9 +8,9 @@ import {
DropdownMenuRadioItemIndicator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useTheme } from 'next-themes'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
const THEMES = ['light', 'dark', 'system'] as const
export type Theme = (typeof THEMES)[number]
@ -39,14 +39,15 @@ export default function ThemeSelector() {
<DropdownMenu>
<DropdownMenuTrigger
render={
<ActionButton
<IconButton
aria-label={t(($) => $['theme.theme'], { ns: 'common' })}
className="h-8 w-8 p-1.5 data-popup-open:bg-state-base-hover"
/>
size="lg"
className="data-popup-open:bg-state-base-hover"
>
{getCurrentIcon()}
</IconButton>
}
>
{getCurrentIcon()}
</DropdownMenuTrigger>
/>
<DropdownMenuContent placement="bottom-end" sideOffset={6} popupClassName="w-[144px]">
<DropdownMenuRadioGroup<Theme>
value={currentTheme}

View File

@ -14,9 +14,9 @@ import {
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import CopyFeedback from '@/app/components/base/copy-feedback'
import Loading from '@/app/components/base/loading'
import useTimestamp from '@/hooks/use-timestamp'
@ -155,12 +155,12 @@ export function SecretKeyModalView({
<div className="flex grow space-x-2 px-3">
<CopyFeedback content={api.token} />
{canManage && (
<ActionButton
<IconButton
aria-label={`${t(($) => $['operation.delete'], { ns: 'common' })} ${generateToken(api.token)}`}
onClick={() => setDeleteKeyId(api.id)}
>
<span aria-hidden className="i-ri-delete-bin-line size-4" />
</ActionButton>
</IconButton>
)}
</div>
</div>

View File

@ -3,13 +3,13 @@ import type { FC } from 'react'
import type { EmbeddedChatbotContextValue } from '@/app/components/base/chat/embedded-chatbot/context'
import type { TryAppInfo } from '@/service/try-app'
import { cn } from '@langgenius/dify-ui/cn'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiResetLeftLine } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Alert from '@/app/components/base/alert'
import AppIcon from '@/app/components/base/app-icon'
import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
@ -79,13 +79,13 @@ const TryApp: FC<Props> = ({ appId, appDetail, className }) => {
<Tooltip>
<TooltipTrigger
render={
<ActionButton
size="l"
<IconButton
size="lg"
aria-label={t(($) => $['chat.resetChat'], { ns: 'share' })}
onClick={handleNewConversation}
>
<RiResetLeftLine className="h-4.5 w-4.5" aria-hidden="true" />
</ActionButton>
</IconButton>
}
/>
<TooltipContent>{t(($) => $['chat.resetChat'], { ns: 'share' })}</TooltipContent>

View File

@ -10,10 +10,10 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@langgenius/dify-ui/dropdown-menu'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import * as React from 'react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import ThemeSwitcher from '@/app/components/base/theme-switcher'
import { useWebAppStore } from '@/context/web-app-context'
import { AccessMode } from '@/models/access-control'
@ -51,11 +51,14 @@ const MenuDropdown: FC<Props> = ({ data, placement, hideLogout }) => {
<DropdownMenu>
<DropdownMenuTrigger
render={
<ActionButton size="l" className="data-popup-open:bg-state-base-hover">
<IconButton
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
size="lg"
className="data-popup-open:bg-state-base-hover"
>
<span aria-hidden className="i-ri-equalizer-2-line h-4.5 w-4.5" />
</ActionButton>
</IconButton>
}
aria-label={t(($) => $['operation.more'], { ns: 'common' })}
/>
<DropdownMenuContent
placement={placement || 'bottom-end'}

View File

@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { IconButton } from '@langgenius/dify-ui/icon-button'
import { RiDownloadLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useCSVDownloader } from 'react-papaparse'
import ActionButton from '@/app/components/base/action-button'
type IResDownloadProps = {
isMobile: boolean
@ -28,9 +28,9 @@ const ResDownload: FC<IResDownloadProps> = ({ isMobile, values }) => {
data={values}
>
{isMobile && (
<ActionButton>
<RiDownloadLine className="size-4" />
</ActionButton>
<IconButton aria-label={t(($) => $['operation.download'], { ns: 'common' })}>
<RiDownloadLine aria-hidden="true" className="size-4" />
</IconButton>
)}
{!isMobile && (
<Button>