From 288588594f369e9470a5f8d6781bf1f3c6d8d790 Mon Sep 17 00:00:00 2001 From: Lina Date: Wed, 1 Oct 2025 13:29:54 +0200 Subject: [PATCH 1/9] App notifications in submodules --- components/AppNotifications.tsx | 82 +++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 components/AppNotifications.tsx diff --git a/components/AppNotifications.tsx b/components/AppNotifications.tsx new file mode 100644 index 0000000..1850507 --- /dev/null +++ b/components/AppNotifications.tsx @@ -0,0 +1,82 @@ +import { combineClassNames } from "@/submodules/javascript-functions/general"; +import { MemoIconBell } from "@/submodules/react-components/components/kern-icons/icons"; +import useOnClickOutside from "@/submodules/react-components/hooks/useHooks/useOnClickOutside"; +import { useLocalStorage } from "@/submodules/react-components/hooks/useLocalStorage"; +import Link from "next/link"; +import { useCallback, useMemo, useRef, useState } from "react"; +import tinycolor from 'tinycolor2' +import { useTranslation } from "react-i18next"; +import { UserRole } from "@/submodules/javascript-functions/enums/enums"; + +type AppNotificationsProps = { + project: { customerColorPrimary: string; } + user: { role: UserRole; } + forChatArea?: boolean; +} +// only last 3 are shown, id still needs to be counted up since the local storage is used to check if the notification was already shown +// key is used for i18n translation +const NOTIFICATIONS = [ + { id: 1, key: "release_1_8", link: "https://www.kern.ai/resources/product-updates/week-28" }, + { id: 2, key: "release_1_9", link: "https://www.kern.ai/resources/product-updates/week-37" } +] + +export default function AppNotifications(props: AppNotificationsProps) { + const { t, i18n } = useTranslation('projectOverview'); + const [lastSeenNotification, setLastSeenNotification] = useLocalStorage('lastSeen', 'releaseNotification', undefined, -1); + const [showNotifications, setShowNotifications] = useState(false); + const justClickedOutsideRef = useRef(false); // to prevent showing the notifications if the user just clicked outside + const refNotificationBox = useRef(null); + useOnClickOutside(refNotificationBox, () => { setShowNotifications(false); justClickedOutsideRef.current = true; setTimeout(() => justClickedOutsideRef.current = false, 200); }); + const clickBell = useCallback(() => { + if (justClickedOutsideRef.current) return; // if the user just clicked outside, don't show the notifications since the person is trying to close via bell icon + setShowNotifications(true); + setLastSeenNotification(NOTIFICATIONS[NOTIFICATIONS.length - 1].id); + }, []); + + const finalNotifications = useMemo(() => NOTIFICATIONS.slice(-3).map((e) => ( + { + ...e, + headline: t("notificationBell." + e.key + ".headline", props.user?.role == UserRole.ENGINEER ? { lng: "en" } : undefined), + description: t("notificationBell." + e.key + ".description", props.user?.role == UserRole.ENGINEER ? { lng: "en" } : undefined) + })), [NOTIFICATIONS, i18n.language, props.user?.role]); + + const hasNewNotifications = useMemo(() => { + if (lastSeenNotification === -1) return true; // if no notification was seen + return finalNotifications.some(notification => notification.id > lastSeenNotification); + }, [lastSeenNotification, finalNotifications]); + + const isLightDesign = useMemo(() => tinycolor(props.project?.customerColorPrimary).isLight(), [props.project?.customerColorPrimary]); + + const buttonClasses = useMemo(() => { + if (props.forChatArea) { + const classes = "items-center justify-center w-8 h-8 border group flex -x-3 rounded-md p-1 text-sm leading-6 font-semibold" + if (isLightDesign) return 'bg-gray-100 text-gray-700 border-gray-300 ' + classes; + else return 'bg-zinc-900 text-zinc-100 border-zinc-700 ' + classes; + } + return "text-gray-400 hover:text-green-600 hover:bg-zinc-800 border-gray-700 items-center justify-center w-10 h-10 border group flex -x-3 rounded-md p-2 text-sm leading-6 font-semibold" + }, [props.forChatArea, isLightDesign]); + + return
+ + {hasNewNotifications &&
} + {showNotifications &&
+
+
{t("notificationBell.header", props.user?.role == UserRole.ENGINEER ? { lng: "en" } : undefined)}
+ {finalNotifications.map((notification, idx) => ( +
+
{notification.headline}
+
{notification.description}
+ {t("notificationBell.link", props.user?.role == UserRole.ENGINEER ? { lng: "en" } : undefined)} +
+ ))} +
+
+ } +
+} From 0106098f0da29e221a1e28827b103af598c51f02 Mon Sep 17 00:00:00 2001 From: Lina Date: Wed, 1 Oct 2025 17:15:40 +0200 Subject: [PATCH 2/9] Added new table cells --- components/kern-table/CellComponents.tsx | 15 ++++++++++++++- components/kern-table/KernTable.tsx | 6 +++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/components/kern-table/CellComponents.tsx b/components/kern-table/CellComponents.tsx index 7c54d4c..88b5119 100644 --- a/components/kern-table/CellComponents.tsx +++ b/components/kern-table/CellComponents.tsx @@ -327,4 +327,17 @@ function ExpiredTokenCell({ value }) { } -export { OrganizationAndUsersCell, MaxRowsColsCharsCell, CommentsCell, ExportConsumptionAndDeleteCell, BadgeCell, OrganizationUserCell, DeleteCell, LevelCell, ArchiveReasonCell, ProjectNameTaskCell, CancelTaskCell, IconCell, ConfigCell, EditDeleteOrgButtonCell, ViewStackCell, AbortSessionButtonCell, FeedbackMessageCell, FeedbackMessageTextCell, JumpToConversationCell, RemoteVersionCell, ExternalLinkCell, ModelDateCell, FileSizeCell, StatusModelCell, DeleteModelCell, LabelCell, ViewCell, EvaluationRunStateCell, EvaluationRunDetailsCell, EtlApiTokenCell, EmailCell, EditIntegrationCell, ExpiredTokenCell } \ No newline at end of file +function LinkCell({ value }) { + return
+ {value} + + + +
+} + +function ConfigReleaseNotificationCell({ onClick }) { + return ; +} + +export { OrganizationAndUsersCell, MaxRowsColsCharsCell, CommentsCell, ExportConsumptionAndDeleteCell, BadgeCell, OrganizationUserCell, DeleteCell, LevelCell, ArchiveReasonCell, ProjectNameTaskCell, CancelTaskCell, IconCell, ConfigCell, EditDeleteOrgButtonCell, ViewStackCell, AbortSessionButtonCell, FeedbackMessageCell, FeedbackMessageTextCell, JumpToConversationCell, RemoteVersionCell, ExternalLinkCell, ModelDateCell, FileSizeCell, StatusModelCell, DeleteModelCell, LabelCell, ViewCell, EvaluationRunStateCell, EvaluationRunDetailsCell, EtlApiTokenCell, EmailCell, EditIntegrationCell, ExpiredTokenCell, LinkCell, ConfigReleaseNotificationCell } \ No newline at end of file diff --git a/components/kern-table/KernTable.tsx b/components/kern-table/KernTable.tsx index b940c1d..4075aa6 100644 --- a/components/kern-table/KernTable.tsx +++ b/components/kern-table/KernTable.tsx @@ -1,6 +1,6 @@ import SortArrows from "@/submodules/react-components/components/kern-table/SortArrows"; import { KernTableProps } from "../../types/kern-table"; -import { AbortSessionButtonCell, ArchiveReasonCell, BadgeCell, CancelTaskCell, CommentsCell, ConfigCell, DeleteModelCell, DeleteCell, EditDeleteOrgButtonCell, EmailCell, EtlApiTokenCell, EvaluationRunDetailsCell, EvaluationRunStateCell, ExportConsumptionAndDeleteCell, ExternalLinkCell, FeedbackMessageCell, FeedbackMessageTextCell, FileSizeCell, IconCell, JumpToConversationCell, LabelCell, LevelCell, MaxRowsColsCharsCell, ModelDateCell, OrganizationAndUsersCell, OrganizationUserCell, ProjectNameTaskCell, RemoteVersionCell, StatusModelCell, ViewCell, ViewStackCell, EditIntegrationCell, ExpiredTokenCell } from "./CellComponents"; +import { AbortSessionButtonCell, ArchiveReasonCell, BadgeCell, CancelTaskCell, CommentsCell, ConfigCell, DeleteModelCell, DeleteCell, EditDeleteOrgButtonCell, EmailCell, EtlApiTokenCell, EvaluationRunDetailsCell, EvaluationRunStateCell, ExportConsumptionAndDeleteCell, ExternalLinkCell, FeedbackMessageCell, FeedbackMessageTextCell, FileSizeCell, IconCell, JumpToConversationCell, LabelCell, LevelCell, MaxRowsColsCharsCell, ModelDateCell, OrganizationAndUsersCell, OrganizationUserCell, ProjectNameTaskCell, RemoteVersionCell, StatusModelCell, ViewCell, ViewStackCell, EditIntegrationCell, ExpiredTokenCell, LinkCell, ConfigReleaseNotificationCell } from "./CellComponents"; import { Fragment, useMemo } from "react"; import KernDropdown from "../KernDropdown"; import { NotApplicableBadge } from "@/submodules/react-components/components/Badges"; @@ -158,6 +158,10 @@ function ComponentMapper(cell: any) { return ; case 'ExpiredTokenCell': return ; + case 'LinkCell': + return ; + case 'ConfigReleaseNotificationCell': + return ; } case 'text': return {cell.value ?? } From 5cf7658296fc64dc572ce97e82c01938ed4f24aa Mon Sep 17 00:00:00 2001 From: Lina Date: Thu, 2 Oct 2025 10:35:35 +0200 Subject: [PATCH 3/9] Added new cell to the kern table --- components/kern-table/CellComponents.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/components/kern-table/CellComponents.tsx b/components/kern-table/CellComponents.tsx index 88b5119..4cd3c02 100644 --- a/components/kern-table/CellComponents.tsx +++ b/components/kern-table/CellComponents.tsx @@ -9,7 +9,7 @@ import { AdminMessageLevel } from "../../types/admin-messages"; import { FeedbackType, ModelsDownloadedStatus } from "@/submodules/javascript-functions/enums/enums"; import LoadingIcon from "@/submodules/react-components/components/LoadingIcon"; import { EvaluationRunState } from "../../types/evaluationRun"; -import { MemoIconAlertCircle, MemoIconAlertTriangleFilled, MemoIconArrowRight, MemoIconCircleCheckFilled, MemoIconExternalLink, MemoIconFileDownload, MemoIconInfoCircle, MemoIconInfoSquare, MemoIconLoader, MemoIconNotes, MemoIconTag, MemoIconThumbDownFilled, MemoIconThumbUpFilled, MemoIconTrash, MemoIconUserX } from "../kern-icons/icons"; +import { MemoIconAlertCircle, MemoIconAlertTriangleFilled, MemoIconArrowRight, MemoIconCircleCheckFilled, MemoIconEdit, MemoIconExternalLink, MemoIconFileDownload, MemoIconInfoCircle, MemoIconInfoSquare, MemoIconLoader, MemoIconNotes, MemoIconTag, MemoIconThumbDownFilled, MemoIconThumbUpFilled, MemoIconTrash, MemoIconUserX } from "../kern-icons/icons"; function OrganizationAndUsersCell({ organization }) { @@ -336,8 +336,11 @@ function LinkCell({ value }) { } -function ConfigReleaseNotificationCell({ onClick }) { - return ; +function ConfigReleaseNotificationCell({ onClickView, onClickEdit }) { + return
+ + +
; } export { OrganizationAndUsersCell, MaxRowsColsCharsCell, CommentsCell, ExportConsumptionAndDeleteCell, BadgeCell, OrganizationUserCell, DeleteCell, LevelCell, ArchiveReasonCell, ProjectNameTaskCell, CancelTaskCell, IconCell, ConfigCell, EditDeleteOrgButtonCell, ViewStackCell, AbortSessionButtonCell, FeedbackMessageCell, FeedbackMessageTextCell, JumpToConversationCell, RemoteVersionCell, ExternalLinkCell, ModelDateCell, FileSizeCell, StatusModelCell, DeleteModelCell, LabelCell, ViewCell, EvaluationRunStateCell, EvaluationRunDetailsCell, EtlApiTokenCell, EmailCell, EditIntegrationCell, ExpiredTokenCell, LinkCell, ConfigReleaseNotificationCell } \ No newline at end of file From 3983d43539540ff0c504a5bf555710218e1f927f Mon Sep 17 00:00:00 2001 From: Lina Date: Thu, 2 Oct 2025 14:36:58 +0200 Subject: [PATCH 4/9] Notifications from admin dashboard --- components/AppNotifications.tsx | 48 +++++++++++++++++---------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/components/AppNotifications.tsx b/components/AppNotifications.tsx index 1850507..570791b 100644 --- a/components/AppNotifications.tsx +++ b/components/AppNotifications.tsx @@ -10,40 +10,39 @@ import { UserRole } from "@/submodules/javascript-functions/enums/enums"; type AppNotificationsProps = { project: { customerColorPrimary: string; } - user: { role: UserRole; } + user: { role: UserRole; languageDisplay: string; }; forChatArea?: boolean; + notifications: any[]; } -// only last 3 are shown, id still needs to be counted up since the local storage is used to check if the notification was already shown -// key is used for i18n translation -const NOTIFICATIONS = [ - { id: 1, key: "release_1_8", link: "https://www.kern.ai/resources/product-updates/week-28" }, - { id: 2, key: "release_1_9", link: "https://www.kern.ai/resources/product-updates/week-37" } -] + +const MIN_NOTIFICATIONS_SHOW = 3; +const MAX_NOTIFICATIONS_SHOW = 7; export default function AppNotifications(props: AppNotificationsProps) { - const { t, i18n } = useTranslation('projectOverview'); + const { t } = useTranslation('projectOverview'); const [lastSeenNotification, setLastSeenNotification] = useLocalStorage('lastSeen', 'releaseNotification', undefined, -1); const [showNotifications, setShowNotifications] = useState(false); + const [showMoreClicked, setShowMoreClicked] = useState(false); const justClickedOutsideRef = useRef(false); // to prevent showing the notifications if the user just clicked outside const refNotificationBox = useRef(null); useOnClickOutside(refNotificationBox, () => { setShowNotifications(false); justClickedOutsideRef.current = true; setTimeout(() => justClickedOutsideRef.current = false, 200); }); + const clickBell = useCallback(() => { if (justClickedOutsideRef.current) return; // if the user just clicked outside, don't show the notifications since the person is trying to close via bell icon setShowNotifications(true); - setLastSeenNotification(NOTIFICATIONS[NOTIFICATIONS.length - 1].id); - }, []); + setLastSeenNotification(props.notifications[props.notifications.length - 1].id); + }, [props.notifications]); - const finalNotifications = useMemo(() => NOTIFICATIONS.slice(-3).map((e) => ( - { - ...e, - headline: t("notificationBell." + e.key + ".headline", props.user?.role == UserRole.ENGINEER ? { lng: "en" } : undefined), - description: t("notificationBell." + e.key + ".description", props.user?.role == UserRole.ENGINEER ? { lng: "en" } : undefined) - })), [NOTIFICATIONS, i18n.language, props.user?.role]); + const finalNotifications = useMemo(() => { + if (props.notifications.length <= MIN_NOTIFICATIONS_SHOW) return props.notifications; + if (!showMoreClicked) return props.notifications.slice(-MIN_NOTIFICATIONS_SHOW); + if (showMoreClicked) return props.notifications.slice(-MAX_NOTIFICATIONS_SHOW); + }, [props.notifications, showMoreClicked]); const hasNewNotifications = useMemo(() => { if (lastSeenNotification === -1) return true; // if no notification was seen - return finalNotifications.some(notification => notification.id > lastSeenNotification); - }, [lastSeenNotification, finalNotifications]); + return props.notifications.some(notification => notification.id > lastSeenNotification); + }, [lastSeenNotification, props.notifications]); const isLightDesign = useMemo(() => tinycolor(props.project?.customerColorPrimary).isLight(), [props.project?.customerColorPrimary]); @@ -61,21 +60,24 @@ export default function AppNotifications(props: AppNotificationsProps) { {hasNewNotifications &&
} - {showNotifications &&
+ {showNotifications &&
-
{t("notificationBell.header", props.user?.role == UserRole.ENGINEER ? { lng: "en" } : undefined)}
+
{t("notificationBell.header")}
{finalNotifications.map((notification, idx) => (
-
{notification.headline}
-
{notification.description}
+
{notification.config[props.user?.languageDisplay].headline}
+
{notification.config[props.user?.languageDisplay].description}
{t("notificationBell.link", props.user?.role == UserRole.ENGINEER ? { lng: "en" } : undefined)} + >{t("notificationBell.link")}
))}
+ {props.notifications.length > MIN_NOTIFICATIONS_SHOW &&
+ +
}
}
From 9456629d981112809801af063ce7ff528c396fd3 Mon Sep 17 00:00:00 2001 From: Lina Date: Mon, 6 Oct 2025 13:21:25 +0200 Subject: [PATCH 5/9] Small change in naming --- components/kern-table/CellComponents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/kern-table/CellComponents.tsx b/components/kern-table/CellComponents.tsx index 4cd3c02..a2139dd 100644 --- a/components/kern-table/CellComponents.tsx +++ b/components/kern-table/CellComponents.tsx @@ -338,7 +338,7 @@ function LinkCell({ value }) { function ConfigReleaseNotificationCell({ onClickView, onClickEdit }) { return
- +
; } From 10a06700bd95ed6169dbe94e4e87ba14cc9ad15d Mon Sep 17 00:00:00 2001 From: Lina Date: Mon, 6 Oct 2025 14:02:26 +0200 Subject: [PATCH 6/9] Message if no new notifications --- components/AppNotifications.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/AppNotifications.tsx b/components/AppNotifications.tsx index 570791b..a1fe40d 100644 --- a/components/AppNotifications.tsx +++ b/components/AppNotifications.tsx @@ -30,6 +30,7 @@ export default function AppNotifications(props: AppNotificationsProps) { const clickBell = useCallback(() => { if (justClickedOutsideRef.current) return; // if the user just clicked outside, don't show the notifications since the person is trying to close via bell icon setShowNotifications(true); + if (props.notifications.length === 0) return; setLastSeenNotification(props.notifications[props.notifications.length - 1].id); }, [props.notifications]); @@ -74,6 +75,7 @@ export default function AppNotifications(props: AppNotificationsProps) { >{t("notificationBell.link")} ))} + {finalNotifications.length === 0 &&
{t("notificationBell.noNotifications")}
} {props.notifications.length > MIN_NOTIFICATIONS_SHOW &&
From cf07fa48470b785466b5280e5675ba7df3be56fd Mon Sep 17 00:00:00 2001 From: Lina Date: Mon, 6 Oct 2025 16:20:26 +0200 Subject: [PATCH 7/9] Small UI text changes --- components/AppNotifications.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/AppNotifications.tsx b/components/AppNotifications.tsx index a1fe40d..8d4182d 100644 --- a/components/AppNotifications.tsx +++ b/components/AppNotifications.tsx @@ -78,7 +78,7 @@ export default function AppNotifications(props: AppNotificationsProps) { {finalNotifications.length === 0 &&
{t("notificationBell.noNotifications")}
}
{props.notifications.length > MIN_NOTIFICATIONS_SHOW &&
- +
} } From f16bfab1905354a257d71acc3ff4bf33a8f3fcf1 Mon Sep 17 00:00:00 2001 From: Lina Date: Tue, 7 Oct 2025 15:18:47 +0200 Subject: [PATCH 8/9] UI improvements and seen notifications --- components/AppNotifications.tsx | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/components/AppNotifications.tsx b/components/AppNotifications.tsx index 8d4182d..cda4f66 100644 --- a/components/AppNotifications.tsx +++ b/components/AppNotifications.tsx @@ -21,11 +21,17 @@ const MAX_NOTIFICATIONS_SHOW = 7; export default function AppNotifications(props: AppNotificationsProps) { const { t } = useTranslation('projectOverview'); const [lastSeenNotification, setLastSeenNotification] = useLocalStorage('lastSeen', 'releaseNotification', undefined, -1); + const [idsSeenNotifications, setIdsSeenNotifications] = useLocalStorage('idsSeenNotifications', 'releaseNotification', undefined, []); const [showNotifications, setShowNotifications] = useState(false); const [showMoreClicked, setShowMoreClicked] = useState(false); const justClickedOutsideRef = useRef(false); // to prevent showing the notifications if the user just clicked outside const refNotificationBox = useRef(null); - useOnClickOutside(refNotificationBox, () => { setShowNotifications(false); justClickedOutsideRef.current = true; setTimeout(() => justClickedOutsideRef.current = false, 200); }); + useOnClickOutside(refNotificationBox, () => { + setShowNotifications(false); justClickedOutsideRef.current = true; + setTimeout(() => justClickedOutsideRef.current = false, 200); + const newIdsSeen = props.notifications.map(n => n.id).filter(id => !idsSeenNotifications.includes(id)); + if (newIdsSeen.length > 0) setIdsSeenNotifications([...idsSeenNotifications, ...newIdsSeen]); + }); const clickBell = useCallback(() => { if (justClickedOutsideRef.current) return; // if the user just clicked outside, don't show the notifications since the person is trying to close via bell icon @@ -62,17 +68,24 @@ export default function AppNotifications(props: AppNotificationsProps) { {hasNewNotifications &&
} {showNotifications &&
-
+
{t("notificationBell.header")}
{finalNotifications.map((notification, idx) => (
-
{notification.config[props.user?.languageDisplay].headline}
-
{notification.config[props.user?.languageDisplay].description}
- {t("notificationBell.link")} +
+
+
{notification.config[props.user?.languageDisplay].headline}
+
{notification.config[props.user?.languageDisplay].description}
+ {t("notificationBell.link")} +
+
+ {!idsSeenNotifications.includes(notification.id) &&
} +
+
))} {finalNotifications.length === 0 &&
{t("notificationBell.noNotifications")}
} From 67aebe9d989b67a4f6e66e9d992cbf8a4c917336 Mon Sep 17 00:00:00 2001 From: Lina Date: Tue, 7 Oct 2025 15:45:53 +0200 Subject: [PATCH 9/9] display notifications in reverse order --- components/AppNotifications.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/AppNotifications.tsx b/components/AppNotifications.tsx index cda4f66..dffc985 100644 --- a/components/AppNotifications.tsx +++ b/components/AppNotifications.tsx @@ -37,13 +37,13 @@ export default function AppNotifications(props: AppNotificationsProps) { if (justClickedOutsideRef.current) return; // if the user just clicked outside, don't show the notifications since the person is trying to close via bell icon setShowNotifications(true); if (props.notifications.length === 0) return; - setLastSeenNotification(props.notifications[props.notifications.length - 1].id); + setLastSeenNotification(props.notifications[0].id); }, [props.notifications]); const finalNotifications = useMemo(() => { if (props.notifications.length <= MIN_NOTIFICATIONS_SHOW) return props.notifications; - if (!showMoreClicked) return props.notifications.slice(-MIN_NOTIFICATIONS_SHOW); - if (showMoreClicked) return props.notifications.slice(-MAX_NOTIFICATIONS_SHOW); + if (!showMoreClicked) return props.notifications.slice(0, MIN_NOTIFICATIONS_SHOW); + if (showMoreClicked) return props.notifications.slice(0, MAX_NOTIFICATIONS_SHOW); }, [props.notifications, showMoreClicked]); const hasNewNotifications = useMemo(() => {