diff --git a/src/components/Catalogue/metadataDisplay.jsx b/src/components/Catalogue/metadataDisplay.jsx index 9e1972a..5ab3368 100644 --- a/src/components/Catalogue/metadataDisplay.jsx +++ b/src/components/Catalogue/metadataDisplay.jsx @@ -113,7 +113,7 @@ function MetadataDisplay({ currentItem, isOpen }) { } // Success case - setMetadata(response.data.raw.data); + setMetadata(response.data.raw.data.metadata); } else { throw new Error("Unexpected response format"); } @@ -245,6 +245,14 @@ function MetadataDisplay({ currentItem, isOpen }) { Caption: {metadata.caption || "N/A"} + {/* Additional figure metadata, if present */} + {metadata.addInfo && ( + <> + Additional Info: + {metadata.addInfo} + + )} + {/* Figure headshots (if available) */} {Array.isArray(metadata.figureIDs) && metadata.figureIDs.length > 0 ? ( @@ -277,14 +285,6 @@ function MetadataDisplay({ currentItem, isOpen }) { ) : ( N/A )} - - {/* Additional figure metadata, if present */} - {metadata.addInfo && ( - <> - Additional Info: - {metadata.addInfo} - - )} )} diff --git a/src/components/Catalogue/transcriptionToggle.jsx b/src/components/Catalogue/transcriptionToggle.jsx index adec14b..b1ec21d 100644 --- a/src/components/Catalogue/transcriptionToggle.jsx +++ b/src/components/Catalogue/transcriptionToggle.jsx @@ -57,7 +57,7 @@ const TranscriptionToggle = ({ value, onChange }) => { p="1" rounded="lg" boxShadow="sm" - gap="00" + gap="0" > diff --git a/src/components/DataStudio/artefactEditorActionBar.jsx b/src/components/DataStudio/artefactEditorActionBar.jsx new file mode 100644 index 0000000..0188d80 --- /dev/null +++ b/src/components/DataStudio/artefactEditorActionBar.jsx @@ -0,0 +1,222 @@ +import { ActionBar, Button, Dialog, Portal } from '@chakra-ui/react' +import { isEqual } from 'lodash'; +import { useEffect, useState } from 'react' +import ToastWizard from '../toastWizard'; +import server, { JSONResponse } from '../../networking'; + +function ArtefactEditorActionBar({ artefactData, originalArtefactData, setArtefactData, getArtefactData, artefactId, isEditing, setIsEditing, isMMArtefact }) { + const [changesMade, setChangesMade] = useState(false); + const [isConfirmSaveOpen, setIsConfirmSaveOpen] = useState(false); + const [saveLoading, setSaveLoading] = useState(false); + + useEffect(() => { + setChangesMade(!isEqual(artefactData, originalArtefactData) && isEditing); + }, [artefactData, originalArtefactData, isEditing]) + + // Validation functions + const validateField = (fieldName, value) => { + if (value === null || value === undefined) return true; + + switch (fieldName) { + case 'name': + return typeof value === 'string' && value.trim().length >= 1 && value.trim().length <= 25; + case 'tradCN': + case 'simplifiedCN': + return typeof value === 'string' && value.trim().length >= 1 && value.trim().length <= 1000; + case 'caption': + return typeof value === 'string' && value.trim().length >= 1 && value.trim().length <= 200; + case 'english': + case 'summary': + return typeof value === 'string' && value.trim().length >= 1 && value.trim().length <= 2000; + case 'addInfo': + return typeof value === 'string' && value.trim().length <= 3000; + default: + return true; + } + }; + + const validateUpdateDict = (updateDict) => { + const errors = []; + + // Common validation + if (!validateField('name', updateDict.name)) { + errors.push('Name must be between 1-25 characters'); + } + + if (isMMArtefact) { + // MM-type validation + if (updateDict.tradCN && !validateField('tradCN', updateDict.tradCN)) errors.push('Traditional Chinese must be between 1-1000 characters') + if (updateDict.simplifiedCN && !validateField('simplifiedCN', updateDict.simplifiedCN)) errors.push('Simplified Chinese must be between 1-1000 characters') + if (updateDict.english && !validateField('english', updateDict.english)) errors.push('English must be between 1-2000 characters') + if (updateDict.summary && !validateField('summary', updateDict.summary)) errors.push('Summary must be between 1-2000 characters') + } else { + // HF-type validation + if (updateDict.caption && !validateField('caption', updateDict.caption)) errors.push('Caption must be between 1-200 characters') + if (updateDict.addInfo && !validateField('addInfo', updateDict.addInfo)) errors.push('Additional Info must be 3000 characters or less') + } + + return errors; + }; + + const handleCancelChanges = () => { + setArtefactData(originalArtefactData); + ToastWizard.standard("warning", "Changes Reset", "All changes reverted.", 3000); + } + + const handleSaveChanges = async () => { + setSaveLoading(true); + + var updateDict = { + artefactID: artefactId, + name: artefactData.name || '', + }; + + // Add fields based on artefact type + if (isMMArtefact) { + // MM-type metadata fields + if (artefactData.metadata.tradCN != originalArtefactData.tradCN) updateDict.tradCN = artefactData.metadata.tradCN + if (artefactData.metadata.simplifiedCN != originalArtefactData.simplifiedCN) updateDict.simplifiedCN = artefactData.metadata.simplifiedCN + if (artefactData.metadata.english != originalArtefactData.english) updateDict.english = artefactData.metadata.english + if (artefactData.metadata.summary != originalArtefactData.summary) updateDict.summary = artefactData.metadata.summary + } else { + // HF-type metadata fields + if (artefactData.metadata.caption != originalArtefactData.caption) updateDict.caption = artefactData.metadata.caption + if (artefactData.metadata.addInfo != originalArtefactData.addInfo) updateDict.addInfo = artefactData.metadata.addInfo + } + + // Validate the update dictionary + const validationErrors = validateUpdateDict(updateDict); + if (validationErrors.length > 0) { + setSaveLoading(false); + ToastWizard.standard( + "error", + "Validation Error", + validationErrors.join('. '), + 5000 + ); + return; + } + + try { + const response = await server.post('/studio/artefact/update', updateDict); + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + // Success case - handle both full success (200) and partial success (207) + getArtefactData(); + setIsConfirmSaveOpen(false); + setIsEditing(false); // Exit editing mode after successful save + + if (response.data.status === 207) { + // Partial success - metadata updated but NER labelling failed + ToastWizard.standard( + "warning", + "Artefact partially updated.", + 5000, + true, + handleSaveChanges, + "Retry" + ); + } else { + // Full success + ToastWizard.standard("success", "Artefact updated.", response.data.message); + } + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in update artefact data request:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard( + "error", + "Artefact update failed.", + err.response.data.message, + 3000, + true, + handleSaveChanges, + "Retry" + ); + } else { + ToastWizard.standard( + "error", + "Artefact update failed.", + "Failed to update your artefact information. Please try again.", + 3000, + true, + handleSaveChanges, + "Retry" + ); + } + } else { + console.log("Unexpected error in artefact update request:", err); + ToastWizard.standard( + "error", + "Artefact update failed.", + "Failed to update your artefact information. Please try again.", + 3000, + true, + handleSaveChanges, + "Retry" + ); + } + } + + setSaveLoading(false); + } + + return ( + + + + + + + setIsConfirmSaveOpen(e.open)}> + + + + + + + + + Update Artefact + + + + Are you sure you'd like to save these changes? This action cannot be undone. + + + + + + + + + + + + + + + ) +} + +export default ArtefactEditorActionBar \ No newline at end of file diff --git a/src/components/DataStudio/editorCard.jsx b/src/components/DataStudio/editorCard.jsx new file mode 100644 index 0000000..29dbc71 --- /dev/null +++ b/src/components/DataStudio/editorCard.jsx @@ -0,0 +1,367 @@ +import { Card, Button, Flex, Text, Field, Input, Textarea, Box } from "@chakra-ui/react"; +import { useState, useEffect, useRef } from "react"; +import { IoInformationCircleOutline } from "react-icons/io5"; +import { MdOutlineEdit, MdOutlineCancel } from "react-icons/md"; +import MetadataToggle from "./metadataToggle"; +import ArtefactEditorActionBar from "./artefactEditorActionBar"; +import ToastWizard from "../toastWizard"; +import FigureDisplaySection from "./figureDisplay"; + +// Helper function to render text with labeled entities highlighted +function renderHighlightedText(text, labels, hoveredIndex, setHoveredIndex) { + if (!labels || !Array.isArray(labels)) return {text}; + + // Define background colors for each entity type + const entityColors = { + PERSON: "yellow.200", + ORGANIZATION: "blue.200", + LOCATION: "green.200", + DATE: "orange.200", + GPE: "purple.200", + EVENT: "pink.200", + }; + + const chunks = []; + let remainingText = text; + + // Iterate through each labeled entity and highlight it + labels.forEach(([entity, label], i) => { + const index = remainingText.indexOf(entity); + if (index === -1) return; + + const before = remainingText.slice(0, index); + const matched = remainingText.slice(index, index + entity.length); + const after = remainingText.slice(index + entity.length); + + if (before) { + chunks.push({before}); + } + + // Render the matched entity with a tooltip for the label + chunks.push( + setHoveredIndex(i)} + onMouseLeave={() => setHoveredIndex(null)} + display="inline-block" + bg={entityColors[label] || "gray.200"} + borderRadius="md" + px={1} + mx="0.5px" + > + {matched} + + {label} + + + ); + + remainingText = after; + }); + + if (remainingText) { + chunks.push({remainingText}); + } + + return {chunks}; +} + +function EditorCard({ metadata, artefactId, refreshArtefactData }) { + const [selectedTranscription, setSelectedTranscription] = useState("tradCN"); + const [isEditing, setIsEditing] = useState(false); + const [hoveredIndex, setHoveredIndex] = useState(null); + const hoverTimeoutRef = useRef(null); + + // Change detection states + const [originalArtefactData, setOriginalArtefactData] = useState({}); + const [artefactData, setArtefactData] = useState({}); + + // Initialize data when metadata prop changes + useEffect(() => { + if (metadata) { + const processedData = { + name: metadata.name || '', + metadata: metadata.metadata || {} + }; + + setOriginalArtefactData(processedData); + setArtefactData(processedData); + } + }, [metadata]); + + // Delay tooltip hover effect for entity labels + const handleMouseEnter = (index) => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current); + } + hoverTimeoutRef.current = setTimeout(() => { + setHoveredIndex(index); + }, 500); + }; + + // Handle edit/cancel button click + const handleEditClick = () => { + setIsEditing(true); + ToastWizard.standard("info", "Editor Mode Enabled", "You are currently in editor mode. Remember to save your changes!", 3000); + } + + const handleCancelClick = () => { + // Reset data to original state + setArtefactData(originalArtefactData); + setIsEditing(false); + ToastWizard.standard("warning", "Editor Mode Disabled", "All changes reverted.", 3000); + } + + // Handle input changes + const handleNameChange = (e) => { + setArtefactData({ + ...artefactData, + name: e.target.value + }); + } + + const handleTranscriptionChange = (e) => { + setArtefactData({ + ...artefactData, + metadata: { + ...artefactData.metadata, + [selectedTranscription]: e.target.value + } + }); + } + + const isMMArtefact = (metadata.metadata.english || metadata.metadata.tradCN || metadata.metadata.simplifiedCN || metadata.metadata.summary) ? true : false; + + return ( + <> + + + + + Artefact Details + + + + + + + + + {isMMArtefact ? ( + <> + + Name: + {isEditing ? ( + + ) : ( + + {artefactData.name || 'No name set'} + + )} + + + {/* MM Artefact Specific Fields */} + + Transcription: + + + + {isEditing ? ( +