import type { FC } from 'react' import { memo, useCallback, useMemo, useState, } from 'react' import { useShallow } from 'zustand/react/shallow' import { useTranslation } from 'react-i18next' import { RiExportLine, RiMoreFill } from '@remixicon/react' import { toJpeg, toPng, toSvg } from 'html-to-image' import { useNodesReadOnly } from '../hooks' import TipPopup from './tip-popup' import cn from '@/utils/classnames' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { getNodesBounds, useReactFlow } from 'reactflow' import ImagePreview from '@/app/components/base/image-uploader/image-preview' import { useStore } from '@/app/components/workflow/store' import { useStore as useAppStore } from '@/app/components/app/store' const MoreActions: FC = () => { const { t } = useTranslation() const { getNodesReadOnly } = useNodesReadOnly() const reactFlow = useReactFlow() const [open, setOpen] = useState(false) const [previewUrl, setPreviewUrl] = useState('') const [previewTitle, setPreviewTitle] = useState('') const knowledgeName = useStore(s => s.knowledgeName) const appName = useStore(s => s.appName) const maximizeCanvas = useStore(s => s.maximizeCanvas) const { appSidebarExpand } = useAppStore(useShallow(state => ({ appSidebarExpand: state.appSidebarExpand, }))) const crossAxisOffset = useMemo(() => { if (maximizeCanvas) return 40 return appSidebarExpand === 'expand' ? 188 : 40 }, [appSidebarExpand, maximizeCanvas]) const handleExportImage = useCallback(async (type: 'png' | 'jpeg' | 'svg', currentWorkflow = false) => { if (!appName && !knowledgeName) return if (getNodesReadOnly()) return setOpen(false) const flowElement = document.querySelector('.react-flow__viewport') as HTMLElement if (!flowElement) return try { let filename = appName || knowledgeName const filter = (node: HTMLElement) => { if (node instanceof HTMLImageElement) return node.complete && node.naturalHeight !== 0 return true } let dataUrl if (currentWorkflow) { const nodes = reactFlow.getNodes() const nodesBounds = getNodesBounds(nodes) const currentViewport = reactFlow.getViewport() const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight const zoom = Math.min( viewportWidth / (nodesBounds.width + 100), viewportHeight / (nodesBounds.height + 100), 1, ) const centerX = nodesBounds.x + nodesBounds.width / 2 const centerY = nodesBounds.y + nodesBounds.height / 2 reactFlow.setViewport({ x: viewportWidth / 2 - centerX * zoom, y: viewportHeight / 2 - centerY * zoom, zoom, }) await new Promise(resolve => setTimeout(resolve, 300)) const padding = 50 const contentWidth = nodesBounds.width + padding * 2 const contentHeight = nodesBounds.height + padding * 2 const exportOptions = { filter, backgroundColor: '#1a1a1a', pixelRatio: 2, width: contentWidth, height: contentHeight, style: { width: `${contentWidth}px`, height: `${contentHeight}px`, transform: `translate(${padding - nodesBounds.x}px, ${padding - nodesBounds.y}px)`, transformOrigin: 'top left', }, } switch (type) { case 'png': dataUrl = await toPng(flowElement, exportOptions) break case 'jpeg': dataUrl = await toJpeg(flowElement, exportOptions) break case 'svg': dataUrl = await toSvg(flowElement, { filter }) break default: dataUrl = await toPng(flowElement, exportOptions) } filename += '-whole-workflow' setTimeout(() => { reactFlow.setViewport(currentViewport) }, 500) } else { // Current viewport export (existing functionality) switch (type) { case 'png': dataUrl = await toPng(flowElement, { filter }) break case 'jpeg': dataUrl = await toJpeg(flowElement, { filter }) break case 'svg': dataUrl = await toSvg(flowElement, { filter }) break default: dataUrl = await toPng(flowElement, { filter }) } } if (currentWorkflow) { setPreviewUrl(dataUrl) setPreviewTitle(`${filename}.${type}`) const link = document.createElement('a') link.href = dataUrl link.download = `${filename}.${type}` document.body.appendChild(link) link.click() document.body.removeChild(link) } else { // For current view, just download const link = document.createElement('a') link.href = dataUrl link.download = `${filename}.${type}` document.body.appendChild(link) link.click() document.body.removeChild(link) } } catch (error) { console.error('Export image failed:', error) } }, [getNodesReadOnly, appName, reactFlow, knowledgeName]) const handleTrigger = useCallback(() => { if (getNodesReadOnly()) return setOpen(v => !v) }, [getNodesReadOnly]) return ( <>
{t('workflow.common.exportImage')}
{t('workflow.common.currentView')}
handleExportImage('png')} > {t('workflow.common.exportPNG')}
handleExportImage('jpeg')} > {t('workflow.common.exportJPEG')}
handleExportImage('svg')} > {t('workflow.common.exportSVG')}
{t('workflow.common.currentWorkflow')}
handleExportImage('png', true)} > {t('workflow.common.exportPNG')}
handleExportImage('jpeg', true)} > {t('workflow.common.exportJPEG')}
handleExportImage('svg', true)} > {t('workflow.common.exportSVG')}
{previewUrl && ( setPreviewUrl('')} /> )} ) } export default memo(MoreActions)