mirror of
https://github.com/langgenius/dify.git
synced 2026-05-13 08:57:28 +08:00
Made-with: Cursor # Conflicts: # api/core/agent/cot_chat_agent_runner.py # api/core/agent/fc_agent_runner.py # api/core/memory/token_buffer_memory.py # api/core/variables/segments.py # api/core/workflow/file/file_manager.py # api/core/workflow/nodes/agent/agent_node.py # api/core/workflow/nodes/llm/llm_utils.py # api/core/workflow/nodes/parameter_extractor/parameter_extractor_node.py # api/core/workflow/workflow_entry.py # api/factories/variable_factory.py # api/pyproject.toml # api/services/variable_truncator.py # api/uv.lock # web/app/components/app/app-publisher/index.tsx # web/app/components/app/overview/settings/index.tsx # web/app/components/apps/app-card.tsx # web/app/components/apps/index.tsx # web/app/components/apps/list.tsx # web/app/components/base/chat/chat-with-history/header-in-mobile.tsx # web/app/components/base/features/new-feature-panel/conversation-opener/modal.tsx # web/app/components/base/features/new-feature-panel/file-upload/setting-content.tsx # web/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal.tsx # web/app/components/base/features/new-feature-panel/text-to-speech/param-config-content.tsx # web/app/components/base/message-log-modal/index.tsx # web/app/components/base/switch/index.tsx # web/app/components/base/tab-slider-plain/index.tsx # web/app/components/explore/try-app/app-info/index.tsx # web/app/components/plugins/plugin-detail-panel/tool-selector/components/reasoning-config-form.tsx # web/app/components/workflow/nodes/llm/components/json-schema-config-modal/visual-editor/edit-card/required-switch.tsx # web/app/components/workflow/nodes/llm/panel.tsx # web/contract/router.ts # web/eslint-suppressions.json # web/i18n/fa-IR/workflow.json
164 lines
4.8 KiB
TypeScript
164 lines
4.8 KiB
TypeScript
'use client'
|
|
|
|
import type { ChangeEvent, FC } from 'react'
|
|
import * as React from 'react'
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { cn } from '@/utils/classnames'
|
|
import { checkKeys } from '@/utils/var'
|
|
import VarHighlight from '../../app/configuration/base/var-highlight'
|
|
import Toast from '../toast'
|
|
|
|
// 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 = cn({
|
|
'block h-full w-full break-all border-0 px-4 py-2 text-sm text-gray-900 outline-0': true,
|
|
'block-input--editing': isEditing,
|
|
})
|
|
|
|
const renderSafeContent = (value: string) => {
|
|
const parts = value.split(/(\{\{[^}]+\}\}|\n)/g)
|
|
return parts.map((part, index) => {
|
|
const variableMatch = /^\{\{([^}]+)\}\}$/.exec(part)
|
|
if (variableMatch) {
|
|
return (
|
|
<VarHighlight
|
|
key={`var-${index}`}
|
|
name={variableMatch[1]}
|
|
/>
|
|
)
|
|
}
|
|
if (part === '\n')
|
|
return <br key={`br-${index}`} />
|
|
|
|
return <span key={`text-${index}`}>{part}</span>
|
|
})
|
|
}
|
|
|
|
// Not use useCallback. That will cause out callback get old data.
|
|
const handleSubmit = (value: string) => {
|
|
if (onConfirm) {
|
|
const keys = getInputKeys(value)
|
|
const result = checkKeys(keys)
|
|
if (!result.isValid) {
|
|
Toast.notify({
|
|
type: 'error',
|
|
message: t(`varKeyError.${result.errorMessageKey}`, { ns: 'appDebug', key: result.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={cn(style, className)}>
|
|
{renderSafeContent(currentValue || '')}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const placeholder = ''
|
|
const editAreaClassName = 'focus:outline-none bg-transparent text-sm'
|
|
|
|
const textAreaContent = (
|
|
<div className={cn(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={cn(editAreaClassName, 'block h-full w-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={cn('block-input w-full overflow-y-auto rounded-xl border-none bg-white')}>
|
|
{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)
|