feat(dify-ui): add textarea primitive

This commit is contained in:
yyh
2026-05-23 17:30:07 +08:00
parent 2a0c098857
commit d5d0d2d96f
58 changed files with 539 additions and 896 deletions

View File

@ -33,6 +33,7 @@ import { Drawer, DrawerPopup, DrawerTrigger } from '@langgenius/dify-ui/drawer'
import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { Form } from '@langgenius/dify-ui/form'
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
import { Textarea } from '@langgenius/dify-ui/textarea'
import '@langgenius/dify-ui/styles.css' // once, in the app root
```
@ -40,16 +41,16 @@ Importing from `@langgenius/dify-ui` (no subpath) is intentionally not supported
## Primitives
| Category | Subpath | Notes |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
| Navigation | `./tabs`, `./toggle-group` | Tabs for panels; ToggleGroup for segmented modes. |
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
| Category | Subpath | Notes |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------- |
| Actions | `./button` | Design-system CTA primitive with `cva` variants. |
| Feedback | `./meter`, `./toast` | Meter is inline status; Toast owns the `z-60` layer. |
| Form | `./form`, `./field`, `./fieldset`, `./input`, `./textarea`, `./checkbox`, `./checkbox-group`, `./radio`, `./radio-group`, `./number-field`, `./select`, `./slider`, `./switch` | Native form boundary, field semantics, and controls. |
| Layout | `./scroll-area` | Custom-styled scrollbar over the host viewport. |
| Media | `./avatar` | Avatar root, image, and fallback primitives. |
| Navigation | `./tabs`, `./toggle-group` | Tabs for panels; ToggleGroup for segmented modes. |
| Overlay / menu | `./alert-dialog`, `./context-menu`, `./dialog`, `./drawer`, `./dropdown-menu`, `./popover`, `./preview-card`, `./tooltip` | Portalled. See [Overlay & portal contract] below. |
| Search / pickers | `./autocomplete`, `./combobox`, `./select` | Search input, searchable picker, and closed picker. |
Utilities:
@ -64,7 +65,7 @@ Use `Form` for the submit boundary. It renders a native `<form>`, preserves Ente
Use `FieldRoot` for each standalone named field. A field must have a stable `name`, a label relationship, and either a `FieldControl` or another control that participates in the same Base UI field context. Prefer a visible label for normal form rows; when the surrounding UI already supplies the visible text, use the matching label primitive visually hidden or put `aria-label` on the actual interactive control. `FieldDescription` and `FieldError` provide the message relationships that screen readers need, while the Dify wrapper adds the default Form Input Set styling from the design system.
Choose the label primitive by the control semantics. Text-like inputs, input-based `Combobox` / `Autocomplete`, single `Checkbox` / `Radio`, `Switch`, and `NumberField` use `FieldLabel`. Trigger-based `Select` fields use `SelectLabel`; `Slider` fields use `SliderLabel`, with per-thumb `aria-label` only when the thumbs need distinct names. `SelectGroupLabel` and `AutocompleteGroupLabel` only label grouped options inside their popup content; they are not field labels.
Choose the label primitive by the control semantics. Text-like inputs, `Textarea`, input-based `Combobox` / `Autocomplete`, single `Checkbox` / `Radio`, `Switch`, and `NumberField` use `FieldLabel`. Trigger-based `Select` fields use `SelectLabel`; `Slider` fields use `SliderLabel`, with per-thumb `aria-label` only when the thumbs need distinct names. `SelectGroupLabel` and `AutocompleteGroupLabel` only label grouped options inside their popup content; they are not field labels.
Use `FieldsetRoot` and `FieldsetLegend` when one field is represented by a group of related controls, such as checkbox groups, radio groups, multi-thumb sliders, or a section that combines several inputs. For checkbox and radio groups, wrap each option with `FieldItem` and give each option its own label:

View File

@ -113,6 +113,10 @@
"types": "./src/tabs/index.tsx",
"import": "./src/tabs/index.tsx"
},
"./textarea": {
"types": "./src/textarea/index.tsx",
"import": "./src/textarea/index.tsx"
},
"./toggle-group": {
"types": "./src/toggle-group/index.tsx",
"import": "./src/toggle-group/index.tsx"

View File

@ -148,7 +148,7 @@ export const AllStates: Story = {
parameters: {
docs: {
description: {
story: 'Complete variant matrix: all sizes × all states, matching Figma design spec (node 2144:1210).',
story: 'Complete variant matrix: all sizes and states.',
},
},
},

View File

@ -0,0 +1,129 @@
import { render } from 'vitest-browser-react'
import {
FieldDescription,
FieldError,
FieldLabel,
FieldRoot,
} from '../../field'
import { Form } from '../../form'
import { Textarea } from '../index'
const asHTMLElement = (element: HTMLElement | SVGElement) => element as HTMLElement
const setTextareaValue = (element: HTMLElement | SVGElement, value: string) => {
const textarea = asHTMLElement(element) as HTMLTextAreaElement
const valueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
valueSetter?.call(textarea, value)
textarea.dispatchEvent(new Event('input', { bubbles: true }))
}
describe('Textarea', () => {
it('should render a labelled textarea through Base UI Field.Control', async () => {
const screen = await render(
<FieldRoot name="description">
<FieldLabel>Description</FieldLabel>
<Textarea defaultValue="A workspace for support automation." />
<FieldDescription>Shown to workspace members.</FieldDescription>
</FieldRoot>,
)
const textarea = screen.getByRole('textbox', { name: 'Description' })
await expect.element(textarea).toHaveValue('A workspace for support automation.')
await expect.element(textarea).toHaveAccessibleDescription('Shown to workspace members.')
await expect.element(textarea).toHaveClass('min-h-20', 'rounded-lg', 'system-sm-regular')
expect(asHTMLElement(textarea.element()).tagName).toBe('TEXTAREA')
})
it('should apply size variants and custom classes', async () => {
const screen = await render(
<label>
Prompt
<Textarea size="large" className="resize-none" />
</label>,
)
await expect.element(screen.getByRole('textbox', { name: 'Prompt' })).toHaveClass(
'rounded-[10px]',
'px-4',
'py-2',
'system-md-regular',
'resize-none',
)
})
it('should call onValueChange and stay controlled until value changes', async () => {
const onValueChange = vi.fn()
const screen = await render(
<label>
Notes
<Textarea value="" onValueChange={onValueChange} />
</label>,
)
const textarea = screen.getByRole('textbox', { name: 'Notes' })
setTextareaValue(textarea.element(), 'a')
expect(onValueChange).toHaveBeenCalledWith('a', expect.any(Object))
await expect.element(textarea).toHaveValue('')
await screen.rerender(
<label>
Notes
<Textarea value="a" onValueChange={onValueChange} />
</label>,
)
await expect.element(screen.getByRole('textbox', { name: 'Notes' })).toHaveValue('a')
})
it('should submit valid values and show validation errors through Base UI Form', async () => {
const onFormSubmit = vi.fn()
const screen = await render(
<Form aria-label="dataset form" onFormSubmit={onFormSubmit}>
<FieldRoot name="summary">
<FieldLabel>Summary</FieldLabel>
<Textarea required minLength={10} />
<FieldError match="valueMissing">Summary is required.</FieldError>
<FieldError match="tooShort">Summary is too short.</FieldError>
</FieldRoot>
<button type="submit">Save</button>
</Form>,
)
const saveButton = asHTMLElement(screen.getByRole('button', { name: 'Save' }).element())
saveButton.click()
await vi.waitFor(async () => {
await expect.element(screen.getByText('Summary is required.')).toBeInTheDocument()
await expect.element(screen.getByRole('textbox', { name: 'Summary' })).toHaveAttribute('aria-invalid', 'true')
})
expect(onFormSubmit).not.toHaveBeenCalled()
await screen.rerender(
<Form aria-label="dataset form" onFormSubmit={onFormSubmit}>
<FieldRoot name="summary">
<FieldLabel>Summary</FieldLabel>
<Textarea key="valid-summary" required minLength={10} defaultValue="Long enough summary" />
<FieldError match="valueMissing">Summary is required.</FieldError>
<FieldError match="tooShort">Summary is too short.</FieldError>
</FieldRoot>
<button type="submit">Save</button>
</Form>,
)
asHTMLElement(screen.getByRole('button', { name: 'Save' }).element()).click()
expect(onFormSubmit).toHaveBeenCalledTimes(1)
expect(onFormSubmit.mock.calls[0]?.[0]).toMatchObject({ summary: 'Long enough summary' })
})
it('should render character count when maxLength is set', async () => {
const screen = await render(
<label>
Release notes
<Textarea defaultValue="Draft" maxLength={20} />
</label>,
)
await expect.element(screen.getByText('5/20')).toBeInTheDocument()
await expect.element(screen.getByRole('textbox', { name: 'Release notes' })).toHaveClass('pb-7')
})
})

View File

@ -0,0 +1,141 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import { Button } from '../button'
import {
FieldDescription,
FieldError,
FieldLabel,
FieldRoot,
} from '../field'
import { Form } from '../form'
import { Textarea } from './index'
const meta = {
title: 'Base/Form/Textarea',
component: Textarea,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Multiline text control built on Base UI Field.Control. Use it with FieldRoot for labelled, described, and validated form fields.',
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof Textarea>
export default meta
type Story = StoryObj<typeof meta>
export const Basic: Story = {
render: () => (
<div className="w-80">
<label htmlFor="workspace-description" className="mb-1 block w-fit py-1 text-text-secondary system-sm-medium">
Workspace description
</label>
<Textarea
id="workspace-description"
name="workspaceDescription"
placeholder="Describe how this workspace is used..."
/>
</div>
),
}
export const Sizes: Story = {
render: () => (
<div className="grid w-80 gap-3">
<label className="grid gap-1 text-text-secondary system-sm-medium" htmlFor="small-textarea">
Small
<Textarea id="small-textarea" size="small" name="smallTextarea" placeholder="Short note..." rows={3} />
</label>
<label className="grid gap-1 text-text-secondary system-sm-medium" htmlFor="medium-textarea">
Medium
<Textarea id="medium-textarea" name="mediumTextarea" placeholder="Add context..." rows={3} />
</label>
<label className="grid gap-1 text-text-secondary system-sm-medium" htmlFor="large-textarea">
Large
<Textarea id="large-textarea" size="large" name="largeTextarea" placeholder="Write a longer instruction..." rows={3} />
</label>
</div>
),
}
export const States: Story = {
render: () => (
<div className="grid w-80 gap-3">
<FieldRoot name="placeholderState">
<FieldLabel>Placeholder</FieldLabel>
<Textarea placeholder="Add a description..." rows={3} />
</FieldRoot>
<FieldRoot name="filledState">
<FieldLabel>Filled</FieldLabel>
<Textarea defaultValue="Use this dataset for support articles and product FAQs." rows={3} />
</FieldRoot>
<FieldRoot name="invalidState" invalid>
<FieldLabel>Invalid</FieldLabel>
<Textarea defaultValue="Too short" rows={3} />
<FieldError match>Use at least 20 characters.</FieldError>
</FieldRoot>
<FieldRoot name="disabledState">
<FieldLabel>Disabled</FieldLabel>
<Textarea disabled placeholder="Editing is unavailable..." rows={3} />
</FieldRoot>
<FieldRoot name="readonlyState">
<FieldLabel>Read-only</FieldLabel>
<Textarea readOnly defaultValue="Generated from the published workflow configuration." rows={3} />
</FieldRoot>
</div>
),
}
export const WithField: Story = {
render: () => (
<Form aria-label="Dataset settings" className="grid w-80 gap-4" onFormSubmit={() => undefined}>
<FieldRoot name="description">
<FieldLabel>Description</FieldLabel>
<Textarea
required
minLength={20}
maxLength={160}
placeholder="Describe what this dataset contains..."
rows={4}
className="resize-y"
/>
<FieldDescription>Shown to teammates when they choose a knowledge source.</FieldDescription>
<FieldError match="valueMissing">Description is required.</FieldError>
<FieldError match="tooShort">Use at least 20 characters.</FieldError>
</FieldRoot>
<div className="flex justify-end">
<Button type="submit" variant="primary">Save Settings</Button>
</div>
</Form>
),
}
const ControlledDemo = () => {
const [value, setValue] = useState('Summarize customer feedback into actionable product themes.')
return (
<FieldRoot name="prompt">
<FieldLabel>Prompt</FieldLabel>
<Textarea
value={value}
onValueChange={setValue}
maxLength={120}
rows={4}
className="resize-y"
/>
<FieldDescription>The saved value is updated from the controlled state.</FieldDescription>
</FieldRoot>
)
}
export const Controlled: Story = {
render: () => (
<div className="w-80">
<ControlledDemo />
</div>
),
}

View File

@ -0,0 +1,119 @@
'use client'
import type { Field as BaseFieldNS } from '@base-ui/react/field'
import type { VariantProps } from 'class-variance-authority'
import type { ComponentPropsWithRef } from 'react'
import { Field as BaseField } from '@base-ui/react/field'
import { cva } from 'class-variance-authority'
import { useState } from 'react'
import { cn } from '../cn'
const textareaVariants = cva(
[
'min-h-20 w-full appearance-none border border-transparent bg-components-input-bg-normal text-components-input-text-filled caret-primary-600 outline-hidden transition-[background-color,border-color,box-shadow]',
'placeholder:text-components-input-text-placeholder',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
'data-invalid:border-components-input-border-destructive data-invalid:bg-components-input-bg-destructive',
'read-only:cursor-default read-only:shadow-none read-only:hover:border-transparent read-only:hover:bg-components-input-bg-normal read-only:focus:border-transparent read-only:focus:bg-components-input-bg-normal read-only:focus:shadow-none',
'disabled:cursor-not-allowed disabled:border-transparent disabled:bg-components-input-bg-disabled disabled:text-components-input-text-filled-disabled',
'disabled:hover:border-transparent disabled:hover:bg-components-input-bg-disabled',
'motion-reduce:transition-none',
],
{
variants: {
size: {
small: 'rounded-md px-2 py-1 system-xs-regular',
medium: 'rounded-lg px-3 py-2 system-sm-regular',
large: 'rounded-[10px] px-4 py-2 system-md-regular',
},
hasCount: {
true: 'pb-7',
false: '',
},
},
defaultVariants: {
size: 'medium',
hasCount: false,
},
},
)
type TextareaValue = string | number
type ControlledTextareaProps = {
value: TextareaValue
defaultValue?: never
}
type UncontrolledTextareaProps = {
value?: never
defaultValue?: TextareaValue
}
type NativeTextareaProps = Omit<
ComponentPropsWithRef<'textarea'>,
'children' | 'className' | 'defaultValue' | 'onChange' | 'size' | 'value'
>
type TextareaControlProps = ControlledTextareaProps | UncontrolledTextareaProps
type TextareaVariantProps = Omit<VariantProps<typeof textareaVariants>, 'hasCount'>
export type TextareaSize = NonNullable<VariantProps<typeof textareaVariants>['size']>
export type TextareaChangeEventDetails = BaseFieldNS.Control.ChangeEventDetails
export type TextareaProps
= NativeTextareaProps
& TextareaControlProps
& TextareaVariantProps
& {
children?: never
className?: string
onValueChange?: (value: string, eventDetails: TextareaChangeEventDetails) => void
}
function getTextareaValueLength(value: TextareaValue | undefined) {
return String(value ?? '').length
}
export function Textarea({
className,
defaultValue,
maxLength,
onValueChange,
ref,
size = 'medium',
value,
...props
}: TextareaProps) {
const [uncontrolledValue, setUncontrolledValue] = useState<TextareaValue | undefined>(defaultValue)
const valueLength = getTextareaValueLength(value ?? uncontrolledValue)
const hasCount = maxLength !== undefined
return (
<div className="relative w-full">
<BaseField.Control
className={cn(textareaVariants({ hasCount, size }), className)}
defaultValue={defaultValue}
maxLength={maxLength}
onValueChange={(nextValue, eventDetails) => {
if (value === undefined)
setUncontrolledValue(nextValue)
onValueChange?.(nextValue, eventDetails)
}}
render={<textarea {...props} ref={ref} />}
value={value}
/>
{hasCount
? (
<span className="pointer-events-none absolute right-2 bottom-2 rounded-sm bg-components-panel-bg px-1 py-0.5 text-text-tertiary system-2xs-medium">
{valueLength}
/
{maxLength}
</span>
)
: null}
</div>
)
}

View File

@ -1,10 +1,10 @@
'use client'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'
import { useAppContext } from '@/context/app-context'
import { useRouter } from '@/next/navigation'
import { useLogout } from '@/service/use-common'
@ -66,8 +66,8 @@ export default function FeedBack(props: DeleteAccountProps) {
rows={6}
value={userFeedback}
placeholder={t('account.feedbackPlaceholder', { ns: 'common' }) as string}
onChange={(e) => {
setUserFeedback(e.target.value)
onValueChange={(value) => {
setUserFeedback(value)
}}
/>
<div className="mt-3 flex w-full flex-col gap-2">

View File

@ -1,9 +1,9 @@
'use client'
import type { FC } from 'react'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
import Textarea from '@/app/components/base/textarea'
export enum EditItemType {
Query = 'query',
@ -34,7 +34,7 @@ const EditItem: FC<Props> = ({
<div className="mb-1 system-xs-semibold text-text-primary">{name}</div>
<Textarea
value={content}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange(e.target.value)}
onValueChange={value => onChange(value)}
placeholder={placeholder}
autoFocus
/>

View File

@ -2,12 +2,12 @@
import type { FC } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { RiDeleteBinLine, RiEditFill, RiEditLine } from '@remixicon/react'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Robot, User } from '@/app/components/base/icons/src/public/avatar'
import Textarea from '@/app/components/base/textarea'
export enum EditItemType {
Query = 'query',
@ -131,7 +131,7 @@ const EditItem: FC<Props> = ({
<EditTitle title={editTitle} />
<Textarea
value={newContent}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setNewContent(e.target.value)}
onValueChange={value => setNewContent(value)}
placeholder={placeholder}
autoFocus
/>

View File

@ -3,12 +3,12 @@ import type { VersionHistory } from '@/types/workflow'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { FieldControl, FieldLabel, FieldRoot } from '@langgenius/dify-ui/field'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Textarea from '../../base/textarea'
type VersionInfoModalProps = {
isOpen: boolean
@ -57,8 +57,8 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
onClose()
}
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setReleaseNotes(e.target.value)
const handleDescriptionChange = useCallback((value: string) => {
setReleaseNotes(value)
}, [])
return (
@ -95,17 +95,16 @@ const VersionInfoModal: FC<VersionInfoModalProps> = ({
onValueChange={setTitle}
/>
</FieldRoot>
<div className="flex flex-col gap-y-1">
<div className="flex h-6 items-center system-sm-semibold text-text-secondary">
<FieldRoot name="releaseNotes" invalid={releaseNotesError} className="gap-y-1">
<FieldLabel className="flex h-6 items-center py-0 system-sm-semibold text-text-secondary">
{t('versionHistory.editField.releaseNotes', { ns: 'workflow' })}
</div>
</FieldLabel>
<Textarea
value={releaseNotes}
placeholder={`${t('versionHistory.releaseNotesPlaceholder', { ns: 'workflow' })}${t('panel.optional', { ns: 'workflow' })}`}
onChange={handleDescriptionChange}
destructive={releaseNotesError}
onValueChange={handleDescriptionChange}
/>
</div>
</FieldRoot>
</div>
<div className="flex justify-end p-6 pt-5">
<div className="flex items-center gap-x-3">

View File

@ -13,12 +13,12 @@ import {
SelectTrigger,
SelectValue,
} from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { Trans } from 'react-i18next'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { Infotip } from '@/app/components/base/infotip'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -122,7 +122,7 @@ const ConfigModalFormFields: FC<ConfigModalFormFieldsProps> = ({
<Field title={t('variableConfig.defaultValue', { ns: 'appDebug' })}>
<Textarea
value={String(tempPayload.default ?? '')}
onChange={e => onPayloadChange('default')(e.target.value || undefined)}
onValueChange={value => onPayloadChange('default')(value || undefined)}
placeholder={t('variableConfig.inputPlaceholder', { ns: 'appDebug' })}
/>
</Field>

View File

@ -1,11 +1,11 @@
'use client'
import type { FC } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { useBoolean } from 'ahooks'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import Textarea from '@/app/components/base/textarea'
const i18nPrefix = 'generate'
@ -43,7 +43,7 @@ const IdeaOutput: FC<Props> = ({
className="h-[80px]"
placeholder={t(`${i18nPrefix}.idealOutputPlaceholder`, { ns: 'appDebug' })}
value={value}
onChange={e => onChange(e.target.value)}
onValueChange={value => onChange(value)}
/>
)}
</div>

View File

@ -4,13 +4,13 @@ import type { DataSet } from '@/models/datasets'
import type { RetrievalConfig } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import { isEqual } from 'es-toolkit/predicate'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model'
import { IndexingType } from '@/app/components/datasets/create/step-two'
import IndexMethod from '@/app/components/datasets/settings/index-method'
@ -225,7 +225,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
<div className="w-full">
<Textarea
value={localeCurrentDataset.description || ''}
onChange={e => handleValueChange('description', e.target.value)}
onValueChange={value => handleValueChange('description', value)}
className="resize-none"
placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''}
/>

View File

@ -84,25 +84,6 @@ vi.mock('@langgenius/dify-ui/select', async () => {
}
})
vi.mock('@/app/components/base/textarea', () => ({
default: ({ value, onChange, placeholder, readOnly, className }: {
value: string
onChange: (e: { target: { value: string } }) => void
placeholder?: string
readOnly?: boolean
className?: string
}) => (
<textarea
data-testid={`textarea-${placeholder}`}
value={value}
onChange={onChange}
placeholder={placeholder}
readOnly={readOnly}
className={className}
/>
),
}))
vi.mock('@/app/components/workflow/nodes/_base/components/before-run-form/bool-input', () => ({
default: ({ name, value, required, onChange, readonly }: {
name: string
@ -223,7 +204,7 @@ describe('ChatUserInput', () => {
}))
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('textarea-Description')).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument()
})
it('should render select input type', () => {
@ -275,7 +256,7 @@ describe('ChatUserInput', () => {
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('input-Name')).toBeInTheDocument()
expect(screen.getByTestId('textarea-Description')).toBeInTheDocument()
expect(screen.getByRole('textbox', { name: 'Description' })).toBeInTheDocument()
expect(screen.getByTestId('select-input')).toBeInTheDocument()
})
@ -334,7 +315,7 @@ describe('ChatUserInput', () => {
}))
render(<ChatUserInput inputs={{ desc: 'Long text here' }} />)
expect(screen.getByTestId('textarea-Description')).toHaveValue('Long text here')
expect(screen.getByRole('textbox', { name: 'Description' })).toHaveValue('Long text here')
})
it('should display existing input values for number type', () => {
@ -418,7 +399,7 @@ describe('ChatUserInput', () => {
}))
render(<ChatUserInput inputs={{}} />)
fireEvent.change(screen.getByTestId('textarea-Description'), { target: { value: 'New Description' } })
fireEvent.change(screen.getByRole('textbox', { name: 'Description' }), { target: { value: 'New Description' } })
expect(mockSetInputs).toHaveBeenCalledWith({ desc: 'New Description' })
})
@ -526,7 +507,7 @@ describe('ChatUserInput', () => {
}))
render(<ChatUserInput inputs={{}} />)
expect(screen.getByTestId('textarea-Description')).toHaveAttribute('readonly')
expect(screen.getByRole('textbox', { name: 'Description' })).toHaveAttribute('readonly')
})
it('should disable select when readonly is true', () => {

View File

@ -1,12 +1,12 @@
import type { Inputs } from '@/models/debug'
import { cn } from '@langgenius/dify-ui/cn'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import ConfigContext from '@/context/debug-configuration'
@ -94,9 +94,10 @@ const ChatUserInput = ({
{type === 'paragraph' && (
<Textarea
className="h-[120px] grow"
aria-label={name || key}
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
onValueChange={(value) => { handleInputValueChange(key, value) }}
readOnly={readonly}
/>
)}

View File

@ -5,6 +5,7 @@ import type { VisionFile, VisionSettings } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import {
RiArrowDownSLine,
@ -19,7 +20,6 @@ import { useStore as useAppStore } from '@/app/components/app/store'
import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import ConfigContext from '@/context/debug-configuration'
import { AppModeEnum, ModelModeType } from '@/types/app'
@ -154,7 +154,7 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
className="h-[120px] grow"
placeholder={name}
value={inputs[key] ? `${inputs[key]}` : ''}
onChange={(e) => { handleInputValueChange(key, e.target.value) }}
onValueChange={(value) => { handleInputValueChange(key, value) }}
readOnly={readonly}
/>
)}

View File

@ -4,6 +4,7 @@ import type { AppIconSelection } from '../../base/app-icon-picker'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { RiArrowRightLine, RiArrowRightSLine, RiExchange2Fill } from '@remixicon/react'
import { useDebounceFn, useKeyPress } from 'ahooks'
@ -13,7 +14,6 @@ import AppIcon from '@/app/components/base/app-icon'
import Divider from '@/app/components/base/divider'
import { BubbleTextMod, ChatBot, ListSparkle, Logic } from '@/app/components/base/icons/src/vender/solid/communication'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
import { useAppContext } from '@/context/app-context'
@ -243,7 +243,7 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
className="resize-none"
placeholder={t('newApp.appDescriptionPlaceholder', { ns: 'app' }) || ''}
value={description}
onChange={e => setDescription(e.target.value)}
onValueChange={value => setDescription(value)}
/>
</div>
</div>

View File

@ -8,6 +8,7 @@ import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import * as React from 'react'
@ -18,7 +19,6 @@ import AppIconPicker from '@/app/components/base/app-icon-picker'
import Divider from '@/app/components/base/divider'
import Input from '@/app/components/base/input'
import { PremiumBadgeButton } from '@/app/components/base/premium-badge'
import Textarea from '@/app/components/base/textarea'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
@ -291,7 +291,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<Textarea
className="mt-1"
value={inputInfo.desc}
onChange={e => onDesChange(e.target.value)}
onValueChange={onDesChange}
placeholder={t(`${prefixSettings}.webDescPlaceholder`, { ns: 'appOverview' }) as string}
/>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>{t(`${prefixSettings}.webDescTip`, { ns: 'appOverview' })}</p>
@ -466,7 +466,7 @@ const SettingsModal: FC<ISettingsModalProps> = ({
<Textarea
className="mt-1"
value={inputInfo.customDisclaimer}
onChange={onChange('customDisclaimer')}
onValueChange={value => setInputInfo(item => ({ ...item, customDisclaimer: value }))}
placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`, { ns: 'appOverview' }) as string}
/>
</div>

