Commit 085faf03 authored by fisherdaddy's avatar fisherdaddy

feat: enhance ImageAnnotator with responsive design, CORS handling, and error...

feat: enhance ImageAnnotator with responsive design, CORS handling, and error messaging for improved user experience
parent 79195e0f
...@@ -8,9 +8,19 @@ import LoadingOverlay from './LoadingOverlay'; ...@@ -8,9 +8,19 @@ import LoadingOverlay from './LoadingOverlay';
const Container = styled.div` const Container = styled.div`
min-height: 100vh; min-height: 100vh;
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%); background: linear-gradient(135deg, #f5f7ff 0%, #ffffff 100%);
padding: 4rem 2rem 2rem; padding: 4rem 2rem 2rem;
position: relative; position: relative;
overflow: hidden;
@media (max-width: 768px) {
height: auto;
min-height: 100vh;
overflow: auto;
}
&::before { &::before {
content: ''; content: '';
...@@ -31,26 +41,37 @@ const ContentWrapper = styled.div` ...@@ -31,26 +41,37 @@ const ContentWrapper = styled.div`
display: flex; display: flex;
gap: 2rem; gap: 2rem;
max-width: 1400px; max-width: 1400px;
width: 100%;
height: 100%;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
z-index: 1; z-index: 1;
flex: 1;
@media (max-width: 768px) { @media (max-width: 768px) {
flex-direction: column; flex-direction: column;
height: auto;
} }
`; `;
const InputContainer = styled.div` const InputContainer = styled.div`
flex: 1; flex: 1;
height: 100%;
display: flex;
flex-direction: column;
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 16px; border-radius: 16px;
padding: 1.5rem; padding: 1.5rem;
box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1); box-shadow: 0 8px 32px rgba(99, 102, 241, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
display: flex;
flex-direction: column;
gap: 1rem; gap: 1rem;
overflow: hidden;
@media (max-width: 768px) {
height: auto;
min-height: 300px;
}
`; `;
const TitleLabel = styled.h2` const TitleLabel = styled.h2`
...@@ -82,6 +103,7 @@ const CoordinatesSection = styled(Section)` ...@@ -82,6 +103,7 @@ const CoordinatesSection = styled(Section)`
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
`; `;
const UploadInput = styled.input` const UploadInput = styled.input`
...@@ -114,7 +136,8 @@ const UrlInput = styled.input` ...@@ -114,7 +136,8 @@ const UrlInput = styled.input`
const CoordinatesEditor = styled.textarea` const CoordinatesEditor = styled.textarea`
width: 100%; width: 100%;
height: 200px; flex: 1;
min-height: 150px;
padding: 1rem; padding: 1rem;
border: 1px solid rgba(99, 102, 241, 0.3); border: 1px solid rgba(99, 102, 241, 0.3);
border-radius: 8px; border-radius: 8px;
...@@ -122,7 +145,7 @@ const CoordinatesEditor = styled.textarea` ...@@ -122,7 +145,7 @@ const CoordinatesEditor = styled.textarea`
font-family: 'SF Mono', monospace; font-family: 'SF Mono', monospace;
font-size: 14px; font-size: 14px;
line-height: 1.5; line-height: 1.5;
resize: vertical; resize: none;
color: #1a1a1a; color: #1a1a1a;
overflow-y: auto; overflow-y: auto;
...@@ -138,10 +161,14 @@ const CoordinatesEditor = styled.textarea` ...@@ -138,10 +161,14 @@ const CoordinatesEditor = styled.textarea`
const PreviewContainer = styled(InputContainer)` const PreviewContainer = styled(InputContainer)`
position: relative; position: relative;
min-height: 400px;
overflow: visible;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
@media (max-width: 768px) {
height: 60vh;
min-height: 400px;
}
`; `;
const ButtonsContainer = styled.div` const ButtonsContainer = styled.div`
...@@ -157,20 +184,33 @@ const ImagePreview = styled.div` ...@@ -157,20 +184,33 @@ const ImagePreview = styled.div`
position: relative; position: relative;
margin: 0 auto; margin: 0 auto;
max-width: 100%; max-width: 100%;
max-height: 100%; height: 100%;
overflow: auto; overflow: auto;
flex: 1; flex: 1;
display: flex;
align-items: flex-start;
justify-content: center;
`; `;
const AnnotatedImage = styled.div` const AnnotatedImage = styled.div`
position: relative; position: relative;
display: inline-block; display: inline-block;
max-height: 100%;
`; `;
const Image = styled.img` const Image = styled.img`
display: block; display: block;
max-width: 100%; max-width: 100%;
max-height: 600px; max-height: calc(100vh - 200px);
object-fit: contain;
@media (min-width: 1200px) {
max-height: calc(100vh - 150px);
}
@media (max-width: 768px) {
max-height: calc(60vh - 100px);
}
`; `;
const BoundingBox = styled.div` const BoundingBox = styled.div`
...@@ -209,6 +249,19 @@ const InfoMessage = styled.div` ...@@ -209,6 +249,19 @@ const InfoMessage = styled.div`
padding: 2rem; padding: 2rem;
`; `;
const ImageInfo = styled.div`
position: absolute;
top: 100%;
left: 0;
background: rgba(0, 0, 0, 0.6);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
z-index: 20;
margin-top: 8px;
`;
const DownloadButton = styled.button` const DownloadButton = styled.button`
background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%); background: linear-gradient(135deg, #6366F1 0%, #4F46E5 100%);
color: white; color: white;
...@@ -257,6 +310,8 @@ function ImageAnnotator() { ...@@ -257,6 +310,8 @@ function ImageAnnotator() {
const [uploadedImage, setUploadedImage] = useState(null); const [uploadedImage, setUploadedImage] = useState(null);
const [coordinates, setCoordinates] = useState(''); const [coordinates, setCoordinates] = useState('');
const [error, setError] = useState(''); const [error, setError] = useState('');
const [imageError, setImageError] = useState('');
const [useCors, setUseCors] = useState(true);
const previewRef = useRef(null); const previewRef = useRef(null);
const imageRef = useRef(null); const imageRef = useRef(null);
const [imageSize, setImageSize] = useState({ width: 0, height: 0 }); const [imageSize, setImageSize] = useState({ width: 0, height: 0 });
...@@ -282,6 +337,38 @@ function ImageAnnotator() { ...@@ -282,6 +337,38 @@ function ImageAnnotator() {
const handleImageUrlChange = (e) => { const handleImageUrlChange = (e) => {
setImageUrl(e.target.value); setImageUrl(e.target.value);
setUploadedImage(null); setUploadedImage(null);
setImageError('');
setUseCors(true);
};
// Process image URL to handle CORS
const processImageUrl = (url) => {
if (!url) return '';
if (!useCors) {
return url;
}
try {
// For URLs that might have CORS issues, we can use a proxy
// This is a simple example - in production you might want to use your own proxy
const urlObj = new URL(url);
if (urlObj.origin !== window.location.origin) {
// For demo purposes we're using a public CORS proxy
// In production, replace this with your own proxy service
return `https://cors-anywhere.herokuapp.com/${url}`;
}
} catch (e) {
// Invalid URL, just return as is
}
return url;
};
// Reset all states
const handleReset = () => {
setSelectedBoxId(null);
setImageError('');
}; };
// Handle coordinates input // Handle coordinates input
...@@ -391,6 +478,19 @@ function ImageAnnotator() { ...@@ -391,6 +478,19 @@ function ImageAnnotator() {
width: img.naturalWidth, width: img.naturalWidth,
height: img.naturalHeight height: img.naturalHeight
}); });
setImageError('');
};
// Handle image load error
const handleImageError = () => {
if (useCors && imageUrl) {
// If loading with CORS fails, try without CORS
setUseCors(false);
setImageError(t('tools.imageAnnotator.tryingWithoutCors') || '正在尝试不使用跨域加载...');
} else {
setImageError(t('tools.imageAnnotator.imageLoadError') || '图片加载失败,可能是跨域问题或图片地址无效');
setImageSize({ width: 0, height: 0 });
}
}; };
// Handle image clicks (to deselect) // Handle image clicks (to deselect)
...@@ -435,10 +535,25 @@ function ImageAnnotator() { ...@@ -435,10 +535,25 @@ function ImageAnnotator() {
} }
}; };
const currentImageUrl = uploadedImage || (imageUrl.trim() && imageUrl); const currentImageUrl = uploadedImage || (imageUrl.trim() && (useCors ? processImageUrl(imageUrl) : imageUrl));
const hasImage = !!currentImageUrl; const hasImage = !!currentImageUrl;
const hasBoxes = parsedBoxes.length > 0; const hasBoxes = parsedBoxes.length > 0;
// When component mounts or window resizes, adjust container height
useEffect(() => {
const handleResize = () => {
if (previewRef.current && imageRef.current) {
// Update any responsive layout if needed
setImageSize(prev => ({...prev})); // Force re-render to update scaled dimensions
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return ( return (
<> <>
{isLoading && <LoadingOverlay />} {isLoading && <LoadingOverlay />}
...@@ -500,7 +615,7 @@ function ImageAnnotator() { ...@@ -500,7 +615,7 @@ function ImageAnnotator() {
</DownloadButton> </DownloadButton>
<ResetButton <ResetButton
onClick={() => setSelectedBoxId(null)} onClick={handleReset}
visible={hasImage && hasBoxes} visible={hasImage && hasBoxes}
> >
{t('tools.imageAnnotator.resetView') || '恢复视图'} {t('tools.imageAnnotator.resetView') || '恢复视图'}
...@@ -515,9 +630,31 @@ function ImageAnnotator() { ...@@ -515,9 +630,31 @@ function ImageAnnotator() {
alt="Uploaded image" alt="Uploaded image"
ref={imageRef} ref={imageRef}
onLoad={handleImageLoad} onLoad={handleImageLoad}
onError={handleImageError}
onClick={handleImageClick} onClick={handleImageClick}
crossOrigin="anonymous" crossOrigin={useCors ? "anonymous" : null}
/> />
{imageError && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
background: 'rgba(255, 0, 0, 0.7)',
color: 'white',
padding: '10px',
borderRadius: '4px',
maxWidth: '80%',
textAlign: 'center'
}}>
{imageError}
</div>
)}
{imageSize.width > 0 && (
<ImageInfo>
{imageSize.width} × {imageSize.height}
</ImageInfo>
)}
{hasBoxes && parsedBoxes.map((box) => { {hasBoxes && parsedBoxes.map((box) => {
// Determine label position based on box position // Determine label position based on box position
const isNearTop = box.y1 < 30; const isNearTop = box.y1 < 30;
......
...@@ -253,15 +253,15 @@ ...@@ -253,15 +253,15 @@
"copied": "Copied!" "copied": "Copied!"
}, },
"imageAnnotator": { "imageAnnotator": {
"title": "Image Annotator", "title": "Image Annotation Tool",
"description": "Upload an image and visualize bounding boxes using coordinates", "description": "Upload an image and visualize bounding boxes",
"uploadLabel": "Upload Image", "uploadLabel": "Upload Image",
"urlLabel": "Or Image URL", "urlLabel": "or Image URL",
"urlPlaceholder": "https://example.com/image.jpg", "urlPlaceholder": "https://example.com/image.jpg",
"coordinatesLabel": "Bounding Box Coordinates (x1,y1,x2,y2)", "coordinatesLabel": "Bounding Box Coordinates [x_min,y_min,x_max,y_max]",
"coordinatesPlaceholder": "Enter coordinates in JSON format: [[x1,y1,x2,y2], ...] or one box per line", "coordinatesPlaceholder": "Enter coordinates in JSON format: [[x_min,y_min,x_max,y_max], ...] or one box per line",
"downloadButton": "Download", "downloadButton": "Download",
"noImageMessage": "Upload an image or provide an image URL to begin", "noImageMessage": "Upload an image or provide an image URL to start",
"resetView": "Reset View" "resetView": "Reset View"
}, },
"aiTimeline": { "aiTimeline": {
......
...@@ -254,11 +254,14 @@ ...@@ -254,11 +254,14 @@
}, },
"imageAnnotator": { "imageAnnotator": {
"title": "画像アノテーションツール", "title": "画像アノテーションツール",
"description": "画像に境界ボックスを追加して編集できます", "description": "画像をアップロードしてバウンディングボックスを可視化します",
"uploadImage": "画像をアップロード", "uploadLabel": "画像をアップロード",
"dropOrClick": "ドラッグまたはクリックして画像をアップロード", "urlLabel": "または画像URL",
"urlPlaceholder": "https://example.com/image.jpg",
"coordinatesLabel": "バウンディングボックス座標 [x_min,y_min,x_max,y_max]",
"coordinatesPlaceholder": "JSON形式で座標を入力:[[x_min,y_min,x_max,y_max], ...] または1行に1つのボックスを入力",
"downloadButton": "ダウンロード", "downloadButton": "ダウンロード",
"noImageMessage": "画像をアップロードするか、画像URLを提供して開始してください", "noImageMessage": "開始するには画像をアップロードするか、画像URLを入力してください",
"resetView": "ビューをリセット" "resetView": "ビューをリセット"
}, },
"aiTimeline": { "aiTimeline": {
......
...@@ -254,12 +254,15 @@ ...@@ -254,12 +254,15 @@
"copied": "복사 완료!" "copied": "복사 완료!"
}, },
"imageAnnotator": { "imageAnnotator": {
"title": "이미지 어노테이터", "title": "이미지 주석 도구",
"description": "이미지에 경계 상자를 추가하고 편집합니다", "description": "이미지를 업로드하고 바운딩 박스를 시각화합니다",
"uploadImage": "이미지 업로드", "uploadLabel": "이미지 업로드",
"dropOrClick": "드래그하거나 클릭하여 이미지 업로드", "urlLabel": "또는 이미지 URL",
"urlPlaceholder": "https://example.com/image.jpg",
"coordinatesLabel": "바운딩 박스 좌표 [x_min,y_min,x_max,y_max]",
"coordinatesPlaceholder": "좌표를 JSON 형식으로 입력: [[x_min,y_min,x_max,y_max], ...] 또는 한 줄에 하나의 박스 입력",
"downloadButton": "다운로드", "downloadButton": "다운로드",
"noImageMessage": "이미지를 업로드하거나 이미지 URL을 제공하세요", "noImageMessage": "시작하려면 이미지를 업로드하거나 이미지 URL을 입력하세요",
"resetView": "뷰 초기화" "resetView": "뷰 초기화"
}, },
"aiTimeline": { "aiTimeline": {
......
...@@ -260,8 +260,8 @@ ...@@ -260,8 +260,8 @@
"uploadLabel": "上传图片", "uploadLabel": "上传图片",
"urlLabel": "或图片URL", "urlLabel": "或图片URL",
"urlPlaceholder": "https://example.com/image.jpg", "urlPlaceholder": "https://example.com/image.jpg",
"coordinatesLabel": "边界框坐标 (x1,y1,x2,y2)", "coordinatesLabel": "边界框坐标 [x_min,y_min,x_max,y_max]",
"coordinatesPlaceholder": "输入坐标,JSON格式:[[x1,y1,x2,y2], ...] 或每行输入一个框", "coordinatesPlaceholder": "输入坐标,JSON格式:[[x_min,y_min,x_max,y_max], ...] 或每行输入一个框",
"downloadButton": "下载", "downloadButton": "下载",
"noImageMessage": "上传图片或提供图片URL开始", "noImageMessage": "上传图片或提供图片URL开始",
"resetView": "重置视图" "resetView": "重置视图"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment