From 40fdef49aa132683746ff89520543add34ccba69 Mon Sep 17 00:00:00 2001 From: Usama Date: Thu, 5 Mar 2026 16:21:44 +0000 Subject: [PATCH 1/4] Implement custom topic management features in Project Portal Editor - Added a new CustomTopicModal component for creating and editing custom verification topics. - Integrated modal functionality into ProjectPortalEditor for managing custom topics. - Implemented hooks for creating, updating, and deleting custom topics, enhancing project customization. - Updated UI components to support new topic label and icon handling, improving user experience. - Enhanced backend API to support custom topic creation and management, including validation and error handling. --- .../conversation/VerifiedArtefactsSection.tsx | 5 +- .../verify/VerifiedArtefactsList.tsx | 6 +- .../participant/verify/VerifySelection.tsx | 6 +- .../components/project/CustomTopicModal.tsx | 247 +++++++++++ .../project/ProjectPortalEditor.tsx | 387 +++++++++++++++--- .../src/components/project/hooks/index.ts | 131 +++++- echo/frontend/src/lib/api.ts | 60 ++- echo/frontend/src/lib/typesDirectus.d.ts | 1 + echo/server/dembrane/api/verify.py | 286 ++++++++++++- 9 files changed, 1028 insertions(+), 101 deletions(-) create mode 100644 echo/frontend/src/components/project/CustomTopicModal.tsx diff --git a/echo/frontend/src/components/conversation/VerifiedArtefactsSection.tsx b/echo/frontend/src/components/conversation/VerifiedArtefactsSection.tsx index be509b4d..1bc2ff2d 100644 --- a/echo/frontend/src/components/conversation/VerifiedArtefactsSection.tsx +++ b/echo/frontend/src/components/conversation/VerifiedArtefactsSection.tsx @@ -116,7 +116,10 @@ export const VerifiedArtefactsSection = ({ - {topicLabelMap.get(artefact.key) ?? artefact.key ?? ""} + {topicLabelMap.get(artefact.key) ?? + artefact.topic_label ?? + artefact.key ?? + ""} {formattedDate && ( diff --git a/echo/frontend/src/components/participant/verify/VerifiedArtefactsList.tsx b/echo/frontend/src/components/participant/verify/VerifiedArtefactsList.tsx index 382b8dd9..b2da7ef7 100644 --- a/echo/frontend/src/components/participant/verify/VerifiedArtefactsList.tsx +++ b/echo/frontend/src/components/participant/verify/VerifiedArtefactsList.tsx @@ -93,7 +93,11 @@ export const VerifiedArtefactsList = ({ { const icon = TOPIC_ICON_MAP[topic.key] ?? (topic.icon && !topic.icon.startsWith(":") ? topic.icon : undefined) ?? - "•"; + (topic.is_custom ? undefined : "•"); return { icon, @@ -231,7 +231,9 @@ export const VerifySelection = () => { {...testId(`portal-verify-topic-${option.key}`)} > - {option.icon} + {option.icon ? ( + {option.icon} + ) : null} {option.label} diff --git a/echo/frontend/src/components/project/CustomTopicModal.tsx b/echo/frontend/src/components/project/CustomTopicModal.tsx new file mode 100644 index 00000000..0a39f116 --- /dev/null +++ b/echo/frontend/src/components/project/CustomTopicModal.tsx @@ -0,0 +1,247 @@ +import { t } from "@lingui/core/macro"; +import { Trans } from "@lingui/react/macro"; +import { + Button, + Collapse, + Group, + Modal, + Stack, + Text, + Textarea, + TextInput, + UnstyledButton, +} from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import { CaretDownIcon, CaretRightIcon } from "@phosphor-icons/react"; +import { useEffect, useState } from "react"; +import type { VerificationTopicMetadata } from "@/lib/api"; +import { testId } from "@/lib/testUtils"; + +const MAX_LABEL_LENGTH = 100; +const MAX_PROMPT_LENGTH = 1000; +const MAX_ICON_LENGTH = 10; + +const EMOJI_REGEX = /[\p{Emoji_Presentation}\p{Extended_Pictographic}]/gu; + +const SUPPORTED_LANGUAGES = [ + { code: "en-US", label: "English" }, + { code: "nl-NL", label: "Nederlands" }, + { code: "de-DE", label: "Deutsch" }, + { code: "fr-FR", label: "Français" }, + { code: "es-ES", label: "Español" }, + { code: "it-IT", label: "Italiano" }, +] as const; + +type CustomTopicModalProps = { + opened: boolean; + onClose: () => void; + mode: "create" | "edit"; + topic?: VerificationTopicMetadata | null; + onSubmit: (data: { + label: string; + prompt: string; + icon: string; + translations: Record; + }) => void; + isLoading?: boolean; +}; + +export const CustomTopicModal = ({ + opened, + onClose, + mode, + topic, + onSubmit, + isLoading = false, +}: CustomTopicModalProps) => { + const [ + translationsOpen, + { toggle: toggleTranslations, close: closeTranslations }, + ] = useDisclosure(false); + const [labels, setLabels] = useState>({}); + const [prompt, setPrompt] = useState(""); + const [icon, setIcon] = useState(""); + + useEffect(() => { + if (!opened) return; + + if (mode === "edit" && topic) { + const translationLabels: Record = {}; + for (const lang of SUPPORTED_LANGUAGES) { + translationLabels[lang.code] = + topic.translations?.[lang.code]?.label ?? ""; + } + setLabels(translationLabels); + setPrompt(topic.prompt ?? ""); + setIcon(topic.icon ?? ""); + } else { + setLabels({}); + setPrompt(""); + setIcon(""); + } + closeTranslations(); + }, [opened, mode, topic, closeTranslations]); + + const enUsLabel = labels["en-US"]?.trim() ?? ""; + + const hasChanges = (() => { + if (mode === "create") return true; + if (!topic) return true; + + if (enUsLabel !== (topic.translations?.["en-US"]?.label ?? "")) return true; + if (prompt.trim() !== (topic.prompt ?? "")) return true; + if (icon.trim() !== (topic.icon ?? "")) return true; + + for (const lang of SUPPORTED_LANGUAGES) { + if (lang.code === "en-US") continue; + const current = labels[lang.code]?.trim() ?? ""; + const original = topic.translations?.[lang.code]?.label ?? ""; + if (current !== original) return true; + } + + return false; + })(); + + const canSubmit = + enUsLabel.length > 0 && prompt.trim().length > 0 && hasChanges; + + const handleSubmit = () => { + if (!canSubmit) return; + + const translations: Record = {}; + for (const lang of SUPPORTED_LANGUAGES) { + const val = labels[lang.code]?.trim(); + if (val) { + translations[lang.code] = val; + } + } + + onSubmit({ + icon: icon.trim(), + label: enUsLabel, + prompt: prompt.trim(), + translations, + }); + }; + + return ( + Add Custom Topic + ) : ( + Edit Custom Topic + ) + } + size="lg" + radius="md" + padding="xl" + {...testId("custom-topic-modal")} + > + + { + const val = e.currentTarget.value; + setLabels((prev) => ({ ...prev, "en-US": val })); + }} + maxLength={MAX_LABEL_LENGTH} + required + {...testId("custom-topic-label-en-US")} + /> + + + + + {translationsOpen ? ( + + ) : ( + + )}{" "} + Add translations + + + + + + {SUPPORTED_LANGUAGES.filter((l) => l.code !== "en-US").map( + (lang) => ( + { + const val = e.currentTarget.value; + setLabels((prev) => ({ + ...prev, + [lang.code]: val, + })); + }} + maxLength={MAX_LABEL_LENGTH} + {...testId(`custom-topic-label-${lang.code}`)} + /> + ), + )} + + + + +