| 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) |
|
|