diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 32ca62caa3bfd16f2ad60a3cd4db4a09efc76562..069b8c561cb523d2a4e42edda319e65cdfed41ec 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,10 +11,13 @@ "@ifrc-go/icons": "^2.0.1", "@ifrc-go/ui": "^1.3.0", "@types/jszip": "^3.4.0", + "cropperjs": "^2.0.1", "jszip": "^3.10.1", "lucide-react": "^0.525.0", "react": "^18.2.0", + "react-cropper": "^2.3.3", "react-dom": "^18.2.0", + "react-easy-crop": "^5.5.0", "react-router-dom": "^6.30.1", "vite": "^7.1.3" }, @@ -420,6 +423,126 @@ "node": ">=6.9.0" } }, + "node_modules/@cropper/element": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element/-/element-2.0.1.tgz", + "integrity": "sha512-Jn1hR7XWzWQM/QfXRGMGzdkJ2gG/UcLdQPZQ7OKs0JiFfRzKpzu4u/nYrXHeH3MM2iOslLqh2kqYju6mjZLMJQ==", + "license": "MIT", + "dependencies": { + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/element-canvas": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element-canvas/-/element-canvas-2.0.1.tgz", + "integrity": "sha512-OKxq/O0HL9W2JegOsc2zh1NRpERZcLM5+M8aQ/eXdmMcfi1lzosPftag3Irp6pTsVpwV6B6ypIxKESzJ4ci9Fw==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/element-crosshair": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element-crosshair/-/element-crosshair-2.0.1.tgz", + "integrity": "sha512-bS5msU9cTU/jf1/kDw+QJmEM9/rw8IgOdpolR85iMVUCR8sRcLa0wgom42MBHcpBYB6hvL5YfiOeXZ7lHIYMpw==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/element-grid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element-grid/-/element-grid-2.0.1.tgz", + "integrity": "sha512-ayqCvYQJ+GVT31HhFpttzHabW1T/LsIwLJY5PLTMG0cEZLw/E8ihg8mxctjZbo852D7oEePbz6/2SeuCb1018Q==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/element-handle": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element-handle/-/element-handle-2.0.1.tgz", + "integrity": "sha512-fdifyyPIaR9S2eQ7qPHuM8fX8uToAfBsi8vQlR9EM+oJkDNil0uO4rWyArLWEtlr0/q7U0OvsufcuJ7ffqfmpg==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/element-image": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element-image/-/element-image-2.0.1.tgz", + "integrity": "sha512-gPj5Sl2T8Cno198Cz3F3TDfcYoALW3yJ3fV6PHXmhMnX8sBkL7J441do7Vwkg0mEd2CogCtTLAf+p7ljdV0kgA==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/element-canvas": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/element-selection": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element-selection/-/element-selection-2.0.1.tgz", + "integrity": "sha512-atv+Aeq2N2eWawelIRPGh1kYFdNrpb0QkUPPheGxz1ImfxpLdcHO9gb9T5noQijizUW2G0pNvts4ZaITQ0I71Q==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/element-canvas": "^2.0.1", + "@cropper/element-image": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/element-shade": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element-shade/-/element-shade-2.0.1.tgz", + "integrity": "sha512-YIYgJ690NdFQ6wJLRFh/EySNVxGFKArncQ4FrsJ3yHU+ShgtOKz4FpjFLpqJRJB9swoVbD3WKTimGyzXrwjZrQ==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/element-canvas": "^2.0.1", + "@cropper/element-selection": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/element-viewer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/element-viewer/-/element-viewer-2.0.1.tgz", + "integrity": "sha512-HDj25l08pWi/AO6El/OqfQHBpBC4Lh5NEnQN1SOldsmxEwt27Ubv6ndDsF8LkTK7XPwjjZRpyQPyfig4w8L2JQ==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/element-canvas": "^2.0.1", + "@cropper/element-image": "^2.0.1", + "@cropper/element-selection": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, + "node_modules/@cropper/elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/elements/-/elements-2.0.1.tgz", + "integrity": "sha512-paFbBLXTKXNngn1yDi2ZIf+FO1pIEQXyBntmqOjuxqtG73KuEKv633wsJPFpj958bgcfSakgBbF80j+3nHbPug==", + "license": "MIT", + "dependencies": { + "@cropper/element": "^2.0.1", + "@cropper/element-canvas": "^2.0.1", + "@cropper/element-crosshair": "^2.0.1", + "@cropper/element-grid": "^2.0.1", + "@cropper/element-handle": "^2.0.1", + "@cropper/element-image": "^2.0.1", + "@cropper/element-selection": "^2.0.1", + "@cropper/element-shade": "^2.0.1", + "@cropper/element-viewer": "^2.0.1" + } + }, + "node_modules/@cropper/utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@cropper/utils/-/utils-2.0.1.tgz", + "integrity": "sha512-A9RnAFmgNF5aZk5q2VZnFnHtXWu1kPyEN0LVsX8wJ2LBRu2nyETKwz+ZXVsVWliktToCaYojHKrS+6/HODyEZA==", + "license": "MIT" + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -3077,6 +3200,16 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, + "node_modules/cropperjs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-2.0.1.tgz", + "integrity": "sha512-hiJwk2SCPZqxMA7aR3byzLpYUqOrQo+ihMk8k/WRm/xe/LX8wNzAIzMwEB/NEGJYA6sbewxW9TUlrRUYi/2Ipg==", + "license": "MIT", + "dependencies": { + "@cropper/elements": "^2.0.1", + "@cropper/utils": "^2.0.1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -4617,6 +4750,12 @@ "node": ">=0.10.0" } }, + "node_modules/normalize-wheel": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", + "integrity": "sha512-1OnlAPZ3zgrk8B91HyRj+eVv+kS5u+Z0SCsak6Xil/kmgEia50ga7zfkumayonZrImffAxPU/5WcyGhzetHNPA==", + "license": "BSD-3-Clause" + }, "node_modules/nwsapi": { "version": "2.2.21", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", @@ -4945,6 +5084,24 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/react-cropper": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/react-cropper/-/react-cropper-2.3.3.tgz", + "integrity": "sha512-zghiEYkUb41kqtu+2jpX2Ntigf+Jj1dF9ew4lAobPzI2adaPE31z0p+5TcWngK6TvmWQUwK3lj4G+NDh1PDQ1w==", + "license": "MIT", + "dependencies": { + "cropperjs": "^1.5.13" + }, + "peerDependencies": { + "react": ">=17.0.2" + } + }, + "node_modules/react-cropper/node_modules/cropperjs": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz", + "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==", + "license": "MIT" + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -4958,6 +5115,20 @@ "react": "^18.2.0" } }, + "node_modules/react-easy-crop": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-5.5.0.tgz", + "integrity": "sha512-OZzU+yXMhe69vLkDex+5QxcfT94FdcgVCyW2dBUw35ZoC3Is42TUxUy04w8nH1mfMKaizVdC3rh/wUfNW1mK4w==", + "license": "MIT", + "dependencies": { + "normalize-wheel": "^1.0.1", + "tslib": "^2.0.1" + }, + "peerDependencies": { + "react": ">=16.4.0", + "react-dom": ">=16.4.0" + } + }, "node_modules/react-focus-lock": { "version": "2.13.6", "resolved": "https://registry.npmjs.org/react-focus-lock/-/react-focus-lock-2.13.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index dce97436989ede9d264d18a54df8a8ab7d61a3f6..14757c694c9ea88d218e35208ecbbf8703987795 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,10 +44,13 @@ "@ifrc-go/icons": "^2.0.1", "@ifrc-go/ui": "^1.3.0", "@types/jszip": "^3.4.0", + "cropperjs": "^2.0.1", "jszip": "^3.10.1", "lucide-react": "^0.525.0", "react": "^18.2.0", + "react-cropper": "^2.3.3", "react-dom": "^18.2.0", + "react-easy-crop": "^5.5.0", "react-router-dom": "^6.30.1", "vite": "^7.1.3" } diff --git a/frontend/src/components/FilterBar.tsx b/frontend/src/components/FilterBar.tsx index 0f45837cbd9e5e5c6aa83af623a6fbd12744f0d0..c0eaba5ebb82dc9b20b7f8a45be1d30f3818d201 100644 --- a/frontend/src/components/FilterBar.tsx +++ b/frontend/src/components/FilterBar.tsx @@ -1,5 +1,6 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Container, TextInput, SelectInput, MultiSelectInput, Button } from '@ifrc-go/ui'; +import { FilterLineIcon } from '@ifrc-go/icons'; import { useFilterContext } from '../hooks/useFilterContext'; interface FilterBarProps { @@ -19,6 +20,8 @@ export default function FilterBar({ imageTypes, isLoadingFilters = false }: FilterBarProps) { + const [showFilters, setShowFilters] = useState(false); + const { search, setSearch, srcFilter, setSrcFilter, @@ -26,14 +29,27 @@ export default function FilterBar({ regionFilter, setRegionFilter, countryFilter, setCountryFilter, imageTypeFilter, setImageTypeFilter, + uploadTypeFilter, setUploadTypeFilter, showReferenceExamples, setShowReferenceExamples, clearAllFilters } = useFilterContext(); return (
- {/* Layer 1: Search, Reference Examples, Clear Filters */} + {/* Layer 1: Search, Filter Button, Clear Filters */}
+ + + + - -
- {/* Layer 2: 5 Filter Bars */} -
- + {/* Layer 2: Filter Dropdown */} + {showFilters && ( +
+
+ - + - + - + - + -
+ + + setUploadTypeFilter(v as string || '')} + keySelector={(o) => o.key} + labelSelector={(o) => o.label} + required={false} + disabled={false} + /> + +
+
+ )}
); } diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..d353003d48f1e87f16943e6c3b99b0aee9378d95 --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,7 @@ +// Main components +export { default as FilterBar } from './FilterBar'; +export { default as HeaderNav } from './HeaderNav'; +export { default as ExportModal } from './ExportModal'; + +// Upload components +export * from './upload'; diff --git a/frontend/src/components/upload/FileUploadSection.tsx b/frontend/src/components/upload/FileUploadSection.tsx new file mode 100644 index 0000000000000000000000000000000000000000..83b8259c7e4731fa70c0071eaa1116840faf2fb0 --- /dev/null +++ b/frontend/src/components/upload/FileUploadSection.tsx @@ -0,0 +1,169 @@ +import { useState } from 'react'; +import type { DragEvent } from 'react'; +import { Button, Container, SegmentInput, IconButton } from '@ifrc-go/ui'; +import { UploadCloudLineIcon, ArrowRightLineIcon, DeleteBinLineIcon } from '@ifrc-go/icons'; +import { Link } from 'react-router-dom'; +import styles from '../../pages/UploadPage/UploadPage.module.css'; + +interface FileUploadSectionProps { + files: File[]; + file: File | null; + preview: string | null; + imageType: string; + onFileChange: (file: File | undefined) => void; + onRemoveImage: (index: number) => void; + onAddImage: () => void; + onImageTypeChange: (value: string | undefined) => void; + + onChangeFile?: (file: File | undefined) => void; +} + +export default function FileUploadSection({ + files, + file, + preview, + imageType, + onFileChange, + onRemoveImage, + onAddImage, + onImageTypeChange, + + onChangeFile, +}: FileUploadSectionProps) { + const onDrop = (e: DragEvent) => { + e.preventDefault(); + const dropped = e.dataTransfer.files?.[0]; + if (dropped) { + onFileChange(dropped); + } + }; + + return ( +
+

