Compare commits
3 Commits
test/real-
...
codex/icon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bee546e45 | ||
|
|
db20f64ac4 | ||
|
|
87156c1037 |
@@ -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
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Dialog, DialogContent, DialogTrigger } from '@langgenius/dify-ui/dialog
|
||||
import { Drawer, DrawerPopup, DrawerTrigger } from '@langgenius/dify-ui/drawer'
|
||||
import { Field, FieldControl, FieldLabel } from '@langgenius/dify-ui/field'
|
||||
import { Form } from '@langgenius/dify-ui/form'
|
||||
import { IconButton } from '@langgenius/dify-ui/icon-button'
|
||||
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
|
||||
@@ -63,7 +64,7 @@ Keep implementation-only render helpers, context values, styling helpers, and up
|
||||
|
||||
| Category | Subpath | Notes |
|
||||
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
|
||||
| Actions | `./button`, `./icon-button` | Visible-label actions and icon-only commands. |
|
||||
| Controls | `./segmented-control` | SegmentedControl for mode, filter, and view selection. |
|
||||
| Display | `./collapsible`, `./kbd` | Collapsible disclosure primitive; keyboard input and shortcut keycap primitives. |
|
||||
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
|
||||
@@ -90,6 +91,26 @@ documented layout exception.
|
||||
|
||||
When `loading` is true, `Button` defaults `focusableWhenDisabled` to true. Loading represents an action that has already been triggered and is temporarily pending, so the button remains focusable while Base UI still suppresses click, pointer, keyboard activation, and submit-button activation. Pass `focusableWhenDisabled={false}` only when a loading button should use native disabled behavior.
|
||||
|
||||
## Icon button contract
|
||||
|
||||
Use `IconButton` for a command represented by one icon and no visible text. Use `Button` when the control has a visible label, including buttons with leading or trailing icons.
|
||||
|
||||
Pass exactly one React element as its child. React SVG components and CSS icons are both supported without an icon-specific prop or wrapper:
|
||||
|
||||
```tsx
|
||||
<IconButton aria-label="Close">
|
||||
<span aria-hidden="true" className="i-ri-close-line size-4" />
|
||||
</IconButton>
|
||||
```
|
||||
|
||||
Every icon button must have an `aria-label` or `aria-labelledby`; a tooltip is only a visual enhancement. The child chooses the glyph and its optical size. Omit `variant` for the neutral action-button appearance, or use the same appearance variants as `Button`. Use `tone="destructive"` for destructive intent. Size, radius, colors, hover, disabled, and focus-visible styles belong to `IconButton`; limit `className` to external layout.
|
||||
|
||||
`IconButton` preserves Base UI Button's `render`, `nativeButton`, event, and ref composition.
|
||||
|
||||
Do not layer `IconButton` over compound controls that already own button behavior and a specialized recipe, such as Number Field increment/decrement, Combobox clear, or Pagination navigation. Those controls keep their Base UI owner and domain-specific styling.
|
||||
|
||||
Dify-authored icon-only compound parts, such as dialog, drawer, and toast close controls, also keep their own Base UI semantic part. They reuse the private icon-button visual recipe directly instead of rendering the public `IconButton` component, so dependency direction and DOM ownership stay unchanged.
|
||||
|
||||
## Segmented control contract
|
||||
|
||||
`SegmentedControl` is Dify's design-system primitive for mode, filter, and view selection. It is built on Base UI `ToggleGroup` + `Toggle`, so use `Tabs` instead when the UI needs `tablist` / `tabpanel` semantics.
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
"types": "./src/button/index.tsx",
|
||||
"import": "./src/button/index.tsx"
|
||||
},
|
||||
"./icon-button": {
|
||||
"types": "./src/icon-button/index.tsx",
|
||||
"import": "./src/icon-button/index.tsx"
|
||||
},
|
||||
"./checkbox": {
|
||||
"types": "./src/checkbox/index.tsx",
|
||||
"import": "./src/checkbox/index.tsx"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type * as React from 'react'
|
||||
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
|
||||
import { cn } from '../cn'
|
||||
import { iconButtonVariants } from '../icon-button/variants'
|
||||
import { modalBackdropClassName, modalPopupAnimationClassName } from '../overlay-shared'
|
||||
|
||||
const Dialog = BaseDialog.Root
|
||||
@@ -66,11 +67,12 @@ function DialogCloseButton({
|
||||
aria-label={ariaLabel}
|
||||
{...props}
|
||||
className={cn(
|
||||
'absolute inset-e-6 top-6 z-10 flex h-5 w-5 cursor-pointer items-center justify-center rounded-2xl hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
||||
iconButtonVariants({ size: 'sm' }),
|
||||
'absolute inset-e-6 top-6 z-10 rounded-2xl disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-close-line h-4 w-4 text-text-tertiary" />
|
||||
<span aria-hidden="true" className="i-ri-close-line size-4 text-text-tertiary" />
|
||||
</BaseDialog.Close>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import type * as React from 'react'
|
||||
import { Drawer as BaseDrawer } from '@base-ui/react/drawer'
|
||||
import { cn } from '../cn'
|
||||
import { iconButtonVariants } from '../icon-button/variants'
|
||||
|
||||
const Drawer = BaseDrawer.Root
|
||||
const DrawerProvider = BaseDrawer.Provider
|
||||
@@ -119,12 +120,13 @@ function DrawerCloseButton({
|
||||
type={type}
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid disabled:cursor-not-allowed disabled:opacity-50',
|
||||
iconButtonVariants({ size: 'lg' }),
|
||||
'focus-visible:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50 data-disabled:text-text-tertiary',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <span aria-hidden="true" className="i-ri-close-line h-4 w-4" />}
|
||||
{children ?? <span aria-hidden="true" className="i-ri-close-line size-4" />}
|
||||
</BaseDrawer.Close>
|
||||
)
|
||||
}
|
||||
|
||||
73
packages/dify-ui/src/icon-button/__tests__/index.spec.tsx
Normal file
73
packages/dify-ui/src/icon-button/__tests__/index.spec.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { userEvent } from 'vite-plus/test/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { IconButton } from '../index'
|
||||
|
||||
describe('IconButton', () => {
|
||||
it('renders a named native button by default', async () => {
|
||||
const screen = await render(
|
||||
<IconButton aria-label="Close">
|
||||
<span aria-hidden="true" className="i-ri-close-line size-4" />
|
||||
</IconButton>,
|
||||
)
|
||||
|
||||
await expect
|
||||
.element(screen.getByRole('button', { name: 'Close' }))
|
||||
.toHaveAttribute('type', 'button')
|
||||
})
|
||||
|
||||
it('preserves Base UI render prop composition', async () => {
|
||||
const onClick = vi.fn()
|
||||
const onRenderedClick = vi.fn()
|
||||
let renderedRef: HTMLButtonElement | null = null
|
||||
let iconButtonRef: HTMLElement | null = null
|
||||
|
||||
const screen = await render(
|
||||
<IconButton
|
||||
aria-label="More actions"
|
||||
onClick={onClick}
|
||||
ref={(element) => {
|
||||
iconButtonRef = element
|
||||
}}
|
||||
render={
|
||||
<button
|
||||
data-trigger="menu"
|
||||
onClick={onRenderedClick}
|
||||
ref={(element) => {
|
||||
renderedRef = element
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-more-line size-4" />
|
||||
</IconButton>,
|
||||
)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'More actions' })
|
||||
await expect.element(button).toHaveAttribute('data-trigger', 'menu')
|
||||
|
||||
await userEvent.click(button)
|
||||
|
||||
expect(onClick).toHaveBeenCalledOnce()
|
||||
expect(onRenderedClick).toHaveBeenCalledOnce()
|
||||
expect(iconButtonRef).toBe(button.element())
|
||||
expect(renderedRef).toBe(button.element())
|
||||
})
|
||||
|
||||
it('preserves Base UI disabled behavior', async () => {
|
||||
const onClick = vi.fn()
|
||||
const screen = await render(
|
||||
<IconButton aria-label="Delete" disabled onClick={onClick}>
|
||||
<span aria-hidden="true" className="i-ri-delete-bin-line size-4" />
|
||||
</IconButton>,
|
||||
)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Delete' })
|
||||
await expect.element(button).toBeDisabled()
|
||||
|
||||
const element = button.element()
|
||||
if (!(element instanceof HTMLButtonElement))
|
||||
throw new TypeError('Expected IconButton to render a button element')
|
||||
element.click()
|
||||
expect(onClick).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
138
packages/dify-ui/src/icon-button/index.stories.tsx
Normal file
138
packages/dify-ui/src/icon-button/index.stories.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import { IconButton } from '.'
|
||||
|
||||
const meta = {
|
||||
title: 'Base/UI/IconButton',
|
||||
component: IconButton,
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Icon-only command button built on Base UI Button. Provide one icon element and an accessible name.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: [
|
||||
'default',
|
||||
'primary',
|
||||
'secondary',
|
||||
'secondary-accent',
|
||||
'tertiary',
|
||||
'ghost',
|
||||
'ghost-accent',
|
||||
],
|
||||
},
|
||||
tone: {
|
||||
control: 'select',
|
||||
options: ['default', 'destructive'],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['xs', 'sm', 'md', 'lg', 'xl'],
|
||||
},
|
||||
disabled: { control: 'boolean' },
|
||||
},
|
||||
args: {
|
||||
'aria-label': 'Information',
|
||||
children: <span aria-hidden="true" className="i-ri-information-2-line size-4" />,
|
||||
},
|
||||
} satisfies Meta<typeof IconButton>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {}
|
||||
|
||||
export const CSSIcon: Story = {
|
||||
args: {
|
||||
'aria-label': 'Close',
|
||||
children: <span aria-hidden="true" className="i-ri-close-line size-4" />,
|
||||
},
|
||||
}
|
||||
|
||||
export const ReactSVGIcon: Story = {
|
||||
args: {
|
||||
'aria-label': 'Add',
|
||||
children: (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="size-4"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M8 3v10M3 8h10" strokeLinecap="round" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
export const DestructiveIntent: Story = {
|
||||
args: {
|
||||
'aria-label': 'Delete',
|
||||
tone: 'destructive',
|
||||
children: <span aria-hidden="true" className="i-ri-delete-bin-line size-4" />,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Destructive intent appears on hover while the resting action remains neutral.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Sizes: Story = {
|
||||
render: () => {
|
||||
const sizes = [
|
||||
{ button: 'xs', icon: 'size-3.5' },
|
||||
{ button: 'sm', icon: 'size-4' },
|
||||
{ button: 'md', icon: 'size-4' },
|
||||
{ button: 'lg', icon: 'size-4' },
|
||||
{ button: 'xl', icon: 'size-5' },
|
||||
] as const
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{sizes.map(({ button, icon }) => (
|
||||
<IconButton key={button} aria-label={`${button} icon button`} size={button}>
|
||||
<span aria-hidden="true" className={`i-ri-information-2-line ${icon}`} />
|
||||
</IconButton>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
export const Appearances: Story = {
|
||||
render: () => (
|
||||
<div className="flex items-center gap-3">
|
||||
{(
|
||||
[
|
||||
'default',
|
||||
'primary',
|
||||
'secondary',
|
||||
'secondary-accent',
|
||||
'tertiary',
|
||||
'ghost',
|
||||
'ghost-accent',
|
||||
] as const
|
||||
).map((variant) => (
|
||||
<IconButton key={variant} aria-label={`${variant} icon button`} variant={variant}>
|
||||
<span aria-hidden="true" className="i-ri-information-2-line size-4" />
|
||||
</IconButton>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}
|
||||
52
packages/dify-ui/src/icon-button/index.tsx
Normal file
52
packages/dify-ui/src/icon-button/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
|
||||
import type { Button as BaseButtonNS } from '@base-ui/react/button'
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
import type * as React from 'react'
|
||||
import { Button as BaseButton } from '@base-ui/react/button'
|
||||
import { cn } from '../cn'
|
||||
import { iconButtonVariants } from './variants'
|
||||
|
||||
type AccessibleName =
|
||||
| {
|
||||
'aria-label': string
|
||||
'aria-labelledby'?: never
|
||||
}
|
||||
| {
|
||||
'aria-label'?: never
|
||||
'aria-labelledby': string
|
||||
}
|
||||
|
||||
type IconButtonProps = Omit<
|
||||
BaseButtonNS.Props,
|
||||
'aria-label' | 'aria-labelledby' | 'children' | 'className'
|
||||
> &
|
||||
AccessibleName &
|
||||
VariantProps<typeof iconButtonVariants> & {
|
||||
children: React.ReactElement
|
||||
className?: string
|
||||
}
|
||||
|
||||
function IconButton({
|
||||
className,
|
||||
variant,
|
||||
tone,
|
||||
size,
|
||||
type = 'button',
|
||||
children,
|
||||
...props
|
||||
}: IconButtonProps) {
|
||||
return (
|
||||
<BaseButton
|
||||
type={type}
|
||||
className={cn(iconButtonVariants({ variant, tone, size }), className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</BaseButton>
|
||||
)
|
||||
}
|
||||
|
||||
export { IconButton }
|
||||
|
||||
export type { IconButtonProps }
|
||||
111
packages/dify-ui/src/icon-button/variants.ts
Normal file
111
packages/dify-ui/src/icon-button/variants.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
const iconButtonVariants = cva(
|
||||
[
|
||||
'inline-flex cursor-pointer items-center justify-center focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden data-disabled:cursor-not-allowed',
|
||||
],
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: [
|
||||
'text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
'data-disabled:text-text-disabled data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
|
||||
],
|
||||
primary: [
|
||||
'bg-components-button-primary-bg text-components-button-primary-text shadow-primary-button inset-ring-[0.5px] inset-ring-components-button-primary-border',
|
||||
'hover:bg-components-button-primary-bg-hover hover:shadow-xs hover:shadow-shadow-shadow-3 hover:inset-ring-components-button-primary-border-hover',
|
||||
'data-disabled:bg-components-button-primary-bg-disabled data-disabled:text-components-button-primary-text-disabled data-disabled:shadow-none data-disabled:inset-ring-components-button-primary-border-disabled',
|
||||
],
|
||||
secondary: [
|
||||
'bg-components-button-secondary-bg text-components-button-secondary-text shadow-xs inset-ring-[0.5px] shadow-shadow-shadow-3 inset-ring-components-button-secondary-border backdrop-blur-[5px]',
|
||||
'hover:bg-components-button-secondary-bg-hover hover:inset-ring-components-button-secondary-border-hover',
|
||||
'data-disabled:bg-components-button-secondary-bg-disabled data-disabled:text-components-button-secondary-text-disabled data-disabled:shadow-none data-disabled:inset-ring-components-button-secondary-border-disabled data-disabled:backdrop-blur-xs',
|
||||
],
|
||||
'secondary-accent': [
|
||||
'bg-components-button-secondary-bg text-components-button-secondary-accent-text shadow-xs inset-ring-[0.5px] shadow-shadow-shadow-3 inset-ring-components-button-secondary-border backdrop-blur-[5px]',
|
||||
'hover:bg-components-button-secondary-bg-hover hover:inset-ring-components-button-secondary-border-hover',
|
||||
'data-disabled:bg-components-button-secondary-bg-disabled data-disabled:text-components-button-secondary-accent-text-disabled data-disabled:shadow-none data-disabled:inset-ring-components-button-secondary-border-disabled data-disabled:backdrop-blur-xs',
|
||||
],
|
||||
tertiary: [
|
||||
'bg-components-button-tertiary-bg text-components-button-tertiary-text',
|
||||
'hover:bg-components-button-tertiary-bg-hover',
|
||||
'data-disabled:bg-components-button-tertiary-bg-disabled data-disabled:text-components-button-tertiary-text-disabled',
|
||||
],
|
||||
ghost: [
|
||||
'text-components-button-ghost-text',
|
||||
'hover:bg-components-button-ghost-bg-hover',
|
||||
'data-disabled:text-components-button-ghost-text-disabled',
|
||||
],
|
||||
'ghost-accent': [
|
||||
'text-components-button-secondary-accent-text',
|
||||
'hover:bg-state-accent-hover',
|
||||
'data-disabled:text-components-button-secondary-accent-text-disabled',
|
||||
],
|
||||
},
|
||||
tone: {
|
||||
default: '',
|
||||
destructive: '',
|
||||
},
|
||||
size: {
|
||||
xs: 'size-4 rounded-sm p-0',
|
||||
sm: 'size-5 rounded-md',
|
||||
md: 'size-6 rounded-md p-0.5',
|
||||
lg: 'size-8 rounded-lg p-1.5',
|
||||
xl: 'size-9 rounded-lg p-2',
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
variant: 'default',
|
||||
tone: 'destructive',
|
||||
class: [
|
||||
'text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive',
|
||||
'data-disabled:text-text-disabled data-disabled:hover:bg-transparent data-disabled:hover:text-text-disabled',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'primary',
|
||||
tone: 'destructive',
|
||||
class: [
|
||||
'bg-components-button-destructive-primary-bg text-components-button-destructive-primary-text inset-ring-components-button-destructive-primary-border',
|
||||
'hover:bg-components-button-destructive-primary-bg-hover hover:inset-ring-components-button-destructive-primary-border-hover',
|
||||
'data-disabled:bg-components-button-destructive-primary-bg-disabled data-disabled:text-components-button-destructive-primary-text-disabled data-disabled:shadow-none data-disabled:inset-ring-components-button-destructive-primary-bg-disabled',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'secondary',
|
||||
tone: 'destructive',
|
||||
class: [
|
||||
'bg-components-button-destructive-secondary-bg text-components-button-destructive-secondary-text inset-ring-components-button-destructive-secondary-border',
|
||||
'hover:bg-components-button-destructive-secondary-bg-hover hover:inset-ring-components-button-destructive-secondary-border-hover',
|
||||
'data-disabled:text-components-button-destructive-secondary-text-disabled',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'tertiary',
|
||||
tone: 'destructive',
|
||||
class: [
|
||||
'bg-components-button-destructive-tertiary-bg text-components-button-destructive-tertiary-text',
|
||||
'hover:bg-components-button-destructive-tertiary-bg-hover',
|
||||
'data-disabled:bg-components-button-destructive-tertiary-bg-disabled data-disabled:text-components-button-destructive-tertiary-text-disabled',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'ghost',
|
||||
tone: 'destructive',
|
||||
class: [
|
||||
'text-components-button-destructive-ghost-text',
|
||||
'hover:bg-components-button-destructive-ghost-bg-hover',
|
||||
'data-disabled:text-components-button-destructive-ghost-text-disabled',
|
||||
],
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
tone: 'default',
|
||||
size: 'md',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export { iconButtonVariants }
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
ContextMenuSeparator,
|
||||
ContextMenuTrigger,
|
||||
} from '../context-menu'
|
||||
import { IconButton } from '../icon-button'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../tooltip'
|
||||
|
||||
const meta = {
|
||||
@@ -169,13 +170,9 @@ export const InTooltip: Story = {
|
||||
<Tooltip open>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Collapse sidebar"
|
||||
className="inline-flex size-8 items-center justify-center rounded-lg border border-divider-subtle bg-components-button-secondary-bg text-text-secondary shadow-xs focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
>
|
||||
<IconButton aria-label="Collapse sidebar" size="lg" variant="secondary">
|
||||
<span aria-hidden className="i-ri-sidebar-fold-line size-4" />
|
||||
</button>
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
<TooltipContent className="flex items-center gap-1">
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
import type * as React from 'react'
|
||||
import { Toast as BaseToast } from '@base-ui/react/toast'
|
||||
import { cn } from '../cn'
|
||||
import { iconButtonVariants } from '../icon-button/variants'
|
||||
|
||||
type ToastData = Record<string, never>
|
||||
type ToastToneStyle = {
|
||||
@@ -234,10 +235,11 @@ function ToastCard({ toast: toastItem }: { toast: ToastObject<ToastData> }) {
|
||||
<BaseToast.Close
|
||||
aria-label={toastCloseLabel}
|
||||
className={cn(
|
||||
'flex h-5 w-5 items-center justify-center rounded-md hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden disabled:cursor-not-allowed disabled:opacity-50',
|
||||
iconButtonVariants({ size: 'sm' }),
|
||||
'focus-visible:bg-state-base-hover disabled:cursor-not-allowed disabled:opacity-50',
|
||||
)}
|
||||
>
|
||||
<span aria-hidden="true" className="i-ri-close-line h-4 w-4 text-text-tertiary" />
|
||||
<span aria-hidden="true" className="i-ri-close-line size-4 text-text-tertiary" />
|
||||
</BaseToast.Close>
|
||||
</div>
|
||||
</BaseToast.Content>
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
|
||||
import type { TooltipContentProps } from '.'
|
||||
import * as React from 'react'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '.'
|
||||
import { IconButton as DifyIconButton } from '../icon-button'
|
||||
|
||||
const iconButtonClassName =
|
||||
'inline-flex h-8 w-8 items-center justify-center rounded-lg border border-divider-subtle bg-components-button-secondary-bg text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
|
||||
const triggerButtonClassName =
|
||||
'rounded-lg border border-divider-subtle bg-components-button-secondary-bg px-3 py-1.5 text-sm text-text-secondary shadow-xs outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid'
|
||||
|
||||
@@ -56,9 +55,9 @@ export const IconButton: Story = {
|
||||
<Tooltip key={label}>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<button type="button" aria-label={label} className={iconButtonClassName}>
|
||||
<span aria-hidden className={`${icon} h-4 w-4`} />
|
||||
</button>
|
||||
<DifyIconButton aria-label={label} size="lg" variant="secondary">
|
||||
<span aria-hidden className={`${icon} size-4`} />
|
||||
</DifyIconButton>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{label}</TooltipContent>
|
||||
@@ -130,9 +129,9 @@ const PlacementsDemo = () => {
|
||||
<Tooltip open>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<button type="button" aria-label="Placement anchor" className={iconButtonClassName}>
|
||||
<span aria-hidden className="i-ri-pushpin-line h-4 w-4" />
|
||||
</button>
|
||||
<DifyIconButton aria-label="Placement anchor" size="lg" variant="secondary">
|
||||
<span aria-hidden className="i-ri-pushpin-line size-4" />
|
||||
</DifyIconButton>
|
||||
}
|
||||
/>
|
||||
<TooltipContent placement={placement}>{`placement="${placement}"`}</TooltipContent>
|
||||
@@ -167,13 +166,9 @@ const DelayDemo = () => (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
aria-label={`${label} (${delay}ms)`}
|
||||
className={iconButtonClassName}
|
||||
>
|
||||
<span aria-hidden className="i-ri-timer-line h-4 w-4" />
|
||||
</button>
|
||||
<DifyIconButton aria-label={`${label} (${delay}ms)`} size="lg" variant="secondary">
|
||||
<span aria-hidden className="i-ri-timer-line size-4" />
|
||||
</DifyIconButton>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{`${label} (${delay}ms)`}</TooltipContent>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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]">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
<>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user