Spaces:
Running
Running
| import { useEffect, useRef, useState } from 'react'; | |
| import { RefreshCw, X } from 'lucide-react'; | |
| import { imageUrlWithRetry } from '../app-core-utils.js'; | |
| export function GeneratedImage({ part, onPreviewImage }) { | |
| const [loadState, setLoadState] = useState('loading'); | |
| const [retryKey, setRetryKey] = useState(0); | |
| const src = imageUrlWithRetry(part.url, retryKey); | |
| return ( | |
| <button | |
| type="button" | |
| className={`message-image-link ${loadState === 'failed' ? 'is-failed' : ''}`} | |
| onClick={() => (loadState === 'failed' ? setRetryKey(Date.now()) : onPreviewImage(part))} | |
| aria-label={loadState === 'failed' ? '重新加载图片' : '预览图片'} | |
| > | |
| <img | |
| className="message-image" | |
| src={src} | |
| alt={part.alt} | |
| loading="eager" | |
| decoding="async" | |
| onLoad={() => setLoadState('loaded')} | |
| onError={() => setLoadState('failed')} | |
| /> | |
| {loadState === 'failed' ? ( | |
| <span className="image-error"> | |
| 图片加载失败 | |
| <span>点击重试</span> | |
| </span> | |
| ) : null} | |
| </button> | |
| ); | |
| } | |
| export function ImagePreviewModal({ image, onClose }) { | |
| const [loadState, setLoadState] = useState('loading'); | |
| const [retryKey, setRetryKey] = useState(0); | |
| const closeButtonRef = useRef(null); | |
| const previousFocusRef = useRef(null); | |
| useEffect(() => { | |
| setLoadState('loading'); | |
| setRetryKey(0); | |
| }, [image?.url]); | |
| useEffect(() => { | |
| if (!image) { | |
| return undefined; | |
| } | |
| previousFocusRef.current = document.activeElement; | |
| const previousOverflow = document.body.style.overflow; | |
| document.body.style.overflow = 'hidden'; | |
| const frame = window.requestAnimationFrame(() => closeButtonRef.current?.focus()); | |
| return () => { | |
| window.cancelAnimationFrame(frame); | |
| document.body.style.overflow = previousOverflow; | |
| previousFocusRef.current?.focus?.(); | |
| previousFocusRef.current = null; | |
| }; | |
| }, [image]); | |
| if (!image) { | |
| return null; | |
| } | |
| const src = imageUrlWithRetry(image.url, retryKey); | |
| return ( | |
| <div className="image-lightbox" role="dialog" aria-modal="true" onClick={onClose}> | |
| <div className="lightbox-top"> | |
| <button ref={closeButtonRef} type="button" className="lightbox-close" onClick={onClose} aria-label="关闭图片预览"> | |
| <X size={22} /> | |
| </button> | |
| </div> | |
| <div className="lightbox-stage" onClick={(event) => event.stopPropagation()}> | |
| <img | |
| src={src} | |
| alt={image.alt || '生成图片'} | |
| onLoad={() => setLoadState('loaded')} | |
| onError={() => setLoadState('failed')} | |
| /> | |
| </div> | |
| {loadState === 'failed' ? ( | |
| <div className="lightbox-actions" onClick={(event) => event.stopPropagation()}> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setLoadState('loading'); | |
| setRetryKey(Date.now()); | |
| }} | |
| > | |
| <RefreshCw size={16} /> | |
| 重新加载 | |
| </button> | |
| </div> | |
| ) : null} | |
| </div> | |
| ); | |
| } | |