mirror of
				https://github.com/langgenius/dify.git
				synced 2025-11-03 20:33:00 +00:00 
			
		
		
		
	Co-authored-by: NFish <douxc512@gmail.com> Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: twwu <twwu@dify.ai> Co-authored-by: jZonG <jzongcode@gmail.com>
		
			
				
	
	
		
			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 position
 | 
						|
      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 outer 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='flex pb-2 pl-4'>
 | 
						|
          <div className="h-[18px] rounded-md bg-gray-100 px-1 text-xs leading-[18px] text-gray-500">{currentValue?.length}</div>
 | 
						|
        </div>
 | 
						|
      )}
 | 
						|
 | 
						|
    </div>
 | 
						|
  )
 | 
						|
}
 | 
						|
 | 
						|
export default React.memo(BlockInput)
 |