diff --git a/src/components/Catalogue/cardComponent.jsx b/src/components/Catalogue/cardComponent.jsx index 9a67ce8..4dcb16a 100644 --- a/src/components/Catalogue/cardComponent.jsx +++ b/src/components/Catalogue/cardComponent.jsx @@ -14,13 +14,13 @@ const CardComponent = ({ imageSrc, itemTitle, itemDescription, expanded }) => { alt={itemTitle} loading={expanded ? "eager" : "lazy"} onLoad={() => setIsImageLoaded(true)} - height="180px" + height="180px" width="100%" /> - {itemTitle} + {itemTitle} {expanded && ( diff --git a/src/components/Catalogue/catalogueItemView.jsx b/src/components/Catalogue/catalogueItemView.jsx index 2afa607..e75adc7 100644 --- a/src/components/Catalogue/catalogueItemView.jsx +++ b/src/components/Catalogue/catalogueItemView.jsx @@ -12,7 +12,7 @@ import { IoOpen } from "react-icons/io5"; const MotionBox = motion.create(Box); const MotionChevron = motion.create(Box); -const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialogTitle, imageSrc }) => { +const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialogTitle, imageSrc, artefactID }) => { const [isNavigating, setIsNavigating] = useState(false); const [direction, setDirection] = useState(0); const [isInitialRender, setIsInitialRender] = useState(true); @@ -92,11 +92,11 @@ const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialog debounceTimeout = setTimeout(() => { const scrollLeft = scrollRef.current.scrollLeft; const newIndex = Math.round(scrollLeft / window.innerWidth); - + if (newIndex !== currentIndex && items[newIndex]) { setDialogTitle(items[newIndex].title); } - + // Reset scrolling flag after debounce isScrolling = false; }, 100); // Keep the debounce time reasonable (100ms) @@ -104,7 +104,7 @@ const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialog const scrollEl = scrollRef.current; scrollEl.addEventListener("scroll", handleScroll); - + return () => { scrollEl.removeEventListener("scroll", handleScroll); if (debounceTimeout) clearTimeout(debounceTimeout); @@ -151,13 +151,13 @@ const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialog left: isMobile ? "10px" : isTablet - ? "calc(5vw - 50px)" - : "calc(10vw - 60px)", + ? "calc(5vw - 50px)" + : "calc(10vw - 60px)", right: isMobile ? "10px" : isTablet - ? "calc(5vw - 50px)" - : "calc(10vw - 60px)", + ? "calc(5vw - 50px)" + : "calc(10vw - 60px)", top: "50%", }; @@ -356,21 +356,21 @@ const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialog gap={2} > {item.title} - - navigate(`/studio/${sectionId}/${currentId}`)} cursor={"pointer"}/> + navigate(`/studio/${sectionId}/${currentId}`)} cursor={"pointer"} /> @@ -394,14 +394,14 @@ const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialog {selectedSegment === - "metadata" ? ( + "metadata" ? ( ) : selectedSegment === - "chat" ? ( - + "chat" ? ( + ) : null} @@ -491,8 +491,8 @@ const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialog > - - navigate(`/studio/${sectionId}/${currentId}`)} cursor={"pointer"}/> + navigate(`/studio/${sectionId}/${currentId}`)} cursor={"pointer"} /> @@ -532,14 +532,14 @@ const CatalogueItemView = ({ isOpen, onClose, title, sectionId, items, setDialog {selectedSegment === - "metadata" ? ( + "metadata" ? ( ) : selectedSegment === - "chat" ? ( - + "chat" ? ( + ) : null} diff --git a/src/components/Catalogue/itemChat.jsx b/src/components/Catalogue/itemChat.jsx index 415be99..51d1ebb 100644 --- a/src/components/Catalogue/itemChat.jsx +++ b/src/components/Catalogue/itemChat.jsx @@ -1,5 +1,132 @@ -function ItemChat() { - return
ItemChat
; +import { useState, useEffect, useRef } from "react"; +import { Input, Box, Button, VStack, HStack, Text } from "@chakra-ui/react"; +import { IoIosSend } from "react-icons/io"; +import server, { JSONResponse } from "../../networking"; +import ToastWizard from "../toastWizard"; + +function ItemChat({ artefactID }) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(""); + const [loading, setLoading] = useState(false); + const messagesEndRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [messages]); + + const sendMessage = async () => { + if (!input.trim() || loading) return; + + const userMessage = { role: "user", content: input }; + setMessages(prev => [...prev, userMessage]); + setInput(""); + setLoading(true); + + try { + const response = await server.post('/chatbot/query', { + artefactID, + newPrompt: input, + history: messages + }) + + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw new Error(errObject); + } + + if (response.data?.raw?.data?.history) { + setMessages(response.data.raw.data.history); + } else if (response.data?.raw?.data?.response) { + setMessages(prev => [...prev, + { role: "assistant", content: response.data.raw.data.response } + ]); + } + + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in chatbot query:", err.response.data.fullMessage()); + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Chatbot query failed.", err.response.data.message); + } else { + ToastWizard.standard("error", "Chatbot query failed.", "An unexpected error occurred."); + } + } else { + console.log("Unexpected error in chatbot query:", err); + ToastWizard.standard("error", "Chatbot query failed.", "An unexpected error occurred."); + } + } finally { + setLoading(false); + } + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + return ( + + + {messages.length === 0 ? ( + + Let's discover this artefact together ! + + ) : ( + messages.map((msg, idx) => ( + + + {msg.role === "user" ? "You: " : "Archivus: "} + + {msg.content} + + )) + )} +
+ + + + setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={loading} + /> + + + + ); } -export default ItemChat; +export default ItemChat; \ No newline at end of file diff --git a/src/components/Catalogue/section.jsx b/src/components/Catalogue/section.jsx index 0d9bb91..8206c51 100644 --- a/src/components/Catalogue/section.jsx +++ b/src/components/Catalogue/section.jsx @@ -25,6 +25,7 @@ const Section = ({ sectionTitle, sectionId, onItemClick, artefacts = [] }) => { const [atEnd, setAtEnd] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [dialogTitle, setDialogTitle] = useState(""); + const [dialogArtefactID, setDialogArtefactID] = useState(""); const checkScrollEdges = () => { const node = scrollRef.current; @@ -45,10 +46,11 @@ const Section = ({ sectionTitle, sectionId, onItemClick, artefacts = [] }) => { }); }; - const handleCardClick = (title) => { - setDialogTitle(title); + const handleCardClick = (item) => { + setDialogTitle(item.title); + setDialogArtefactID(item.id); setDialogOpen(true); - if (onItemClick) onItemClick(title); + if (onItemClick) onItemClick(item.id); }; useEffect(() => { @@ -129,7 +131,7 @@ const Section = ({ sectionTitle, sectionId, onItemClick, artefacts = [] }) => { flex="0 0 auto" cursor="pointer" borderRadius="md" - onClick={() => handleCardClick(item.title)} + onClick={() => handleCardClick(item)} > { setDialogOpen(false)} + artefactID={dialogArtefactID} title={dialogTitle} sectionId={sectionId} items={items} diff --git a/src/components/DataStudio/associationCard.jsx b/src/components/DataStudio/associationCard.jsx new file mode 100644 index 0000000..900e7a8 --- /dev/null +++ b/src/components/DataStudio/associationCard.jsx @@ -0,0 +1,34 @@ +import { Card, Box, Text, Flex } from "@chakra-ui/react"; +import AssociationSwitch from "./associationSwitch"; + +function AssociationCard({ associationData, isMember, onToggle }) { + if (!associationData) return null; + + return ( + + + + {/* Left: title & description */} + + + {associationData.name || "Untitled"} + + + {associationData.description} + + + + {/* Right: membership switch */} + + + + + + + ); +} + +export default AssociationCard; \ No newline at end of file diff --git a/src/components/DataStudio/associationSwitch.jsx b/src/components/DataStudio/associationSwitch.jsx new file mode 100644 index 0000000..75a9114 --- /dev/null +++ b/src/components/DataStudio/associationSwitch.jsx @@ -0,0 +1,26 @@ +import { Switch } from "@chakra-ui/react"; +import { HiCheck, HiX } from "react-icons/hi"; + +const AssociationSwitch = ({ isMember, onToggle }) => { + return ( + onToggle(details.checked)} + colorPalette={'green'} + > + + + + } + > + + + + + + ); +}; + +export default AssociationSwitch; \ No newline at end of file diff --git a/src/components/DataStudio/editorCard.jsx b/src/components/DataStudio/editorCard.jsx index 0b1e50b..013277d 100644 --- a/src/components/DataStudio/editorCard.jsx +++ b/src/components/DataStudio/editorCard.jsx @@ -6,6 +6,7 @@ import MetadataToggle from "./metadataToggle"; import ArtefactEditorActionBar from "./artefactEditorActionBar"; import ToastWizard from "../toastWizard"; import FigureDisplaySection from "./figureDisplay"; +import ManageAssociationsDialog from "./manageAssociationsDialog"; // Helper function to render text with labeled entities highlighted function renderHighlightedText(text, labels, hoveredIndex, setHoveredIndex) { @@ -91,6 +92,7 @@ function EditorCard({ metadata, artefactId, refreshArtefactData }) { const [isEditing, setIsEditing] = useState(false); const [hoveredIndex, setHoveredIndex] = useState(null); const hoverTimeoutRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); // Change detection states const [originalArtefactData, setOriginalArtefactData] = useState({}); @@ -164,7 +166,7 @@ function EditorCard({ metadata, artefactId, refreshArtefactData }) { Artefact Details - + + + + + + + + + + + + ); +} + +export default ManageAssociationsDialog; \ No newline at end of file diff --git a/src/pages/GroupView.jsx b/src/pages/GroupView.jsx index f7dbc72..9eea4f1 100644 --- a/src/pages/GroupView.jsx +++ b/src/pages/GroupView.jsx @@ -13,97 +13,76 @@ function GroupView() { const [collection, setCollection] = useState(null); const [loading, setLoading] = useState(true); - useEffect(() => { - const fetchCollection = async () => { - try { - setLoading(true); - const response = await server.get(`/cdn/collection/${colID}`); - - if (response.data instanceof JSONResponse) { - if (response.data.isErrorStatus()) { - const errObject = { - response: { - data: response.data - } - }; - throw errObject; - } + const fetchCollection = async () => { + try { + setLoading(true); + const response = await server.get(`/cdn/collection/${colID}`); - // Success case - format the data - let formattedData; - const apiData = response.data.raw.data; + if (response.data instanceof JSONResponse) { + if (response.data.isErrorStatus()) { + const errObject = { + response: { + data: response.data + } + }; + throw errObject; + } - if (apiData.type === 'book') { - formattedData = { - type: 'book', - name: apiData.title, - description: apiData.subtitle || '', - items: apiData.mmArtefacts.map(art => ({ - id: art.id, - title: art.name, - description: art.description, - image: art.image || null - })) - }; - } else if (apiData.type === 'category') { - formattedData = { - type: 'category', - name: apiData.name, - description: apiData.description || '', - items: apiData.members.map(art => ({ - id: art.id, - title: art.name, - description: art.description, - image: art.image - })) - }; - } else if (apiData.type === 'batch') { - formattedData = { - type: 'batch', - name: apiData.name || `Batch ${apiData.id}`, - description: apiData.description || '', - items: apiData.artefacts.map(art => ({ - id: art.id, - title: art.name, - description: art.description, - image: art.image || null - })) - }; - } else { - throw new Error('Unknown collection type'); - } + // Success case - format the data + let formattedData; + const apiData = response.data.raw.data; - setCollection(formattedData); + if (apiData.type === 'book') { + formattedData = { + type: 'book', + name: apiData.title, + description: apiData.subtitle || '', + items: apiData.mmArtefacts.map(art => ({ + id: art.id, + title: art.name, + description: art.description, + image: art.image || null + })) + }; + } else if (apiData.type === 'category') { + formattedData = { + type: 'category', + name: apiData.name, + description: apiData.description || '', + items: apiData.members.map(art => ({ + id: art.id, + title: art.name, + description: art.description, + image: art.image + })) + }; + } else if (apiData.type === 'batch') { + formattedData = { + type: 'batch', + name: apiData.name || `Batch ${apiData.id}`, + description: apiData.description || '', + items: apiData.artefacts.map(art => ({ + id: art.id, + title: art.name, + description: art.description, + image: art.image || null + })) + }; } else { - throw new Error("Unexpected response format"); + throw new Error('Unknown collection type'); } - } catch (err) { - if (err.response && err.response.data instanceof JSONResponse) { - console.log("Error response in collection request:", err.response.data.fullMessage()); - - if (err.response.data.userErrorType()) { - ToastWizard.standard( - "error", - "An Error Occured", - "We could not fetch the collection data. Please try again later.", - 3000, - true, - fetchCollection, - 'Retry' - ); - } else { - ToastWizard.standard( - "error", - "An Error Occured", - "We could not fetch the collection data. Please try again later.", - 3000, - true, - fetchCollection, - 'Retry' - ); - } + + setCollection(formattedData); + } else { + throw new Error("Unexpected response format"); + } + } catch (err) { + if (err.response && err.response.data instanceof JSONResponse) { + console.log("Error response in collection request:", err.response.data.fullMessage()); + + if (err.response.data.userErrorType()) { + ToastWizard.standard("error", "Error fetching data.", err.response.data.message); } else { - console.log("Unexpected error in collection request:", err); ToastWizard.standard( "error", "An Error Occured", @@ -114,11 +93,24 @@ function GroupView() { 'Retry' ); } - } finally { - setLoading(false); + } else { + console.log("Unexpected error in collection request:", err); + ToastWizard.standard( + "error", + "An Error Occured", + "We could not fetch the collection data. Please try again later.", + 3000, + true, + fetchCollection, + 'Retry' + ); } - }; + } finally { + setLoading(false); + } + }; + useEffect(() => { fetchCollection(); }, [colID]); @@ -161,9 +153,9 @@ function GroupView() { {collection.items.map(item => ( ))}