diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx index acb4a9c4f1..13d13c55a9 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsCard.tsx @@ -2,7 +2,7 @@ import { CodeOutlined } from '@ant-design/icons' import { loggerService } from '@logger' import { useTheme } from '@renderer/context/ThemeProvider' import { ThemeMode } from '@renderer/types' -import { extractTitle } from '@renderer/utils/formats' +import { extractHtmlTitle, getFileNameFromHtmlTitle } from '@renderer/utils/formats' import { Button } from 'antd' import { Code, DownloadIcon, Globe, LinkIcon, Sparkles } from 'lucide-react' import { FC, useState } from 'react' @@ -28,7 +28,7 @@ const getTerminalStyles = (theme: ThemeMode) => ({ const HtmlArtifactsCard: FC = ({ html, onSave, isStreaming = false }) => { const { t } = useTranslation() - const title = extractTitle(html) || 'HTML Artifacts' + const title = extractHtmlTitle(html) || 'HTML Artifacts' const [isPopupOpen, setIsPopupOpen] = useState(false) const { theme } = useTheme() @@ -48,7 +48,7 @@ const HtmlArtifactsCard: FC = ({ html, onSave, isStreaming = false }) => } const handleDownload = async () => { - const fileName = `${title.replace(/[^a-zA-Z0-9\s]/g, '').replace(/\s+/g, '-') || 'html-artifact'}.html` + const fileName = `${getFileNameFromHtmlTitle(title) || 'html-artifact'}.html` await window.api.file.save(fileName, htmlContent) window.message.success({ content: t('message.download.success'), key: 'download' }) } diff --git a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx index 216e247701..8cdf4e4d45 100644 --- a/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx +++ b/src/renderer/src/components/CodeBlockView/HtmlArtifactsPopup.tsx @@ -1,9 +1,13 @@ import CodeEditor, { CodeEditorHandles } from '@renderer/components/CodeEditor' +import { CopyIcon, FilePngIcon } from '@renderer/components/Icons' import { isLinux, isMac, isWin } from '@renderer/config/constant' +import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue' import { classNames } from '@renderer/utils' -import { Button, Modal, Splitter, Tooltip, Typography } from 'antd' -import { Code, Eye, Maximize2, Minimize2, SaveIcon, SquareSplitHorizontal, X } from 'lucide-react' -import { useEffect, useRef, useState } from 'react' +import { extractHtmlTitle, getFileNameFromHtmlTitle } from '@renderer/utils/formats' +import { captureScrollableIframeAsBlob, captureScrollableIframeAsDataURL } from '@renderer/utils/image' +import { Button, Dropdown, Modal, Splitter, Tooltip, Typography } from 'antd' +import { Camera, Check, Code, Eye, Maximize2, Minimize2, SaveIcon, SquareSplitHorizontal, X } from 'lucide-react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' @@ -21,7 +25,9 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht const { t } = useTranslation() const [viewMode, setViewMode] = useState('split') const [isFullscreen, setIsFullscreen] = useState(false) + const [saved, setSaved] = useTemporaryValue(false, 2000) const codeEditorRef = useRef(null) + const previewFrameRef = useRef(null) // Prevent body scroll when fullscreen useEffect(() => { @@ -38,8 +44,32 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht const handleSave = () => { codeEditorRef.current?.save?.() + setSaved(true) } + const handleCapture = useCallback( + async (to: 'file' | 'clipboard') => { + const title = extractHtmlTitle(html) + const fileName = getFileNameFromHtmlTitle(title) || 'html-artifact' + + if (to === 'file') { + const dataUrl = await captureScrollableIframeAsDataURL(previewFrameRef) + if (dataUrl) { + window.api.file.saveImage(fileName, dataUrl) + } + } + if (to === 'clipboard') { + await captureScrollableIframeAsBlob(previewFrameRef, async (blob) => { + if (blob) { + await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]) + window.message.success(t('message.copy.success')) + } + }) + } + }, + [html, t] + ) + const renderHeader = () => ( setIsFullscreen(!isFullscreen)} className={classNames({ drag: isFullscreen })}> @@ -47,7 +77,7 @@ const HtmlArtifactsPopup: React.FC = ({ open, title, ht - + e.stopPropagation()}> = ({ open, title, ht - + e.stopPropagation()}> + , + onClick: () => handleCapture('file') + }, + { + label: t('html_artifacts.capture.to_clipboard'), + key: 'capture_to_clipboard', + icon: , + onClick: () => handleCapture('clipboard') + } + ] + }}> + +