+ This app evaluates how well multimodal AI models analyze and describe + crisis maps and drone imagery. Upload an image and the AI will generate a description. + Then you can review and rate the result based on your expertise. +

+ + {/* "More »" link */} +
+ + More + +
+ + {/* Image Type Selection */} +
+ + onImageTypeChange(value as string)} + options={[ + { key: 'crisis_map', label: 'Crisis Maps' }, + { key: 'drone_image', label: 'Drone Imagery' } + ]} + keySelector={(o) => o.key} + labelSelector={(o) => o.label} + /> + +
+ +
e.preventDefault()} + onDrop={onDrop} + > + {files.length > 1 ? ( +
+ {files.map((file, index) => ( +
+ {`Image + onRemoveImage(index)} + title="Remove image" + ariaLabel="Remove image" + className="absolute top-2 right-2 bg-white/90 hover:bg-white shadow-md hover:shadow-lg border border-gray-200 hover:border-red-300 transition-all duration-200 backdrop-blur-sm" + > + + +
{file.name}
+
+ ))} +
+ ) : file && preview ? ( +
+
+ File preview +
+

+ {file.name} +

+

+ {(file.size / 1024 / 1024).toFixed(2)} MB +

+
+ ) : ( + <> + +

Drag & Drop any file here

+

or

+ + )} + +
+ + + {file && files.length < 5 && ( + + )} + + +
+
+
+ ); +} diff --git a/frontend/src/components/upload/GeneratedTextSection.tsx b/frontend/src/components/upload/GeneratedTextSection.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c5d187bfdbf503492122022a6530c79109936fef --- /dev/null +++ b/frontend/src/components/upload/GeneratedTextSection.tsx @@ -0,0 +1,102 @@ +import { Container, TextArea, Button, IconButton } from '@ifrc-go/ui'; +import { DeleteBinLineIcon } from '@ifrc-go/icons'; +import styles from '../../pages/UploadPage/UploadPage.module.css'; + +interface GeneratedTextSectionProps { + description: string; + analysis: string; + recommendedActions: string; + onDescriptionChange: (value: string | undefined) => void; + onAnalysisChange: (value: string | undefined) => void; + onRecommendedActionsChange: (value: string | undefined) => void; + onBack: () => void; + onDelete: () => void; + onSubmit: () => void; + onEditRatings?: () => void; + isPerformanceConfirmed?: boolean; +} + +export default function GeneratedTextSection({ + description, + analysis, + recommendedActions, + onDescriptionChange, + onAnalysisChange, + onRecommendedActionsChange, + onBack, + onDelete, + onSubmit, + onEditRatings, + isPerformanceConfirmed = false, +}: GeneratedTextSectionProps) { + const handleTextChange = (value: string | undefined) => { + if (value) { + const lines = value.split('\n'); + const descIndex = lines.findIndex(line => line.startsWith('Description:')); + const analysisIndex = lines.findIndex(line => line.startsWith('Analysis:')); + const actionsIndex = lines.findIndex(line => line.startsWith('Recommended Actions:')); + + if (descIndex !== -1 && analysisIndex !== -1 && actionsIndex !== -1) { + onDescriptionChange(lines.slice(descIndex + 1, analysisIndex).join('\n').trim()); + onAnalysisChange(lines.slice(analysisIndex + 1, actionsIndex).join('\n').trim()); + onRecommendedActionsChange(lines.slice(actionsIndex + 1).join('\n').trim()); + } + } + }; + + return ( + +
+
+