|
import { |
|
memo, |
|
useCallback, |
|
useRef, |
|
} from 'react' |
|
import { useTranslation } from 'react-i18next' |
|
import { useClickAway } from 'ahooks' |
|
import type { NodeProps } from 'reactflow' |
|
import NodeResizer from '../nodes/_base/components/node-resizer' |
|
import { |
|
useNodeDataUpdate, |
|
useNodesInteractions, |
|
} from '../hooks' |
|
import { useStore } from '../store' |
|
import { |
|
NoteEditor, |
|
NoteEditorContextProvider, |
|
NoteEditorToolbar, |
|
} from './note-editor' |
|
import { THEME_MAP } from './constants' |
|
import { useNote } from './hooks' |
|
import type { NoteNodeType } from './types' |
|
import cn from '@/utils/classnames' |
|
|
|
const Icon = () => { |
|
return ( |
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18" fill="none"> |
|
<path fillRule="evenodd" clipRule="evenodd" d="M12 9.75V6H13.5V9.75C13.5 11.8211 11.8211 13.5 9.75 13.5H6V12H9.75C10.9926 12 12 10.9926 12 9.75Z" fill="black" fillOpacity="0.16" /> |
|
</svg> |
|
) |
|
} |
|
|
|
const NoteNode = ({ |
|
id, |
|
data, |
|
}: NodeProps<NoteNodeType>) => { |
|
const { t } = useTranslation() |
|
const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey) |
|
const ref = useRef<HTMLDivElement | null>(null) |
|
const theme = data.theme |
|
const { |
|
handleThemeChange, |
|
handleEditorChange, |
|
handleShowAuthorChange, |
|
} = useNote(id) |
|
const { |
|
handleNodesCopy, |
|
handleNodesDuplicate, |
|
handleNodeDelete, |
|
} = useNodesInteractions() |
|
const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() |
|
|
|
const handleDeleteNode = useCallback(() => { |
|
handleNodeDelete(id) |
|
}, [id, handleNodeDelete]) |
|
|
|
useClickAway(() => { |
|
handleNodeDataUpdateWithSyncDraft({ id, data: { selected: false } }) |
|
}, ref) |
|
|
|
return ( |
|
<div |
|
className={cn( |
|
'flex flex-col relative rounded-md shadow-xs border hover:shadow-md', |
|
)} |
|
style={{ |
|
background: THEME_MAP[theme].bg, |
|
borderColor: data.selected ? THEME_MAP[theme].border : 'rgba(0, 0, 0, 0.05)', |
|
width: data.width, |
|
height: data.height, |
|
}} |
|
ref={ref} |
|
> |
|
<NoteEditorContextProvider |
|
key={controlPromptEditorRerenderKey} |
|
value={data.text} |
|
> |
|
<> |
|
<NodeResizer |
|
nodeId={id} |
|
nodeData={data} |
|
icon={<Icon />} |
|
minWidth={240} |
|
maxWidth={640} |
|
minHeight={88} |
|
/> |
|
<div className='shrink-0 h-2 opacity-50 rounded-t-md' style={{ background: THEME_MAP[theme].title }}></div> |
|
{ |
|
data.selected && ( |
|
<div className='absolute -top-[41px] left-1/2 -translate-x-1/2'> |
|
<NoteEditorToolbar |
|
theme={theme} |
|
onThemeChange={handleThemeChange} |
|
onCopy={handleNodesCopy} |
|
onDuplicate={handleNodesDuplicate} |
|
onDelete={handleDeleteNode} |
|
showAuthor={data.showAuthor} |
|
onShowAuthorChange={handleShowAuthorChange} |
|
/> |
|
</div> |
|
) |
|
} |
|
<div className='grow px-3 py-2.5 overflow-y-auto'> |
|
<div className={cn( |
|
data.selected && 'nodrag nopan nowheel cursor-text', |
|
)}> |
|
<NoteEditor |
|
containerElement={ref.current} |
|
placeholder={t('workflow.nodes.note.editor.placeholder') || ''} |
|
onChange={handleEditorChange} |
|
/> |
|
</div> |
|
</div> |
|
{ |
|
data.showAuthor && ( |
|
<div className='p-3 pt-0 text-xs text-black/[0.32]'> |
|
{data.author} |
|
</div> |
|
) |
|
} |
|
</> |
|
</NoteEditorContextProvider> |
|
</div> |
|
) |
|
} |
|
|
|
export default memo(NoteNode) |
|
|