View File

@ -7,8 +7,8 @@ import {
SelectTrigger,
SelectValue,
} from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { InputVarType } from '@/app/components/workflow/types'
@ -74,7 +74,7 @@ const WorkflowHiddenInputFields = ({
<Textarea
id={fieldId}
value={typeof fieldValue === 'string' ? fieldValue : ''}
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => onValueChange(variable.variable, event.target.value)}
onValueChange={value => onValueChange(variable.variable, value)}
placeholder={label}
maxLength={variable.max_length}
className="min-h-24"

View File

@ -1,10 +1,10 @@
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -72,7 +72,7 @@ const InputsFormContent = ({ showTip }: Props) => {
{form.type === InputVarType.paragraph && (
<Textarea
value={inputsFormValue?.[form.variable] || ''}
onChange={e => handleFormChange(form.variable, e.target.value)}
onValueChange={value => handleFormChange(form.variable, value)}
placeholder={form.label}
/>
)}

View File

@ -1,8 +1,8 @@
import type { ContentItemProps } from './type'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useMemo } from 'react'
import { Markdown } from '@/app/components/base/markdown'
import Textarea from '@/app/components/base/textarea'
const ContentItem = ({
content,
@ -44,7 +44,7 @@ const ContentItem = ({
<Textarea
className="h-[104px] sm:text-xs"
value={inputs[fieldName]!}
onChange={(e) => { onInputChange(fieldName, e.target.value) }}
onValueChange={(value) => { onInputChange(fieldName, value) }}
data-testid="content-item-textarea"
/>
)}

View File

@ -6,6 +6,7 @@ import type {
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogDescription, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import copy from 'copy-to-clipboard'
@ -21,7 +22,6 @@ import ActionButton, { ActionButtonState } from '@/app/components/base/action-bu
import Log from '@/app/components/base/chat/chat/log'
import AnnotationCtrlButton from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-button'
import NewAudioButton from '@/app/components/base/new-audio-button'
import Textarea from '@/app/components/base/textarea'
import { useChatContext } from '../context'
type OperationProps = {
@ -394,7 +394,7 @@ function Operation({
id={feedbackTextareaId}
name="feedback-content"
value={feedbackContent}
onChange={e => setFeedbackContent(e.target.value)}
onValueChange={value => setFeedbackContent(value)}
placeholder={t('feedback.placeholder', { ns: 'common' }) || 'Please describe what went wrong or how we can improve…'}
rows={4}
className="w-full"

View File

@ -1,10 +1,10 @@
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -72,7 +72,7 @@ const InputsFormContent = ({ showTip }: Props) => {
{form.type === InputVarType.paragraph && (
<Textarea
value={inputsFormValue?.[form.variable] || ''}
onChange={e => handleFormChange(form.variable, e.target.value)}
onValueChange={value => handleFormChange(form.variable, value)}
placeholder={form.label}
/>
)}

View File

@ -12,10 +12,10 @@ import { FieldItem, FieldRoot } from '@langgenius/dify-ui/field'
import { FieldsetLegend, FieldsetRoot } from '@langgenius/dify-ui/fieldset'
import { RadioControl, RadioRoot } from '@langgenius/dify-ui/radio'
import { RadioGroup } from '@langgenius/dify-ui/radio-group'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { produce } from 'immer'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@ -222,7 +222,7 @@ const FollowUpSettingModal = ({
<Textarea
className="mt-3 min-h-32 resize-y border-components-input-border-active bg-components-input-bg-normal"
value={prompt}
onChange={e => setPrompt(e.target.value)}
onValueChange={value => setPrompt(value)}
maxLength={CUSTOM_FOLLOW_UP_PROMPT_MAX_LENGTH}
placeholder={t('feature.suggestedQuestionsAfterAnswer.modal.promptPlaceholder', { ns: 'appDebug' }) || ''}
/>

View File

@ -2,7 +2,7 @@ import type { FC } from 'react'
import type { CodeBasedExtensionForm } from '@/models/common'
import type { ModerationConfig } from '@/models/debug'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import Textarea from '@/app/components/base/textarea'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { useLocale } from '@/context/i18n'
type FormGenerationProps = {
@ -58,7 +58,7 @@ const FormGeneration: FC<FormGenerationProps> = ({
className="resize-none"
value={value?.[form.variable] || ''}
placeholder={form.placeholder}
onChange={e => handleFormChange(form.variable, e.target.value)}
onValueChange={value => handleFormChange(form.variable, value)}
/>
</div>
)

View File

@ -1,16 +1,16 @@
import type { TextareaProps } from '../../../textarea'
import type { TextareaProps } from '@langgenius/dify-ui/textarea'
import type { LabelProps } from '../label'
import { cn } from '@langgenius/dify-ui/cn'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useFieldContext } from '../..'
import Textarea from '../../../textarea'
import Label from '../label'
type TextAreaFieldProps = {
label: string
labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
className?: string
} & Omit<TextareaProps, 'className' | 'onChange' | 'onBlur' | 'value' | 'id'>
} & Omit<TextareaProps, 'className' | 'defaultValue' | 'onBlur' | 'value' | 'id'>
const TextAreaField = ({
label,
@ -30,7 +30,7 @@ const TextAreaField = ({
<Textarea
id={field.name}
value={field.state.value}
onChange={e => field.handleChange(e.target.value)}
onValueChange={value => field.handleChange(value)}
onBlur={field.handleBlur}
{...inputProps}
/>

View File

@ -3,6 +3,7 @@ import type { Dayjs } from 'dayjs'
import { Button } from '@langgenius/dify-ui/button'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger, SelectValue } from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useCallback, useMemo, useState } from 'react'
import { useChatContext } from '@/app/components/base/chat/chat/context'
@ -10,7 +11,6 @@ import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
import { formatDateForOutput, toDayjs } from '@/app/components/base/date-and-time-picker/utils/dayjs'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
const DATA_FORMAT = {
TEXT: 'text',
@ -376,7 +376,7 @@ const MarkdownForm = ({ node }: { node: HastElement }) => {
name={name}
placeholder={str(child.properties.placeholder)}
value={str(formValues[name])}
onChange={e => updateValue(name, e.target.value)}
onValueChange={value => updateValue(name, value)}
/>
)
}

View File

@ -2,12 +2,12 @@
import type { FC } from 'react'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useCallback, useEffect, useState } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import { VarType } from '@/app/components/workflow/types'
import Textarea from '../../../textarea'
import TagLabel from './tag-label'
import TypeSwitch from './type-switch'
@ -129,7 +129,7 @@ const PrePopulate: FC<Props> = ({
<Textarea
value={value || ''}
className="h-[43px] min-h-[43px] rounded-none border-none bg-transparent px-3 hover:bg-transparent focus:bg-transparent focus:shadow-none"
onChange={e => onValueChange?.(e.target.value)}
onValueChange={value => onValueChange?.(value)}
onFocus={() => {
setOnPlaceholderClicked(true)
setIsFocus(true)

View File

@ -1,77 +0,0 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import TextArea from '../index'
describe('TextArea', () => {
it('should render correctly with default props', () => {
render(<TextArea value="" onChange={vi.fn()} />)
const textarea = screen.getByTestId('text-area')
expect(textarea).toBeInTheDocument()
expect(textarea).toHaveValue('')
})
it('should handle value and onChange correctly', async () => {
const user = userEvent.setup()
const handleChange = vi.fn()
const { rerender } = render(<TextArea value="initial" onChange={handleChange} />)
const textarea = screen.getByTestId('text-area')
expect(textarea).toHaveValue('initial')
await user.type(textarea, ' updated')
expect(handleChange).toHaveBeenCalled()
rerender(<TextArea value="initial updated" onChange={handleChange} />)
expect(textarea).toHaveValue('initial updated')
})
it('should handle autoFocus correctly', () => {
render(<TextArea value="" onChange={vi.fn()} autoFocus />)
const textarea = screen.getByTestId('text-area')
expect(textarea).toHaveFocus()
})
it('should handle disabled state', () => {
render(<TextArea value="" onChange={vi.fn()} disabled />)
const textarea = screen.getByTestId('text-area')
expect(textarea).toBeDisabled()
expect(textarea).toHaveClass('cursor-not-allowed')
})
it('should handle placeholder', () => {
render(<TextArea value="" onChange={vi.fn()} placeholder="Enter text here" />)
expect(screen.getByPlaceholderText('Enter text here')).toBeInTheDocument()
})
it('should handle className', () => {
render(<TextArea value="" onChange={vi.fn()} className="custom-class" />)
expect(screen.getByTestId('text-area')).toHaveClass('custom-class')
})
it('should handle size variants', () => {
const { rerender } = render(<TextArea value="" onChange={vi.fn()} size="small" />)
expect(screen.getByTestId('text-area')).toHaveClass('py-1')
rerender(<TextArea value="" onChange={vi.fn()} size="large" />)
expect(screen.getByTestId('text-area')).toHaveClass('px-4')
})
it('should handle destructive state', () => {
render(<TextArea value="" onChange={vi.fn()} destructive />)
expect(screen.getByTestId('text-area')).toHaveClass('border-components-input-border-destructive')
})
it('should handle onFocus and onBlur', async () => {
const user = userEvent.setup()
const handleFocus = vi.fn()
const handleBlur = vi.fn()
render(<TextArea value="" onChange={vi.fn()} onFocus={handleFocus} onBlur={handleBlur} />)
const textarea = screen.getByTestId('text-area')
await user.click(textarea)
expect(handleFocus).toHaveBeenCalled()
await user.tab()
expect(handleBlur).toHaveBeenCalled()
})
})

View File

@ -1,562 +0,0 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { useState } from 'react'
import Textarea from '.'
const meta = {
title: 'Base/Data Entry/Textarea',
component: Textarea,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Textarea component with multiple sizes (small, regular, large). Built with class-variance-authority for consistent styling.',
},
},
},
tags: ['autodocs'],
argTypes: {
size: {
control: 'select',
options: ['small', 'regular', 'large'],
description: 'Textarea size',
},
value: {
control: 'text',
description: 'Textarea value',
},
placeholder: {
control: 'text',
description: 'Placeholder text',
},
disabled: {
control: 'boolean',
description: 'Disabled state',
},
destructive: {
control: 'boolean',
description: 'Error/destructive state',
},
rows: {
control: 'number',
description: 'Number of visible text rows',
},
},
} satisfies Meta<typeof Textarea>
export default meta
type Story = StoryObj<typeof meta>
// Interactive demo wrapper
const TextareaDemo = (args: any) => {
const [value, setValue] = useState(args.value || '')
return (
<div style={{ width: '500px' }}>
<Textarea
{...args}
value={value}
onChange={(e) => {
setValue(e.target.value)
console.log('Textarea changed:', e.target.value)
}}
/>
{value && (
<div className="mt-3 text-sm text-gray-600">
Character count:
{' '}
<span className="font-semibold">{value.length}</span>
</div>
)}
</div>
)
}
// Default state
export const Default: Story = {
render: args => <TextareaDemo {...args} />,
args: {
size: 'regular',
placeholder: 'Enter text...',
rows: 4,
value: '',
},
}
// Small size
export const SmallSize: Story = {
render: args => <TextareaDemo {...args} />,
args: {
size: 'small',
placeholder: 'Small textarea...',
rows: 3,
value: '',
},
}
// Large size
export const LargeSize: Story = {
render: args => <TextareaDemo {...args} />,
args: {
size: 'large',
placeholder: 'Large textarea...',
rows: 5,
value: '',
},
}
// With initial value
export const WithInitialValue: Story = {
render: args => <TextareaDemo {...args} />,
args: {
size: 'regular',
value: 'This is some initial text content.\n\nIt spans multiple lines.',
rows: 4,
},
}
// Disabled state
export const Disabled: Story = {
render: args => <TextareaDemo {...args} />,
args: {
size: 'regular',
value: 'This textarea is disabled and cannot be edited.',
disabled: true,
rows: 3,
},
}
// Destructive/error state
export const DestructiveState: Story = {
render: args => <TextareaDemo {...args} />,
args: {
size: 'regular',
value: 'This content has an error.',
destructive: true,
rows: 3,
},
}
// Size comparison
const SizeComparisonDemo = () => {
const [small, setSmall] = useState('')
const [regular, setRegular] = useState('')
const [large, setLarge] = useState('')
return (
<div style={{ width: '600px' }} className="space-y-4">
<div>
<label className="mb-2 block text-xs font-medium text-gray-600">Small</label>
<Textarea
size="small"
value={small}
onChange={e => setSmall(e.target.value)}
placeholder="Small textarea..."
rows={3}
/>
</div>
<div>
<label className="mb-2 block text-xs font-medium text-gray-600">Regular</label>
<Textarea
size="regular"
value={regular}
onChange={e => setRegular(e.target.value)}
placeholder="Regular textarea..."
rows={4}
/>
</div>
<div>
<label className="mb-2 block text-xs font-medium text-gray-600">Large</label>
<Textarea
size="large"
value={large}
onChange={e => setLarge(e.target.value)}
placeholder="Large textarea..."
rows={5}
/>
</div>
</div>
)
}
export const SizeComparison: Story = {
render: () => <SizeComparisonDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// State comparison
const StateComparisonDemo = () => {
const [normal, setNormal] = useState('Normal state')
const [error, setError] = useState('Error state')
return (
<div style={{ width: '500px' }} className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Normal</label>
<Textarea
value={normal}
onChange={e => setNormal(e.target.value)}
rows={3}
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Destructive</label>
<Textarea
value={error}
onChange={e => setError(e.target.value)}
destructive
rows={3}
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Disabled</label>
<Textarea
value="Disabled state"
onChange={() => undefined}
disabled
rows={3}
/>
</div>
</div>
)
}
export const StateComparison: Story = {
render: () => <StateComparisonDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// Real-world example - Comment form
const CommentFormDemo = () => {
const [comment, setComment] = useState('')
const maxLength = 500
return (
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">Leave a Comment</h3>
<Textarea
value={comment}
onChange={e => setComment(e.target.value)}
placeholder="Share your thoughts..."
rows={5}
maxLength={maxLength}
/>
<div className="mt-2 flex items-center justify-between">
<span className="text-xs text-gray-500">
{comment.length}
{' '}
/
{maxLength}
{' '}
characters
</span>
<button
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
disabled={comment.trim().length === 0}
>
Post Comment
</button>
</div>
</div>
)
}
export const CommentForm: Story = {
render: () => <CommentFormDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// Real-world example - Feedback form
const FeedbackFormDemo = () => {
const [feedback, setFeedback] = useState('')
const [email, setEmail] = useState('')
return (
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-2 text-lg font-semibold">Send Feedback</h3>
<p className="mb-4 text-sm text-gray-600">Help us improve our product</p>
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Your Email</label>
<input
type="email"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
value={email}
onChange={e => setEmail(e.target.value)}
placeholder="email@example.com"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Your Feedback</label>
<Textarea
value={feedback}
onChange={e => setFeedback(e.target.value)}
placeholder="Tell us what you think..."
rows={6}
/>
</div>
<button className="w-full rounded-lg bg-green-600 px-4 py-2 text-sm font-medium text-white hover:bg-green-700">
Submit Feedback
</button>
</div>
</div>
)
}
export const FeedbackForm: Story = {
render: () => <FeedbackFormDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// Real-world example - Code snippet
const CodeSnippetDemo = () => {
const [code, setCode] = useState(`function hello() {
console.log("Hello, world!");
}`)
return (
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">Code Editor</h3>
<Textarea
value={code}
onChange={e => setCode(e.target.value)}
className="font-mono"
rows={8}
/>
<div className="mt-4 flex gap-2">
<button className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
Run Code
</button>
<button className="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300">
Copy
</button>
</div>
</div>
)
}
export const CodeSnippet: Story = {
render: () => <CodeSnippetDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// Real-world example - Message composer
const MessageComposerDemo = () => {
const [message, setMessage] = useState('')
return (
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">Compose Message</h3>
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">To</label>
<input
type="text"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
placeholder="Recipient name"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Subject</label>
<input
type="text"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
placeholder="Message subject"
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Message</label>
<Textarea
value={message}
onChange={e => setMessage(e.target.value)}
placeholder="Type your message here..."
rows={8}
/>
</div>
<div className="flex gap-2">
<button className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
Send Message
</button>
<button className="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300">
Save Draft
</button>
</div>
</div>
</div>
)
}
export const MessageComposer: Story = {
render: () => <MessageComposerDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// Real-world example - Bio editor
const BioEditorDemo = () => {
const [bio, setBio] = useState('Software developer passionate about building great products.')
const maxLength = 200
return (
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">Edit Your Bio</h3>
<Textarea
value={bio}
onChange={e => setBio(e.target.value.slice(0, maxLength))}
placeholder="Tell us about yourself..."
rows={4}
/>
<div className="mt-2 flex items-center justify-between text-xs">
<span className={bio.length > maxLength * 0.9 ? 'text-orange-600' : 'text-gray-500'}>
{bio.length}
{' '}
/
{maxLength}
{' '}
characters
</span>
{bio.length > maxLength * 0.9 && (
<span className="text-orange-600">
{maxLength - bio.length}
{' '}
characters remaining
</span>
)}
</div>
<div className="mt-4 rounded-lg bg-gray-50 p-4">
<div className="mb-2 text-xs font-medium text-gray-600">Preview:</div>
<p className="text-sm text-gray-800">{bio || 'Your bio will appear here...'}</p>
</div>
</div>
)
}
export const BioEditor: Story = {
render: () => <BioEditorDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// Real-world example - JSON editor
const JSONEditorDemo = () => {
const [json, setJson] = useState(`{
"name": "John Doe",
"age": 30,
"email": "john@example.com"
}`)
const [isValid, setIsValid] = useState(true)
const validateJSON = (value: string) => {
try {
JSON.parse(value)
setIsValid(true)
}
catch {
setIsValid(false)
}
}
return (
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-semibold">JSON Editor</h3>
<span className={`rounded-sm px-2 py-1 text-xs ${isValid ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
{isValid ? '✓ Valid' : '✗ Invalid'}
</span>
</div>
<Textarea
value={json}
onChange={(e) => {
setJson(e.target.value)
validateJSON(e.target.value)
}}
className="font-mono"
destructive={!isValid}
rows={10}
/>
<div className="mt-4 flex gap-2">
<button className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 disabled:opacity-50" disabled={!isValid}>
Save JSON
</button>
<button
className="rounded-lg bg-gray-200 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-300"
onClick={() => {
try {
const formatted = JSON.stringify(JSON.parse(json), null, 2)
setJson(formatted)
}
catch {
// Invalid JSON, do nothing
}
}}
>
Format
</button>
</div>
</div>
)
}
export const JSONEditor: Story = {
render: () => <JSONEditorDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// Real-world example - Task description
const TaskDescriptionDemo = () => {
const [title, setTitle] = useState('Implement user authentication')
const [description, setDescription] = useState('Add login and registration functionality with JWT tokens.')
return (
<div style={{ width: '600px' }} className="rounded-lg border border-gray-200 bg-white p-6">
<h3 className="mb-4 text-lg font-semibold">Create New Task</h3>
<div className="space-y-4">
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Task Title</label>
<input
type="text"
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm"
value={title}
onChange={e => setTitle(e.target.value)}
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Description</label>
<Textarea
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Describe the task in detail..."
rows={6}
/>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-gray-700">Priority</label>
<select className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm">
<option>Low</option>
<option>Medium</option>
<option>High</option>
<option>Urgent</option>
</select>
</div>
<button className="w-full rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700">
Create Task
</button>
</div>
</div>
)
}
export const TaskDescription: Story = {
render: () => <TaskDescriptionDemo />,
parameters: { controls: { disable: true } },
} as unknown as Story
// Interactive playground
export const Playground: Story = {
render: args => <TextareaDemo {...args} />,
args: {
size: 'regular',
placeholder: 'Enter text...',
rows: 4,
disabled: false,
destructive: false,
value: '',
},
}

View File

@ -1,60 +0,0 @@
import type { VariantProps } from 'class-variance-authority'
import type { CSSProperties } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { cva } from 'class-variance-authority'
import * as React from 'react'
const textareaVariants = cva(
'',
{
variants: {
size: {
small: 'rounded-md py-1 system-xs-regular',
regular: 'rounded-md px-3 system-sm-regular',
large: 'rounded-lg px-4 system-md-regular',
},
},
defaultVariants: {
size: 'regular',
},
},
)
export type TextareaProps = {
value: string | number
disabled?: boolean
destructive?: boolean
styleCss?: CSSProperties
ref?: React.Ref<HTMLTextAreaElement>
onFocus?: React.FocusEventHandler<HTMLTextAreaElement>
onBlur?: React.FocusEventHandler<HTMLTextAreaElement>
} & React.TextareaHTMLAttributes<HTMLTextAreaElement> & VariantProps<typeof textareaVariants>
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, value, onChange, disabled, size, destructive, styleCss, onFocus, onBlur, ...props }, ref) => {
return (
<textarea
ref={ref}
onFocus={onFocus}
onBlur={onBlur}
style={styleCss}
className={cn(
'min-h-20 w-full appearance-none border border-transparent bg-components-input-bg-normal p-2 text-components-input-text-filled caret-primary-600 outline-hidden placeholder:text-components-input-text-placeholder hover:border-components-input-border-hover hover:bg-components-input-bg-hover focus:border-components-input-border-active focus:bg-components-input-bg-active focus:shadow-xs',
textareaVariants({ size }),
disabled && 'cursor-not-allowed border-transparent bg-components-input-bg-disabled text-components-input-text-filled-disabled hover:border-transparent hover:bg-components-input-bg-disabled',
destructive && 'border-components-input-border-destructive bg-components-input-bg-destructive text-components-input-text-filled hover:border-components-input-border-destructive hover:bg-components-input-bg-destructive focus:border-components-input-border-destructive focus:bg-components-input-bg-destructive',
className,
)}
value={value ?? ''}
onChange={onChange}
disabled={disabled}
data-testid="text-area"
{...props}
>
</textarea>
)
},
)
Textarea.displayName = 'Textarea'
export default Textarea

View File

@ -1,6 +1,7 @@
import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import type { PipelineTemplate } from '@/models/pipeline'
import { Button } from '@langgenius/dify-ui/button'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
@ -9,7 +10,6 @@ import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { useInvalidCustomizedTemplateList, useUpdateTemplateInfo } from '@/service/use-pipeline'
type EditPipelineInfoProps = {
@ -57,8 +57,7 @@ const EditPipelineInfo = ({
setShowAppIconPicker(false)
}, [])
const handleDescriptionChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = event.target.value
const handleDescriptionChange = useCallback((value: string) => {
setDescription(value)
}, [])
@ -133,7 +132,7 @@ const EditPipelineInfo = ({
{t('knowledgeDescription', { ns: 'datasetPipeline' })}
</label>
<Textarea
onChange={handleDescriptionChange}
onValueChange={handleDescriptionChange}
value={description}
placeholder={t('knowledgeDescriptionPlaceholder', { ns: 'datasetPipeline' })}
/>

View File

@ -5,12 +5,12 @@ import type { DataSet } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { RiCloseLine } from '@remixicon/react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { updateDatasetSetting } from '@/service/datasets'
import AppIcon from '../../base/app-icon'
import AppIconPicker from '../../base/app-icon-picker'
@ -117,7 +117,7 @@ const RenameDatasetModal = ({ show, dataset, onSuccess, onClose }: RenameDataset
{t('form.desc', { ns: 'datasetSettings' })}
</div>
<div className="w-full">
<Textarea value={description} onChange={e => setDescription(e.target.value)} className="resize-none" placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''} />
<Textarea value={description} onValueChange={value => setDescription(value)} className="resize-none" placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''} />
</div>
</div>
</div>

View File

@ -3,11 +3,11 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import type { Member } from '@/models/common'
import type { DataSet, DatasetPermission, IconInfo } from '@/models/datasets'
import type { AppIconType } from '@/types/app'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import PermissionSelector from '../../permission-selector'
const rowClass = 'flex gap-x-1'
@ -89,7 +89,7 @@ const BasicInfoSection = ({
className="resize-none"
placeholder={t('form.descPlaceholder', { ns: 'datasetSettings' }) || ''}
value={description}
onChange={e => setDescription(e.target.value)}
onValueChange={value => setDescription(value)}
/>
</div>
</div>

View File

@ -1,7 +1,7 @@
import type { ChangeEvent } from 'react'
import type { DefaultModel } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { SummaryIndexSetting as SummaryIndexSettingType } from '@/models/datasets'
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
import {
memo,
useCallback,
@ -9,7 +9,6 @@ import {
} from 'react'
import { useTranslation } from 'react-i18next'
import { Infotip } from '@/app/components/base/infotip'
import Textarea from '@/app/components/base/textarea'
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
@ -53,9 +52,9 @@ const SummaryIndexSetting = ({
})
}, [onSummaryIndexSettingChange])
const handleSummaryIndexPromptChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
const handleSummaryIndexPromptChange = useCallback((value: string) => {
onSummaryIndexSettingChange?.({
summary_prompt: e.target.value,
summary_prompt: value,
})
}, [onSummaryIndexSettingChange])
@ -96,7 +95,7 @@ const SummaryIndexSetting = ({
</div>
<Textarea
value={summaryIndexSetting?.summary_prompt ?? ''}
onChange={handleSummaryIndexPromptChange}
onValueChange={handleSummaryIndexPromptChange}
disabled={readonly}
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
/>
@ -167,7 +166,7 @@ const SummaryIndexSetting = ({
<div className="grow">
<Textarea
value={summaryIndexSetting?.summary_prompt ?? ''}
onChange={handleSummaryIndexPromptChange}
onValueChange={handleSummaryIndexPromptChange}
disabled={readonly}
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
/>
@ -215,7 +214,7 @@ const SummaryIndexSetting = ({
</div>
<Textarea
value={summaryIndexSetting?.summary_prompt ?? ''}
onChange={handleSummaryIndexPromptChange}
onValueChange={handleSummaryIndexPromptChange}
disabled={readonly}
placeholder={t('form.summaryInstructionsPlaceholder', { ns: 'datasetSettings' })}
/>

View File

@ -3,6 +3,7 @@ import type { AppIconType } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { useDebounceFn, useKeyPress } from 'ahooks'
import * as React from 'react'
@ -10,7 +11,6 @@ import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import AppsFull from '@/app/components/billing/apps-full-in-dialog'
import { useProviderContext } from '@/context/provider-context'
import { AppModeEnum } from '@/types/app'
@ -148,7 +148,7 @@ const CreateAppModal = ({
className="resize-none"
placeholder={t('newApp.appDescriptionPlaceholder', { ns: 'app' }) || ''}
value={description}
onChange={e => setDescription(e.target.value)}
onValueChange={value => setDescription(value)}
/>
</div>
{/* answer icon */}

View File

@ -1,9 +1,9 @@
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { InputVarType } from '@/app/components/workflow/types'
type Props = {
@ -56,7 +56,7 @@ const AppInputsForm = ({
return (
<Textarea
value={inputs[variable] || ''}
onChange={e => handleFormChange(variable, e.target.value)}
onValueChange={value => handleFormChange(variable, value)}
placeholder={label}
/>
)

View File

@ -523,9 +523,7 @@ describe('useToolSelectorState Hook', () => {
)
act(() => {
result.current.handleDescriptionChange({
target: { value: 'new description' },
} as React.ChangeEvent<HTMLTextAreaElement>)
result.current.handleDescriptionChange('new description')
})
expect(onSelect).toHaveBeenCalledWith(
@ -1724,9 +1722,7 @@ describe('Edge Cases', () => {
)
act(() => {
result.current.handleDescriptionChange({
target: { value: '' },
} as React.ChangeEvent<HTMLTextAreaElement>)
result.current.handleDescriptionChange('')
})
expect(onSelect).toHaveBeenCalledWith(

View File

@ -1,24 +1,6 @@
import { fireEvent, render, screen } from '@testing-library/react'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/app/components/base/textarea', () => ({
default: ({ value, onChange, disabled, placeholder }: {
value?: string
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
disabled?: boolean
placeholder?: string
}) => (
<textarea
data-testid="description-textarea"
value={value || ''}
onChange={onChange}
disabled={disabled}
placeholder={placeholder}
/>
),
}))
vi.mock('../../../../readme-panel/entrance', () => ({
ReadmeEntrance: () => <div data-testid="readme-entrance" />,
}))
@ -68,28 +50,28 @@ describe('ToolBaseForm', () => {
it('should render description textarea', () => {
render(<ToolBaseForm {...defaultProps} />)
expect(screen.getByTestId('description-textarea')).toBeInTheDocument()
expect(screen.getByRole('textbox')).toBeInTheDocument()
})
it('should disable textarea when no provider_name in value', () => {
render(<ToolBaseForm {...defaultProps} />)
expect(screen.getByTestId('description-textarea')).toBeDisabled()
expect(screen.getByRole('textbox')).toBeDisabled()
})
it('should enable textarea when value has provider_name', () => {
const value = { provider_name: 'test-provider', tool_name: 'test', extra: { description: 'Hello' } } as never
render(<ToolBaseForm {...defaultProps} value={value} />)
expect(screen.getByTestId('description-textarea')).not.toBeDisabled()
expect(screen.getByRole('textbox')).not.toBeDisabled()
})
it('should call onDescriptionChange when textarea content changes', () => {
const value = { provider_name: 'test-provider', tool_name: 'test', extra: { description: 'Hello' } } as never
render(<ToolBaseForm {...defaultProps} value={value} />)
fireEvent.change(screen.getByTestId('description-textarea'), { target: { value: 'Updated' } })
expect(mockOnDescriptionChange).toHaveBeenCalled()
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'Updated' } })
expect(mockOnDescriptionChange).toHaveBeenCalledWith('Updated', expect.any(Object))
})
it('should show ReadmeEntrance when provider has plugin_unique_identifier', () => {

View File

@ -4,8 +4,8 @@ import type { FC } from 'react'
import type { PluginDetail } from '@/app/components/plugins/types'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import { ReadmeEntrance } from '../../../readme-panel/entrance'
import ToolTrigger from './tool-trigger'
@ -23,7 +23,7 @@ type ToolBaseFormProps = {
onPanelShowStateChange?: (state: boolean) => void
onSelectTool: (tool: ToolDefaultValue) => void
onSelectMultipleTool: (tools: ToolDefaultValue[]) => void
onDescriptionChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
onDescriptionChange: (value: string) => void
}
const ToolBaseForm: FC<ToolBaseFormProps> = ({
@ -85,9 +85,10 @@ const ToolBaseForm: FC<ToolBaseFormProps> = ({
</div>
<Textarea
className="resize-none"
aria-label={t('detailPanel.toolSelector.descriptionLabel', { ns: 'plugin' })}
placeholder={t('detailPanel.toolSelector.descriptionPlaceholder', { ns: 'plugin' })}
value={value?.extra?.description || ''}
onChange={onDescriptionChange}
onValueChange={onDescriptionChange}
disabled={!value?.provider_name}
/>
</div>

View File

@ -1,4 +1,3 @@
import type * as React from 'react'
import type { ToolValue } from '@/app/components/workflow/block-selector/types'
import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
@ -161,9 +160,8 @@ describe('useToolSelectorState', () => {
useToolSelectorState({ value: toolValue, onSelect: mockOnSelect }),
)
const event = { target: { value: 'New description' } } as React.ChangeEvent<HTMLTextAreaElement>
act(() => {
result.current.handleDescriptionChange(event)
result.current.handleDescriptionChange('New description')
})
expect(mockOnSelect).toHaveBeenCalledWith(expect.objectContaining({

View File

@ -144,14 +144,14 @@ export const useToolSelectorState = ({
onSelectMultiple?.(toolValues)
}, [getToolValue, onSelectMultiple])
const handleDescriptionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const handleDescriptionChange = useCallback((description: string) => {
if (!value)
return
onSelect({
...value,
extra: {
...value.extra,
description: e.target.value || '',
description: description || '',
},
})
}, [value, onSelect])

View File

@ -44,17 +44,6 @@ vi.mock('@/app/components/base/input', () => ({
),
}))
vi.mock('@/app/components/base/textarea', () => ({
default: ({ value, onChange, ...props }: Record<string, unknown>) => (
<textarea
data-testid="description-textarea"
value={value as string}
onChange={onChange as () => void}
{...props}
/>
),
}))
vi.mock('@/app/components/base/app-icon', () => ({
default: ({ onClick }: { onClick?: () => void }) => (
<div data-testid="app-icon" onClick={onClick} />
@ -115,7 +104,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
it('should initialize description as empty', () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
const textarea = screen.getByTestId('description-textarea') as HTMLTextAreaElement
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' }) as HTMLTextAreaElement
expect(textarea.value).toBe('')
})
@ -159,7 +148,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
it('should update description when textarea changes', () => {
render(<PublishAsKnowledgePipelineModal {...defaultProps} />)
const textarea = screen.getByTestId('description-textarea')
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' })
fireEvent.change(textarea, { target: { value: 'My description' } })
expect((textarea as HTMLTextAreaElement).value).toBe('My description')
@ -233,7 +222,7 @@ describe('PublishAsKnowledgePipelineModal', () => {
const nameInput = screen.getByTestId('name-input')
fireEvent.change(nameInput, { target: { value: ' Trimmed Name ' } })
const textarea = screen.getByTestId('description-textarea')
const textarea = screen.getByRole('textbox', { name: 'pipeline.common.publishAsPipeline.description' })
fireEvent.change(textarea, { target: { value: ' Some desc ' } })
fireEvent.click(screen.getByText('workflow.common.publish'))

View File

@ -3,13 +3,13 @@ import type { AppIconSelection } from '@/app/components/base/app-icon-picker'
import type { IconInfo } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { RiCloseLine } from '@remixicon/react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import AppIconPicker from '@/app/components/base/app-icon-picker'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { useWorkflowStore } from '@/app/components/workflow/store'
type PublishAsKnowledgePipelineModalProps = {
@ -118,9 +118,10 @@ const PublishAsKnowledgePipelineModal = ({
</div>
<Textarea
className="resize-none"
aria-label={t('common.publishAsPipeline.description', { ns: 'pipeline' })}
placeholder={t('common.publishAsPipeline.descriptionPlaceholder', { ns: 'pipeline' }) || ''}
value={description}
onChange={e => setDescription(e.target.value)}
onValueChange={value => setDescription(value)}
/>
</div>
</div>

View File

@ -7,6 +7,7 @@ import type { VisionFile, VisionSettings } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import {
RiLoader2Line,
RiPlayLargeLine,
@ -18,7 +19,6 @@ import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uplo
import { StopCircle } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import BoolInput from '@/app/components/workflow/nodes/_base/components/before-run-form/bool-input'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -162,7 +162,7 @@ const RunOnce: FC<IRunOnceProps> = ({
className="h-[104px] sm:text-xs"
placeholder={item.name}
value={inputs[item.key] as string}
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
onValueChange={(value) => { handleInputsChange({ ...inputsRef.current, [item.key]: value }) }}
/>
)}
{item.type === 'number' && (

View File

@ -13,6 +13,7 @@ import {
DrawerTitle,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { RiSettings2Line } from '@remixicon/react'
import { useDebounce, useGetState } from 'ahooks'
@ -23,7 +24,6 @@ import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import EmojiPicker from '@/app/components/base/emoji-picker'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import LabelSelector from '@/app/components/tools/labels/selector'
import { parseParamsSchema } from '@/service/tools'
import { LinkExternal02 } from '../../base/icons/src/vender/line/general'
@ -282,7 +282,7 @@ const EditCustomCollectionModal: FC<Props> = ({
<Textarea
className="h-[240px] resize-none"
value={schema}
onChange={e => setSchema(e.target.value)}
onValueChange={value => setSchema(value)}
placeholder={t('createTool.schemaPlaceHolder', { ns: 'tools' })!}
/>
</div>

View File

@ -4,11 +4,11 @@ import type {
} from '@/app/components/tools/types'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { RiCloseLine } from '@remixicon/react'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Textarea from '@/app/components/base/textarea'
import MCPServerParamItem from '@/app/components/tools/mcp/mcp-server-param-item'
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
import {
@ -157,9 +157,8 @@ const MCPServerModal = ({
className="h-[96px] resize-none"
value={description}
placeholder={t('mcp.server.modal.descriptionPlaceholder', { ns: 'tools' })}
onChange={e => setDescription(e.target.value)}
>
</Textarea>
onValueChange={value => setDescription(value)}
/>
</div>
{latestParams.length > 0 && (

View File

@ -1,7 +1,7 @@
'use client'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'
type Props = {
data?: any
@ -28,9 +28,8 @@ const MCPServerParamItem = ({
className="h-8 resize-none"
value={value}
placeholder={t('mcp.server.modal.parametersPlaceholder', { ns: 'tools' })}
onChange={e => onChange(e.target.value)}
>
</Textarea>
onValueChange={value => onChange(value)}
/>
</div>
)
}

View File

@ -14,6 +14,7 @@ import {
DrawerTitle,
DrawerViewport,
} from '@langgenius/dify-ui/drawer'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { produce } from 'immer'
@ -25,7 +26,6 @@ import Divider from '@/app/components/base/divider'
import EmojiPickerInner from '@/app/components/base/emoji-picker/Inner'
import { Infotip } from '@/app/components/base/infotip'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import LabelSelector from '@/app/components/tools/labels/selector'
import ConfirmModal from '@/app/components/tools/workflow-tool/confirm-modal'
import MethodSelector from '@/app/components/tools/workflow-tool/method-selector'
@ -305,7 +305,7 @@ export function WorkflowToolDrawer({
<Textarea
placeholder={t('createTool.descriptionPlaceholder', { ns: 'tools' }) || ''}
value={description}
onChange={e => setDescription(e.target.value)}
onValueChange={value => setDescription(value)}
/>
</div>
{/* Tool Input */}

View File

@ -4,6 +4,7 @@ import type { InputVar } from '../../../../types'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import { cn } from '@langgenius/dify-ui/cn'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Textarea } from '@langgenius/dify-ui/textarea'
import {
RiDeleteBinLine,
} from '@remixicon/react'
@ -18,7 +19,6 @@ import { Variable02 } from '@/app/components/base/icons/src/vender/solid/develop
import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
import Input from '@/app/components/base/input'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import Textarea from '@/app/components/base/textarea'
import { VarBlockIcon } from '@/app/components/workflow/block-icon'
import { useHooksStore } from '@/app/components/workflow/hooks-store'
import { Resolution, TransferMethod } from '@/types/app'
@ -171,7 +171,7 @@ const FormItem: FC<Props> = ({
type === InputVarType.paragraph && (
<Textarea
value={value || ''}
onChange={e => onChange(e.target.value)}
onValueChange={value => onChange(value)}
placeholder={typeof payload.label === 'object' ? payload.label.variable : payload.label}
autoFocus={autoFocus}
/>

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { AssignerNodeOperation } from '../../types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { RiDeleteBinLine } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import { produce } from 'immer'
@ -10,7 +11,6 @@ import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import ListNoDataPlaceholder from '@/app/components/workflow/nodes/_base/components/list-no-data-placeholder'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
@ -191,7 +191,7 @@ const VarList: FC<Props> = ({
{assignedVarType === 'string' && (
<Textarea
value={item.value as string}
onChange={e => handleToAssignedVarChange(index)(e.target.value)}
onValueChange={value => handleToAssignedVarChange(index)(value)}
className="w-full"
/>
)}

View File

@ -3,11 +3,11 @@ import type { FC } from 'react'
import type { HttpNodeType } from '../types'
import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Textarea from '@/app/components/base/textarea'
import { useNodesInteractions } from '@/app/components/workflow/hooks'
import { parseCurl } from './curl-parser'
@ -58,7 +58,7 @@ const CurlPanel: FC<Props> = ({ nodeId, isShow, onHide, handleCurlImport }) => {
<Textarea
value={inputString}
className="my-3 h-40 w-full grow"
onChange={e => setInputString(e.target.value)}
onValueChange={value => setInputString(value)}
placeholder={t('nodes.http.curl.placeholder', { ns: 'workflow' })!}
/>
</div>

View File

@ -2,12 +2,12 @@ import type { FC } from 'react'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Model } from '@/types/app'
import { Button } from '@langgenius/dify-ui/button'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { RiCloseLine, RiSparklingFill } from '@remixicon/react'
import * as React from 'react'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Infotip } from '@/app/components/base/infotip'
import Textarea from '@/app/components/base/textarea'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
type ModelInfo = {
@ -38,8 +38,8 @@ const PromptEditor: FC<PromptEditorProps> = ({
}) => {
const { t } = useTranslation()
const handleInstructionChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
onInstructionChange(e.target.value)
const handleInstructionChange = useCallback((value: string) => {
onInstructionChange(value)
}, [onInstructionChange])
return (
@ -93,7 +93,7 @@ const PromptEditor: FC<PromptEditorProps> = ({
className="h-[364px] resize-none px-2 py-1"
value={instruction}
placeholder={t('nodes.llm.jsonSchema.promptPlaceholder', { ns: 'workflow' })}
onChange={handleInstructionChange}
onValueChange={handleInstructionChange}
/>
</div>
</div>

View File

@ -1,9 +1,9 @@
import type { FC } from 'react'
import { Textarea } from '@langgenius/dify-ui/textarea'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Divider from '@/app/components/base/divider'
import Textarea from '@/app/components/base/textarea'
export type AdvancedOptionsType = {
enum: string
@ -22,8 +22,8 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
// const [showAdvancedOptions, setShowAdvancedOptions] = useState(false)
const [enumValue, setEnumValue] = useState(options.enum)
const handleEnumChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEnumValue(e.target.value)
const handleEnumChange = useCallback((value: string) => {
setEnumValue(value)
}, [])
const handleEnumBlur = useCallback((e: React.FocusEvent<HTMLTextAreaElement>) => {
@ -54,7 +54,7 @@ const AdvancedOptions: FC<AdvancedOptionsProps> = ({
size="small"
className="min-h-6"
value={enumValue}
onChange={handleEnumChange}
onValueChange={handleEnumChange}
onBlur={handleEnumBlur}
placeholder="abcd, 1, 1.5, etc."
/>

View File

@ -4,13 +4,13 @@ import type {
import type {
Var,
} from '@/app/components/workflow/types'
import { Textarea } from '@langgenius/dify-ui/textarea'
import {
useCallback,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import VarReferencePicker from '@/app/components/workflow/nodes/_base/components/variable/var-reference-picker'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
@ -49,6 +49,10 @@ const FormItem = ({
onChange(e.target.value)
}, [onChange])
const handleValueChange = useCallback((value: string) => {
onChange(value)
}, [onChange])
const handleChange = useCallback((value: any) => {
onChange(value)
}, [onChange])
@ -93,7 +97,7 @@ const FormItem = ({
value_type === ValueType.constant && var_type === VarType.string && (
<Textarea
value={value}
onChange={handleInputChange}
onValueChange={handleValueChange}
className="min-h-12 w-full"
/>
)

View File

@ -6,6 +6,7 @@ import { Button } from '@langgenius/dify-ui/button'
import { Dialog, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select'
import { Switch } from '@langgenius/dify-ui/switch'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { toast } from '@langgenius/dify-ui/toast'
import { useBoolean } from 'ahooks'
import * as React from 'react'
@ -14,7 +15,6 @@ import { useTranslation } from 'react-i18next'
import Field from '@/app/components/app/configuration/config-var/config-modal/field'
import ConfigSelect from '@/app/components/app/configuration/config-var/config-select'
import Input from '@/app/components/base/input'
import Textarea from '@/app/components/base/textarea'
import { ChangeType } from '@/app/components/workflow/types'
import { checkKeys } from '@/utils/var'
import { ParamType } from '../../types'
@ -176,7 +176,7 @@ const AddExtractParameter: FC<Props> = ({
<Field title={t(`${i18nPrefix}.addExtractParameterContent.description`, { ns: 'workflow' })}>
<Textarea
value={param.description}
onChange={e => handleParamChange('description')(e.target.value)}
onValueChange={value => handleParamChange('description')(value)}
placeholder={t(`${i18nPrefix}.addExtractParameterContent.descriptionPlaceholder`, { ns: 'workflow' })!}
/>
</Field>

View File

@ -2,11 +2,11 @@ import type { VarType } from '../types'
import type { ChunkInfo } from '@/app/components/rag-pipeline/components/chunk-card-list/types'
import type { ParentMode } from '@/models/datasets'
import { cn } from '@langgenius/dify-ui/cn'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { ToggleGroup, ToggleGroupItem } from '@langgenius/dify-ui/toggle-group'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Markdown } from '@/app/components/base/markdown'
import Textarea from '@/app/components/base/textarea'
import { ChunkCardList } from '@/app/components/rag-pipeline/components/chunk-card-list'
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
import { ChunkingMode } from '@/models/datasets'
@ -95,7 +95,7 @@ export function DisplayContent(props: DisplayContentProps) {
disabled={readonly}
className="h-full border-none bg-transparent p-0 text-text-secondary hover:bg-transparent focus:bg-transparent focus:shadow-none"
value={mdString as any}
onChange={e => handleTextChange?.(e.target.value)}
onValueChange={value => handleTextChange?.(value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
/>

View File

@ -2,9 +2,9 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types'
import type { FileUploadConfigResponse } from '@/models/common'
import type { VarInInspect } from '@/types/workflow'
import { cn } from '@langgenius/dify-ui/cn'
import { Textarea } from '@langgenius/dify-ui/textarea'
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import Textarea from '@/app/components/base/textarea'
import ErrorMessage from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/error-message'
import SchemaEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/schema-editor'
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
@ -50,7 +50,7 @@ export const TextEditorSection = ({
disabled={textEditorDisabled || isTruncated}
className={cn('h-full', isTruncated && 'pt-[48px]')}
value={typeof value === 'number' ? value : String(value ?? '')}
onChange={e => onTextChange(e.target.value)}
onValueChange={value => onTextChange(value)}
/>
)}
</>