mirror of
https://github.com/langgenius/dify.git
synced 2024-11-16 19:59:50 +08:00
147 lines
4.5 KiB
TypeScript
147 lines
4.5 KiB
TypeScript
'use client'
|
|
|
|
import type { ChangeEvent, FC } from 'react'
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { varHighlightHTML } from '../../app/configuration/base/var-highlight'
|
|
import Toast from '../toast'
|
|
import classNames from '@/utils/classnames'
|
|
import { checkKeys } from '@/utils/var'
|
|
|
|
// regex to match the {{}} and replace it with a span
|
|
const regex = /\{\{([^}]+)\}\}/g
|
|
|
|
export const getInputKeys = (value: string) => {
|
|
const keys = value.match(regex)?.map((item) => {
|
|
return item.replace('{{', '').replace('}}', '')
|
|
}) || []
|
|
const keyObj: Record<string, boolean> = {}
|
|
// remove duplicate keys
|
|
const res: string[] = []
|
|
keys.forEach((key) => {
|
|
if (keyObj[key])
|
|
return
|
|
|
|
keyObj[key] = true
|
|
res.push(key)
|
|
})
|
|
return res
|
|
}
|
|
|
|
export type IBlockInputProps = {
|
|
value: string
|
|
className?: string // wrapper class
|
|
highLightClassName?: string // class for the highlighted text default is text-blue-500
|
|
readonly?: boolean
|
|
onConfirm?: (value: string, keys: string[]) => void
|
|
}
|
|
|
|
const BlockInput: FC<IBlockInputProps> = ({
|
|
value = '',
|
|
className,
|
|
readonly = false,
|
|
onConfirm,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
// current is used to store the current value of the contentEditable element
|
|
const [currentValue, setCurrentValue] = useState<string>(value)
|
|
useEffect(() => {
|
|
setCurrentValue(value)
|
|
}, [value])
|
|
|
|
const contentEditableRef = useRef<HTMLTextAreaElement>(null)
|
|
const [isEditing, setIsEditing] = useState<boolean>(false)
|
|
useEffect(() => {
|
|
if (isEditing && contentEditableRef.current) {
|
|
// TODO: Focus at the click positon
|
|
if (currentValue)
|
|
contentEditableRef.current.setSelectionRange(currentValue.length, currentValue.length)
|
|
|
|
contentEditableRef.current.focus()
|
|
}
|
|
}, [isEditing])
|
|
|
|
const style = classNames({
|
|
'block px-4 py-2 w-full h-full text-sm text-gray-900 outline-0 border-0 break-all': true,
|
|
'block-input--editing': isEditing,
|
|
})
|
|
|
|
const coloredContent = (currentValue || '')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
|
|
.replace(/\n/g, '<br />')
|
|
|
|
// Not use useCallback. That will cause out callback get old data.
|
|
const handleSubmit = (value: string) => {
|
|
if (onConfirm) {
|
|
const keys = getInputKeys(value)
|
|
const { isValid, errorKey, errorMessageKey } = checkKeys(keys)
|
|
if (!isValid) {
|
|
Toast.notify({
|
|
type: 'error',
|
|
message: t(`appDebug.varKeyError.${errorMessageKey}`, { key: errorKey }),
|
|
})
|
|
return
|
|
}
|
|
onConfirm(value, keys)
|
|
}
|
|
}
|
|
|
|
const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
|
|
const value = e.target.value
|
|
setCurrentValue(value)
|
|
handleSubmit(value)
|
|
}, [])
|
|
|
|
// Prevent rerendering caused cursor to jump to the start of the contentEditable element
|
|
const TextAreaContentView = () => {
|
|
return <div
|
|
className={classNames(style, className)}
|
|
dangerouslySetInnerHTML={{ __html: coloredContent }}
|
|
suppressContentEditableWarning={true}
|
|
/>
|
|
}
|
|
|
|
const placeholder = ''
|
|
const editAreaClassName = 'focus:outline-none bg-transparent text-sm'
|
|
|
|
const textAreaContent = (
|
|
<div className={classNames(readonly ? 'max-h-[180px] pb-5' : 'h-[180px]', ' overflow-y-auto')} onClick={() => !readonly && setIsEditing(true)}>
|
|
{isEditing
|
|
? <div className='h-full px-4 py-2'>
|
|
<textarea
|
|
ref={contentEditableRef}
|
|
className={classNames(editAreaClassName, 'block w-full h-full resize-none')}
|
|
placeholder={placeholder}
|
|
onChange={onValueChange}
|
|
value={currentValue}
|
|
onBlur={() => {
|
|
blur()
|
|
setIsEditing(false)
|
|
// click confirm also make blur. Then outter value is change. So below code has problem.
|
|
// setTimeout(() => {
|
|
// handleCancel()
|
|
// }, 1000)
|
|
}}
|
|
/>
|
|
</div>
|
|
: <TextAreaContentView />}
|
|
</div>)
|
|
|
|
return (
|
|
<div className={classNames('block-input w-full overflow-y-auto bg-white border-none rounded-xl')}>
|
|
{textAreaContent}
|
|
{/* footer */}
|
|
{!readonly && (
|
|
<div className='pl-4 pb-2 flex'>
|
|
<div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue?.length}</div>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default React.memo(BlockInput)
|