From 419163ebcf7e17101c3a967c608b841d0aed79c3 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 17:04:29 +0300 Subject: [PATCH 01/30] PM-2133 - dismissable banner --- src/apps/platform/src/PlatformApp.tsx | 3 +- src/apps/platform/src/providers/Providers.tsx | 6 +- .../ChallengeDetailsPage.tsx | 10 +++ .../core/lib/profile/profile-context/index.ts | 2 +- .../profile-context/profile.context.tsx | 4 +- src/libs/shared/lib/components/index.ts | 1 + .../notifications/Notifications.container.tsx | 19 +++++ .../notifications/Notifications.context.tsx | 74 +++++++++++++++++++ .../lib/components/notifications/index.ts | 2 + .../notifications/localstorage.utils.ts | 7 ++ src/libs/ui/lib/components/index.ts | 1 + .../components/notification/Notification.tsx | 19 +++++ .../banner/NotificationBanner.module.scss | 26 +++++++ .../banner/NotificationBanner.stories.tsx | 33 +++++++++ .../banner/NotificationBanner.tsx | 36 +++++++++ .../components/notification/banner/index.ts | 1 + .../ui/lib/components/notification/index.ts | 2 + 17 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 src/libs/shared/lib/components/notifications/Notifications.container.tsx create mode 100644 src/libs/shared/lib/components/notifications/Notifications.context.tsx create mode 100644 src/libs/shared/lib/components/notifications/index.ts create mode 100644 src/libs/shared/lib/components/notifications/localstorage.utils.ts create mode 100644 src/libs/ui/lib/components/notification/Notification.tsx create mode 100644 src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss create mode 100644 src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx create mode 100644 src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx create mode 100644 src/libs/ui/lib/components/notification/banner/index.ts create mode 100644 src/libs/ui/lib/components/notification/index.ts diff --git a/src/apps/platform/src/PlatformApp.tsx b/src/apps/platform/src/PlatformApp.tsx index dd89390fb..e8d9480d5 100644 --- a/src/apps/platform/src/PlatformApp.tsx +++ b/src/apps/platform/src/PlatformApp.tsx @@ -1,7 +1,7 @@ import { FC } from 'react' import { toast, ToastContainer } from 'react-toastify' -import { useViewportUnitsFix } from '~/libs/shared' +import { useViewportUnitsFix, NotificationsContainer } from '~/libs/shared' import { AppFooter } from './components/app-footer' import { AppHeader } from './components/app-header' @@ -14,6 +14,7 @@ const PlatformApp: FC<{}> = () => { return ( +
diff --git a/src/apps/platform/src/providers/Providers.tsx b/src/apps/platform/src/providers/Providers.tsx index b16709bb2..b7064659d 100644 --- a/src/apps/platform/src/providers/Providers.tsx +++ b/src/apps/platform/src/providers/Providers.tsx @@ -1,7 +1,7 @@ import { FC, ReactNode } from 'react' import { authUrlLogout, ProfileProvider } from '~/libs/core' -import { ConfigContextProvider } from '~/libs/shared' +import { ConfigContextProvider, NotificationProvider } from '~/libs/shared' import { PlatformRouterProvider } from './platform-router.provider' @@ -13,7 +13,9 @@ const Providers: FC = props => ( - {props.children} + + {props.children} + diff --git a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx index bd44eb851..066de86c4 100644 --- a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx @@ -60,6 +60,7 @@ import { } from '../../../config/routes.config' import styles from './ChallengeDetailsPage.module.scss' +import { useNotification } from '~/libs/shared' interface Props { className?: string @@ -226,6 +227,7 @@ const computePhaseCompletionFromScreenings = ( // eslint-disable-next-line complexity export const ChallengeDetailsPage: FC = (props: Props) => { + const { showBannerNotification, removeNotification } = useNotification(); const [searchParams, setSearchParams] = useSearchParams() const location = useLocation() const navigate = useNavigate() @@ -1323,6 +1325,14 @@ export const ChallengeDetailsPage: FC = (props: Props) => { : undefined const shouldShowChallengeMetaRow = Boolean(statusLabel) || trackTypePills.length > 0 + useEffect(() => { + const notification = showBannerNotification({ + id: 'ai-review-scores-warning', + message: 'AI Review Scores are advisory only to provide immediate, educational, and actionable feedback to members. AI Review Scores are not influence winner selection.', + }) + return () => notification && removeNotification(notification.id); + }, [showBannerNotification]); + return ( = createContext(defaultProfileContextData) +export const useProfileContext = () => useContext(profileContext); + export default profileContext diff --git a/src/libs/shared/lib/components/index.ts b/src/libs/shared/lib/components/index.ts index ac827849a..8b9d29e69 100644 --- a/src/libs/shared/lib/components/index.ts +++ b/src/libs/shared/lib/components/index.ts @@ -3,6 +3,7 @@ export * from './modals' export * from './profile-picture' export * from './input-skill-selector' export * from './member-skill-editor' +export * from './notifications' export * from './skill-pill' export * from './expandable-list' export * from './grouped-skills-ui' diff --git a/src/libs/shared/lib/components/notifications/Notifications.container.tsx b/src/libs/shared/lib/components/notifications/Notifications.container.tsx new file mode 100644 index 000000000..1e809dcfd --- /dev/null +++ b/src/libs/shared/lib/components/notifications/Notifications.container.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react' + +import { Notification } from '~/libs/ui' + +import { useNotification } from './Notifications.context'; + +const NotificationsContainer: FC = () => { + const { notifications, removeNotification } = useNotification(); + + return ( +
+ {notifications.map(n => ( + + ))} +
+ ) +} + +export default NotificationsContainer diff --git a/src/libs/shared/lib/components/notifications/Notifications.context.tsx b/src/libs/shared/lib/components/notifications/Notifications.context.tsx new file mode 100644 index 000000000..323ec6972 --- /dev/null +++ b/src/libs/shared/lib/components/notifications/Notifications.context.tsx @@ -0,0 +1,74 @@ +import React, { createContext, useContext, useState, useCallback, ReactNode } from "react"; +import { useProfileContext } from "~/libs/core"; +import { dismiss, wasDismissed } from "./localstorage.utils"; + +export type NotificationType = "success" | "error" | "info" | "warning" | "banner"; + +export interface Notification { + id: string; + type: NotificationType; + message: string; + duration?: number; // in ms +} + +type NotifyPayload = string | (Partial & { message: string }) + +interface NotificationContextType { + notifications: Notification[]; + notify: (message: NotifyPayload, type?: NotificationType, duration?: number) => Notification | void; + showBannerNotification: (message: NotifyPayload) => Notification | void; + removeNotification: (id: string) => void; +} + +const NotificationContext = createContext(undefined); + +export const useNotification = (): NotificationContextType => { + const context = useContext(NotificationContext); + if (!context) throw new Error("useNotification must be used within a NotificationProvider"); + return context; +}; + +export const NotificationProvider: React.FC<{ + children: ReactNode, +}> = ({ children }) => { + const profileCtx = useProfileContext() + const uuid = profileCtx.profile?.userId ?? 'annon'; + const [notifications, setNotifications] = useState([]); + + const removeNotification = useCallback((id: string, persist?: boolean) => { + setNotifications(prev => prev.filter(n => n.id !== id)); + if (persist) { + dismiss(id); + } + }, []); + + const notify = useCallback( + (message: NotifyPayload, type: NotificationType = "info", duration = 3000) => { + const id = `${uuid}[${typeof message === 'string' ? message : message.id}]`; + const newNotification: Notification = typeof message === 'string' ? { id, message, type, duration } : { type, duration, ...message, id }; + + if (wasDismissed(id)) { + return; + } + + setNotifications(prev => [...prev, newNotification]); + + if (duration > 0) { + setTimeout(() => removeNotification(id), duration); + } + + return newNotification; + }, + [uuid] + ); + + const showBannerNotification = useCallback(( + message: NotifyPayload, + ) => notify(message, 'banner', 0), [notify]); + + return ( + + {children} + + ); +}; diff --git a/src/libs/shared/lib/components/notifications/index.ts b/src/libs/shared/lib/components/notifications/index.ts new file mode 100644 index 000000000..d2eaff448 --- /dev/null +++ b/src/libs/shared/lib/components/notifications/index.ts @@ -0,0 +1,2 @@ +export { default as NotificationsContainer } from './Notifications.container' +export * from './Notifications.context' diff --git a/src/libs/shared/lib/components/notifications/localstorage.utils.ts b/src/libs/shared/lib/components/notifications/localstorage.utils.ts new file mode 100644 index 000000000..46e776cb8 --- /dev/null +++ b/src/libs/shared/lib/components/notifications/localstorage.utils.ts @@ -0,0 +1,7 @@ +export const wasDismissed = (id: string): boolean => ( + (localStorage.getItem(`dismissed[${id}]`)) !== null +) + +export const dismiss = (id: string): void => { + localStorage.setItem(`dismissed[${id}]`, JSON.stringify(true)) +} diff --git a/src/libs/ui/lib/components/index.ts b/src/libs/ui/lib/components/index.ts index 306c469d6..2e0d2f067 100644 --- a/src/libs/ui/lib/components/index.ts +++ b/src/libs/ui/lib/components/index.ts @@ -3,6 +3,7 @@ export * from './content-layout' export * from './default-member-icon' // NOTE: for some reason, modals needs to be imported prior to form export * from './modals' +export * from './notification' export * from './form' export * from './loading-spinner' export * from './page-divider' diff --git a/src/libs/ui/lib/components/notification/Notification.tsx b/src/libs/ui/lib/components/notification/Notification.tsx new file mode 100644 index 000000000..712d3a83c --- /dev/null +++ b/src/libs/ui/lib/components/notification/Notification.tsx @@ -0,0 +1,19 @@ +import { FC } from 'react' + +import { NotificationBanner } from './banner' + +interface NotificationProps { + notification: { message: string; id: string; type: string } + onClose: (id: string, save?: boolean) => void +} + +const Notification: FC = props => { + + if (props.notification.type === 'banner') { + return props.onClose(props.notification.id, save)} /> + } + + return null; +} + +export default Notification diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss b/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss new file mode 100644 index 000000000..3fa146f8f --- /dev/null +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss @@ -0,0 +1,26 @@ +@import '../../../styles/includes'; + +.wrap { + background: #60267D; + color: $tc-white; + + .inner { + max-width: $xxl-min; + padding: $sp-3 0; + @include pagePaddings; + margin: 0 auto; + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; + @include ltemd { + display: block; + position: relative; + } + } +} + +.close { + cursor: pointer; + color: $tc-white; +} diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx new file mode 100644 index 000000000..4560fe417 --- /dev/null +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx @@ -0,0 +1,33 @@ +/* eslint-disable no-underscore-dangle */ +/* eslint-disable camelcase */ + +import { Meta, StoryObj } from '@storybook/react' + +import NotificationBanner from './NotificationBanner' + +const meta: Meta = { + argTypes: { + persistent: { + defaultValue: false, + description: 'Set to true to allow clicks inside the tooltip', + }, + content: { + description: 'Content displayed inside the tooltip', + }, + }, + component: NotificationBanner, + excludeStories: /.*Decorator$/, + tags: ['autodocs'], + title: 'Components/NotificationBanner', +} + +export default meta + +type Story = StoryObj; + +export const Primary: Story = { + args: { + // children: , + content: 'Help tooltip', + }, +} diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx new file mode 100644 index 000000000..04dfd28b9 --- /dev/null +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx @@ -0,0 +1,36 @@ +import { FC, ReactNode, useCallback } from 'react' + +import styles from './NotificationBanner.module.scss' +import { InformationCircleIcon, XCircleIcon } from '@heroicons/react/outline' + +interface NotificationBannerProps { + persistent?: boolean + content: ReactNode + icon?: ReactNode + onClose?: (save?: boolean) => void +} + +const NotificationBanner: FC = props => { + + return ( +
+
+ {props.icon || ( +
+ +
+ )} + + {props.content} + + {!props.persistent && ( +
props.onClose?.(true)}> + +
+ )} +
+
+ ) +} + +export default NotificationBanner diff --git a/src/libs/ui/lib/components/notification/banner/index.ts b/src/libs/ui/lib/components/notification/banner/index.ts new file mode 100644 index 000000000..51f3cf392 --- /dev/null +++ b/src/libs/ui/lib/components/notification/banner/index.ts @@ -0,0 +1 @@ +export { default as NotificationBanner } from './NotificationBanner' diff --git a/src/libs/ui/lib/components/notification/index.ts b/src/libs/ui/lib/components/notification/index.ts new file mode 100644 index 000000000..ef0ca420e --- /dev/null +++ b/src/libs/ui/lib/components/notification/index.ts @@ -0,0 +1,2 @@ +export * from './banner' +export { default as Notification } from './Notification' From 6f9bb5681a651671b8c88bc9b18a2147cf314416 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 17:06:22 +0300 Subject: [PATCH 02/30] update pr reviewer --- .github/workflows/code_reviewer-updated.yml | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/code_reviewer-updated.yml diff --git a/.github/workflows/code_reviewer-updated.yml b/.github/workflows/code_reviewer-updated.yml new file mode 100644 index 000000000..cc270edc1 --- /dev/null +++ b/.github/workflows/code_reviewer-updated.yml @@ -0,0 +1,22 @@ +name: AI PR Reviewer Updated + +on: + pull_request: + types: + - opened + - synchronize +permissions: + pull-requests: write +jobs: + tc-ai-pr-review: + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: TC AI PR Reviewer + uses: topcoder-platform/tc-ai-pr-reviewer@prompt-update + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) + LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }} + exclude: '**/*.json, **/*.md, **/*.jpg, **/*.png, **/*.jpeg, **/*.bmp, **/*.webp' # Optional: exclude patterns separated by commas From 1fc2ecd0e1e3b7cea822ce99fc2affbaaf061bc2 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 17:33:42 +0300 Subject: [PATCH 03/30] lint fixes --- src/apps/platform/src/PlatformApp.tsx | 2 +- .../ChallengeDetailsPage.tsx | 12 +-- .../profile-context/profile.context.tsx | 2 +- .../notifications/Notifications.container.tsx | 4 +- .../notifications/Notifications.context.tsx | 77 +++++++++++-------- .../components/notification/Notification.tsx | 14 +++- .../banner/NotificationBanner.stories.tsx | 10 +-- .../banner/NotificationBanner.tsx | 8 +- 8 files changed, 78 insertions(+), 51 deletions(-) diff --git a/src/apps/platform/src/PlatformApp.tsx b/src/apps/platform/src/PlatformApp.tsx index e8d9480d5..e9c34cceb 100644 --- a/src/apps/platform/src/PlatformApp.tsx +++ b/src/apps/platform/src/PlatformApp.tsx @@ -1,7 +1,7 @@ import { FC } from 'react' import { toast, ToastContainer } from 'react-toastify' -import { useViewportUnitsFix, NotificationsContainer } from '~/libs/shared' +import { NotificationsContainer, useViewportUnitsFix } from '~/libs/shared' import { AppFooter } from './components/app-footer' import { AppHeader } from './components/app-header' diff --git a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx index 066de86c4..e8cde8672 100644 --- a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx @@ -13,6 +13,7 @@ import { TableLoading } from '~/apps/admin/src/lib' import { handleError } from '~/apps/admin/src/lib/utils' import { EnvironmentConfig } from '~/config' import { BaseModal, Button, InputCheckbox, InputText } from '~/libs/ui' +import { NotificationContextType, useNotification } from '~/libs/shared' import { useFetchScreeningReview, @@ -60,7 +61,6 @@ import { } from '../../../config/routes.config' import styles from './ChallengeDetailsPage.module.scss' -import { useNotification } from '~/libs/shared' interface Props { className?: string @@ -227,7 +227,7 @@ const computePhaseCompletionFromScreenings = ( // eslint-disable-next-line complexity export const ChallengeDetailsPage: FC = (props: Props) => { - const { showBannerNotification, removeNotification } = useNotification(); + const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() const [searchParams, setSearchParams] = useSearchParams() const location = useLocation() const navigate = useNavigate() @@ -1328,10 +1328,12 @@ export const ChallengeDetailsPage: FC = (props: Props) => { useEffect(() => { const notification = showBannerNotification({ id: 'ai-review-scores-warning', - message: 'AI Review Scores are advisory only to provide immediate, educational, and actionable feedback to members. AI Review Scores are not influence winner selection.', + message: `AI Review Scores are advisory only to provide immediate, + educational, and actionable feedback to members. + AI Review Scores are not influence winner selection.`, }) - return () => notification && removeNotification(notification.id); - }, [showBannerNotification]); + return () => notification && removeNotification(notification.id) + }, [showBannerNotification]) return ( = createContext(defaultProfileContextData) -export const useProfileContext = () => useContext(profileContext); +export const useProfileContext = (): ProfileContextData => useContext(profileContext) export default profileContext diff --git a/src/libs/shared/lib/components/notifications/Notifications.container.tsx b/src/libs/shared/lib/components/notifications/Notifications.container.tsx index 1e809dcfd..b71134aba 100644 --- a/src/libs/shared/lib/components/notifications/Notifications.container.tsx +++ b/src/libs/shared/lib/components/notifications/Notifications.container.tsx @@ -2,10 +2,10 @@ import { FC } from 'react' import { Notification } from '~/libs/ui' -import { useNotification } from './Notifications.context'; +import { NotificationContextType, useNotification } from './Notifications.context' const NotificationsContainer: FC = () => { - const { notifications, removeNotification } = useNotification(); + const { notifications, removeNotification }: NotificationContextType = useNotification() return (
diff --git a/src/libs/shared/lib/components/notifications/Notifications.context.tsx b/src/libs/shared/lib/components/notifications/Notifications.context.tsx index 323ec6972..70eb89e9b 100644 --- a/src/libs/shared/lib/components/notifications/Notifications.context.tsx +++ b/src/libs/shared/lib/components/notifications/Notifications.context.tsx @@ -1,8 +1,10 @@ -import React, { createContext, useContext, useState, useCallback, ReactNode } from "react"; -import { useProfileContext } from "~/libs/core"; -import { dismiss, wasDismissed } from "./localstorage.utils"; +import React, { createContext, ReactNode, useCallback, useContext, useMemo, useState } from 'react' -export type NotificationType = "success" | "error" | "info" | "warning" | "banner"; +import { useProfileContext } from '~/libs/core' + +import { dismiss, wasDismissed } from './localstorage.utils' + +export type NotificationType = 'success' | 'error' | 'info' | 'warning' | 'banner'; export interface Notification { id: string; @@ -13,62 +15,77 @@ export interface Notification { type NotifyPayload = string | (Partial & { message: string }) -interface NotificationContextType { +export interface NotificationContextType { notifications: Notification[]; notify: (message: NotifyPayload, type?: NotificationType, duration?: number) => Notification | void; showBannerNotification: (message: NotifyPayload) => Notification | void; removeNotification: (id: string) => void; } -const NotificationContext = createContext(undefined); +const NotificationContext = createContext(undefined) export const useNotification = (): NotificationContextType => { - const context = useContext(NotificationContext); - if (!context) throw new Error("useNotification must be used within a NotificationProvider"); - return context; -}; + const context = useContext(NotificationContext) + if (!context) throw new Error('useNotification must be used within a NotificationProvider') + return context +} export const NotificationProvider: React.FC<{ children: ReactNode, -}> = ({ children }) => { +}> = props => { const profileCtx = useProfileContext() - const uuid = profileCtx.profile?.userId ?? 'annon'; - const [notifications, setNotifications] = useState([]); + const uuid = profileCtx.profile?.userId ?? 'annon' + const [notifications, setNotifications] = useState([]) const removeNotification = useCallback((id: string, persist?: boolean) => { - setNotifications(prev => prev.filter(n => n.id !== id)); + setNotifications(prev => prev.filter(n => n.id !== id)) if (persist) { - dismiss(id); + dismiss(id) } - }, []); + }, []) const notify = useCallback( - (message: NotifyPayload, type: NotificationType = "info", duration = 3000) => { - const id = `${uuid}[${typeof message === 'string' ? message : message.id}]`; - const newNotification: Notification = typeof message === 'string' ? { id, message, type, duration } : { type, duration, ...message, id }; + (message: NotifyPayload, type: NotificationType = 'info', duration = 3000) => { + const id = `${uuid}[${typeof message === 'string' ? message : message.id}]` + const newNotification: Notification + = typeof message === 'string' + ? { duration, id, message, type } + : { duration, type, ...message, id } if (wasDismissed(id)) { - return; + return undefined } - setNotifications(prev => [...prev, newNotification]); + setNotifications(prev => [...prev, newNotification]) if (duration > 0) { - setTimeout(() => removeNotification(id), duration); + setTimeout(() => removeNotification(id), duration) } - return newNotification; + return newNotification }, - [uuid] - ); + [uuid], + ) const showBannerNotification = useCallback(( message: NotifyPayload, - ) => notify(message, 'banner', 0), [notify]); + ) => notify(message, 'banner', 0), [notify]) + + const ctxValue = useMemo(() => ({ + notifications, + notify, + removeNotification, + showBannerNotification, + }), [ + notifications, + notify, + removeNotification, + showBannerNotification, + ]) return ( - - {children} + + {props.children} - ); -}; + ) +} diff --git a/src/libs/ui/lib/components/notification/Notification.tsx b/src/libs/ui/lib/components/notification/Notification.tsx index 712d3a83c..ffffa1439 100644 --- a/src/libs/ui/lib/components/notification/Notification.tsx +++ b/src/libs/ui/lib/components/notification/Notification.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react' +import { FC, useCallback } from 'react' import { NotificationBanner } from './banner' @@ -8,12 +8,20 @@ interface NotificationProps { } const Notification: FC = props => { + const handleClose = useCallback((save?: boolean) => { + props.onClose(props.notification.id, save) + }, [props.onClose]) if (props.notification.type === 'banner') { - return props.onClose(props.notification.id, save)} /> + return ( + + ) } - return null; + return <> } export default Notification diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx index 4560fe417..8af3170e9 100644 --- a/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx @@ -1,19 +1,16 @@ -/* eslint-disable no-underscore-dangle */ -/* eslint-disable camelcase */ - import { Meta, StoryObj } from '@storybook/react' import NotificationBanner from './NotificationBanner' const meta: Meta = { argTypes: { + content: { + description: 'Content displayed inside the tooltip', + }, persistent: { defaultValue: false, description: 'Set to true to allow clicks inside the tooltip', }, - content: { - description: 'Content displayed inside the tooltip', - }, }, component: NotificationBanner, excludeStories: /.*Decorator$/, @@ -27,7 +24,6 @@ type Story = StoryObj; export const Primary: Story = { args: { - // children: , content: 'Help tooltip', }, } diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx index 04dfd28b9..7b9b058c1 100644 --- a/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx @@ -1,8 +1,9 @@ import { FC, ReactNode, useCallback } from 'react' -import styles from './NotificationBanner.module.scss' import { InformationCircleIcon, XCircleIcon } from '@heroicons/react/outline' +import styles from './NotificationBanner.module.scss' + interface NotificationBannerProps { persistent?: boolean content: ReactNode @@ -11,6 +12,9 @@ interface NotificationBannerProps { } const NotificationBanner: FC = props => { + const handleClose = useCallback(() => { + props.onClose?.(true) + }, [props.onClose]) return (
@@ -24,7 +28,7 @@ const NotificationBanner: FC = props => { {props.content} {!props.persistent && ( -
props.onClose?.(true)}> +
)} From 7b7d7b77338d44eb2a2f65b9b9f599c2336d8e86 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 17:49:51 +0300 Subject: [PATCH 04/30] PM-2133 - update desktop styles --- .../notifications/Notifications.container.tsx | 3 ++- .../notifications/Notifications.context.tsx | 1 + .../NotificationsContainer.module.scss | 7 ++++++ .../components/notification/Notification.tsx | 10 +++++++-- .../banner/NotificationBanner.module.scss | 22 ++++++++++++++----- .../banner/NotificationBanner.tsx | 8 ++++--- 6 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 src/libs/shared/lib/components/notifications/NotificationsContainer.module.scss diff --git a/src/libs/shared/lib/components/notifications/Notifications.container.tsx b/src/libs/shared/lib/components/notifications/Notifications.container.tsx index b71134aba..5aa329be7 100644 --- a/src/libs/shared/lib/components/notifications/Notifications.container.tsx +++ b/src/libs/shared/lib/components/notifications/Notifications.container.tsx @@ -3,12 +3,13 @@ import { FC } from 'react' import { Notification } from '~/libs/ui' import { NotificationContextType, useNotification } from './Notifications.context' +import styles from './NotificationsContainer.module.scss' const NotificationsContainer: FC = () => { const { notifications, removeNotification }: NotificationContextType = useNotification() return ( -
+
{notifications.map(n => ( ))} diff --git a/src/libs/shared/lib/components/notifications/Notifications.context.tsx b/src/libs/shared/lib/components/notifications/Notifications.context.tsx index 70eb89e9b..23dbcf558 100644 --- a/src/libs/shared/lib/components/notifications/Notifications.context.tsx +++ b/src/libs/shared/lib/components/notifications/Notifications.context.tsx @@ -9,6 +9,7 @@ export type NotificationType = 'success' | 'error' | 'info' | 'warning' | 'banne export interface Notification { id: string; type: NotificationType; + icon?: ReactNode message: string; duration?: number; // in ms } diff --git a/src/libs/shared/lib/components/notifications/NotificationsContainer.module.scss b/src/libs/shared/lib/components/notifications/NotificationsContainer.module.scss new file mode 100644 index 000000000..9cbc394ef --- /dev/null +++ b/src/libs/shared/lib/components/notifications/NotificationsContainer.module.scss @@ -0,0 +1,7 @@ +@import "@libs/ui/styles/includes"; + +.wrap { + position: relative; + width: 100%; + z-index: 1000; +} diff --git a/src/libs/ui/lib/components/notification/Notification.tsx b/src/libs/ui/lib/components/notification/Notification.tsx index ffffa1439..ab616cb1f 100644 --- a/src/libs/ui/lib/components/notification/Notification.tsx +++ b/src/libs/ui/lib/components/notification/Notification.tsx @@ -1,9 +1,14 @@ -import { FC, useCallback } from 'react' +import { FC, ReactNode, useCallback } from 'react' import { NotificationBanner } from './banner' interface NotificationProps { - notification: { message: string; id: string; type: string } + notification: { + icon?: ReactNode; + id: string; + message: string; + type: string; +} onClose: (id: string, save?: boolean) => void } @@ -15,6 +20,7 @@ const Notification: FC = props => { if (props.notification.type === 'banner') { return ( diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss b/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss index 3fa146f8f..e9aab0a7b 100644 --- a/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss @@ -4,23 +4,35 @@ background: #60267D; color: $tc-white; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 20px; + .inner { max-width: $xxl-min; - padding: $sp-3 0; + padding: $sp-2 0; @include pagePaddings; margin: 0 auto; width: 100%; display: flex; justify-content: space-between; align-items: center; - @include ltemd { - display: block; - position: relative; - } } } .close { cursor: pointer; color: $tc-white; + flex: 0 0; + margin-left: auto; + border-radius: 50%; + border: 2px solid white; + @include ltemd { + margin-left: $sp-3; + } +} + +.icon { + flex: 0 0; + margin-right: $sp-2; } diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx index 7b9b058c1..66900057a 100644 --- a/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx @@ -1,6 +1,8 @@ import { FC, ReactNode, useCallback } from 'react' -import { InformationCircleIcon, XCircleIcon } from '@heroicons/react/outline' +import { InformationCircleIcon } from '@heroicons/react/outline' + +import { IconOutline } from '../../svgs' import styles from './NotificationBanner.module.scss' @@ -20,7 +22,7 @@ const NotificationBanner: FC = props => {
{props.icon || ( -
+
)} @@ -29,7 +31,7 @@ const NotificationBanner: FC = props => { {!props.persistent && (
- +
)}
From a6d033a04b5a78d53a3174d25490e88a27f31390 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 18:38:59 +0300 Subject: [PATCH 05/30] PM-2133 - PR feedback --- .../lib/components/notifications/localstorage.utils.ts | 6 ++++-- src/libs/ui/lib/components/notification/Notification.tsx | 2 +- .../notification/banner/NotificationBanner.stories.tsx | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/libs/shared/lib/components/notifications/localstorage.utils.ts b/src/libs/shared/lib/components/notifications/localstorage.utils.ts index 46e776cb8..c33d2ff72 100644 --- a/src/libs/shared/lib/components/notifications/localstorage.utils.ts +++ b/src/libs/shared/lib/components/notifications/localstorage.utils.ts @@ -1,7 +1,9 @@ +const lsKeyPrefix = 'notificationDismissed' + export const wasDismissed = (id: string): boolean => ( - (localStorage.getItem(`dismissed[${id}]`)) !== null + (localStorage.getItem(`${lsKeyPrefix}[${id}]`)) !== null ) export const dismiss = (id: string): void => { - localStorage.setItem(`dismissed[${id}]`, JSON.stringify(true)) + localStorage.setItem(`${lsKeyPrefix}[${id}]`, JSON.stringify(true)) } diff --git a/src/libs/ui/lib/components/notification/Notification.tsx b/src/libs/ui/lib/components/notification/Notification.tsx index ab616cb1f..34fe01595 100644 --- a/src/libs/ui/lib/components/notification/Notification.tsx +++ b/src/libs/ui/lib/components/notification/Notification.tsx @@ -15,7 +15,7 @@ interface NotificationProps { const Notification: FC = props => { const handleClose = useCallback((save?: boolean) => { props.onClose(props.notification.id, save) - }, [props.onClose]) + }, [props.onClose, props.notification.id]) if (props.notification.type === 'banner') { return ( diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx index 8af3170e9..127e09970 100644 --- a/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.stories.tsx @@ -5,11 +5,11 @@ import NotificationBanner from './NotificationBanner' const meta: Meta = { argTypes: { content: { - description: 'Content displayed inside the tooltip', + description: 'Content displayed inside the notification banner', }, persistent: { defaultValue: false, - description: 'Set to true to allow clicks inside the tooltip', + description: 'Set to true to hide the close icon button', }, }, component: NotificationBanner, From 53baf43a6e1249e86ac93d12db5effac450c8c7b Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 18:54:16 +0300 Subject: [PATCH 06/30] update workflow --- ...{code_reviewer-updated.yml => code_reviewer-complete-diff.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{code_reviewer-updated.yml => code_reviewer-complete-diff.yml} (100%) diff --git a/.github/workflows/code_reviewer-updated.yml b/.github/workflows/code_reviewer-complete-diff.yml similarity index 100% rename from .github/workflows/code_reviewer-updated.yml rename to .github/workflows/code_reviewer-complete-diff.yml From 8f364833fd40f8b63f8169ab34eb87f28c12ebb8 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 22:30:03 +0300 Subject: [PATCH 07/30] test buddy v2 --- .github/workflows/code_reviewer-complete-diff.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code_reviewer-complete-diff.yml b/.github/workflows/code_reviewer-complete-diff.yml index cc270edc1..89472466f 100644 --- a/.github/workflows/code_reviewer-complete-diff.yml +++ b/.github/workflows/code_reviewer-complete-diff.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: TC AI PR Reviewer - uses: topcoder-platform/tc-ai-pr-reviewer@prompt-update + uses: topcoder-platform/tc-ai-pr-reviewer@prompt-v2 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }} From b9fe4ffcf12a5ebcaecc885ebe01d7e371e41e5f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 23:01:03 +0300 Subject: [PATCH 08/30] use only pr buddy v2 --- .../workflows/code_reviewer-complete-diff.yml | 22 ------------------- .github/workflows/code_reviewer.yml | 2 +- 2 files changed, 1 insertion(+), 23 deletions(-) delete mode 100644 .github/workflows/code_reviewer-complete-diff.yml diff --git a/.github/workflows/code_reviewer-complete-diff.yml b/.github/workflows/code_reviewer-complete-diff.yml deleted file mode 100644 index 89472466f..000000000 --- a/.github/workflows/code_reviewer-complete-diff.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: AI PR Reviewer Updated - -on: - pull_request: - types: - - opened - - synchronize -permissions: - pull-requests: write -jobs: - tc-ai-pr-review: - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v3 - - - name: TC AI PR Reviewer - uses: topcoder-platform/tc-ai-pr-reviewer@prompt-v2 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) - LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }} - exclude: '**/*.json, **/*.md, **/*.jpg, **/*.png, **/*.jpeg, **/*.bmp, **/*.webp' # Optional: exclude patterns separated by commas diff --git a/.github/workflows/code_reviewer.yml b/.github/workflows/code_reviewer.yml index 02f198a18..40e24b681 100644 --- a/.github/workflows/code_reviewer.yml +++ b/.github/workflows/code_reviewer.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: TC AI PR Reviewer - uses: topcoder-platform/tc-ai-pr-reviewer@master + uses: topcoder-platform/tc-ai-pr-reviewer@prompt-v2 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }} From ca1e43278967045178ea065bf32de92db4f2340e Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 17 Oct 2025 09:08:25 +0300 Subject: [PATCH 09/30] typo fix --- .github/workflows/code_reviewer.yml | 2 +- .../ChallengeDetailsPage/ChallengeDetailsPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code_reviewer.yml b/.github/workflows/code_reviewer.yml index 40e24b681..02f198a18 100644 --- a/.github/workflows/code_reviewer.yml +++ b/.github/workflows/code_reviewer.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: TC AI PR Reviewer - uses: topcoder-platform/tc-ai-pr-reviewer@prompt-v2 + uses: topcoder-platform/tc-ai-pr-reviewer@master with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret) LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }} diff --git a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx index e8cde8672..29e8b058d 100644 --- a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailsPage/ChallengeDetailsPage.tsx @@ -1330,7 +1330,7 @@ export const ChallengeDetailsPage: FC = (props: Props) => { id: 'ai-review-scores-warning', message: `AI Review Scores are advisory only to provide immediate, educational, and actionable feedback to members. - AI Review Scores are not influence winner selection.`, + AI Review Scores do not influence winner selection.`, }) return () => notification && removeNotification(notification.id) }, [showBannerNotification]) From f44825243d5b51ab8092367ef8b9100226f47ead Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 17 Oct 2025 09:17:22 +0300 Subject: [PATCH 10/30] deploy to dev --- .circleci/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index a4479adcc..c8998be2c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -232,6 +232,7 @@ workflows: - feat/system-admin - feat/v6 - pm-2074_1 + - feat/ai-workflows - deployQa: context: org-global From 1365c0ed3cedce0006c8f6c68a0f30598276bbd1 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 20 Oct 2025 14:45:26 +0300 Subject: [PATCH 11/30] PM-1904 - show AI icon when AI review is assigned to challenge --- .../src/lib/assets/icons/icon-ai-review.svg | 5 ++++ .../icons/icon-phase-appeal-response.svg | 3 ++ .../lib/assets/icons/icon-phase-appeal.svg | 3 ++ .../assets/icons/icon-phase-registration.svg | 3 ++ .../lib/assets/icons/icon-phase-review.svg | 3 ++ ...bmission.svg => icon-phase-submission.svg} | 0 .../lib/assets/icons/icon-phase-winners.svg | 3 ++ src/apps/review/src/lib/assets/icons/index.ts | 29 ++++++++++++++++++- .../TableActiveReviews.module.scss | 4 +++ .../TableActiveReviews/TableActiveReviews.tsx | 20 +++++++++---- .../src/lib/hooks/useFetchActiveReviews.ts | 1 + .../models/ActiveReviewAssignment.model.ts | 1 + .../models/BackendMyReviewAssignment.model.ts | 1 + 13 files changed, 70 insertions(+), 6 deletions(-) create mode 100644 src/apps/review/src/lib/assets/icons/icon-ai-review.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-phase-appeal-response.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-phase-appeal.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-phase-registration.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-phase-review.svg rename src/apps/review/src/lib/assets/icons/{icon-submission.svg => icon-phase-submission.svg} (100%) create mode 100644 src/apps/review/src/lib/assets/icons/icon-phase-winners.svg diff --git a/src/apps/review/src/lib/assets/icons/icon-ai-review.svg b/src/apps/review/src/lib/assets/icons/icon-ai-review.svg new file mode 100644 index 000000000..0448ec082 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-ai-review.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-phase-appeal-response.svg b/src/apps/review/src/lib/assets/icons/icon-phase-appeal-response.svg new file mode 100644 index 000000000..edc7d9459 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-phase-appeal-response.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-phase-appeal.svg b/src/apps/review/src/lib/assets/icons/icon-phase-appeal.svg new file mode 100644 index 000000000..f28dd1bf8 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-phase-appeal.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-phase-registration.svg b/src/apps/review/src/lib/assets/icons/icon-phase-registration.svg new file mode 100644 index 000000000..7305b63dd --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-phase-registration.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-phase-review.svg b/src/apps/review/src/lib/assets/icons/icon-phase-review.svg new file mode 100644 index 000000000..0e0f58507 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-phase-review.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-submission.svg b/src/apps/review/src/lib/assets/icons/icon-phase-submission.svg similarity index 100% rename from src/apps/review/src/lib/assets/icons/icon-submission.svg rename to src/apps/review/src/lib/assets/icons/icon-phase-submission.svg diff --git a/src/apps/review/src/lib/assets/icons/icon-phase-winners.svg b/src/apps/review/src/lib/assets/icons/icon-phase-winners.svg new file mode 100644 index 000000000..146c041f6 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-phase-winners.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/index.ts b/src/apps/review/src/lib/assets/icons/index.ts index 067315b11..7ec1bf70b 100644 --- a/src/apps/review/src/lib/assets/icons/index.ts +++ b/src/apps/review/src/lib/assets/icons/index.ts @@ -2,6 +2,13 @@ import { ReactComponent as IconArrowLeft } from './arrow-left.svg' import { ReactComponent as IconExternalLink } from './external-link.svg' import { ReactComponent as IconChevronDown } from './selector.svg' import { ReactComponent as IconError } from './icon-error.svg' +import { ReactComponent as IconAiReview } from './icon-ai-review.svg' +import { ReactComponent as IconSubmission } from './icon-phase-submission.svg' +import { ReactComponent as IconRegistration } from './icon-phase-registration.svg' +import { ReactComponent as IconReview } from './icon-phase-review.svg' +import { ReactComponent as IconAppeal } from './icon-phase-appeal.svg' +import { ReactComponent as IconAppealResponse } from './icon-phase-appeal-response.svg' +import { ReactComponent as IconPhaseWinners } from './icon-phase-winners.svg' export * from './editor/bold' export * from './editor/code' @@ -19,4 +26,24 @@ export * from './editor/table' export * from './editor/unordered-list' export * from './editor/upload-file' -export { IconArrowLeft, IconExternalLink, IconChevronDown, IconError } +export { + IconArrowLeft, + IconExternalLink, + IconChevronDown, + IconError, + IconAiReview, + IconSubmission, + IconReview, + IconAppeal, + IconAppealResponse, + IconPhaseWinners, +} + +export const phasesIcons = { + appeal: IconAppeal, + appealResponse: IconAppealResponse, + 'iterative review': IconReview, + registration: IconRegistration, + review: IconReview, + submission: IconSubmission, +} diff --git a/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.module.scss b/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.module.scss index 2798cf333..6679068ba 100644 --- a/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.module.scss +++ b/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.module.scss @@ -79,3 +79,7 @@ background-color: $red-25; color: $red-140; } + +.mr2 { + margin-right: $sp-2; +} diff --git a/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.tsx b/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.tsx index ef4d1c63f..329dd4bb8 100644 --- a/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.tsx +++ b/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.tsx @@ -17,6 +17,7 @@ import { } from '../../models' import { TableWrapper } from '../TableWrapper' import { ProgressBar } from '../ProgressBar' +import { IconAiReview, phasesIcons } from '../../assets/icons' import styles from './TableActiveReviews.module.scss' @@ -253,11 +254,20 @@ export const TableActiveReviews: FC = (props: Props) => { isSortable: true, label: 'Phase', propertyName: 'currentPhase', - renderer: (data: ActiveReviewAssignment) => ( -
- {data.currentPhase} -
- ), + renderer: (data: ActiveReviewAssignment) => { + const Icon = data.hasAsAIReview ? IconAiReview : ( + phasesIcons[data.currentPhase.toLowerCase() as keyof typeof phasesIcons] + ) + + return ( +
+ {Icon && ( + + )} + {data.currentPhase} +
+ ) + }, type: 'element', }, { diff --git a/src/apps/review/src/lib/hooks/useFetchActiveReviews.ts b/src/apps/review/src/lib/hooks/useFetchActiveReviews.ts index b226180f2..73ed5d937 100644 --- a/src/apps/review/src/lib/hooks/useFetchActiveReviews.ts +++ b/src/apps/review/src/lib/hooks/useFetchActiveReviews.ts @@ -141,6 +141,7 @@ export const transformAssignments = ( .local() .format(TABLE_DATE_FORMAT) : undefined, + hasAsAIReview: base.hasAsAIReview, id: base.challengeId, index: currentIndex, name: base.challengeName, diff --git a/src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts b/src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts index 2ba052480..04b2553d3 100644 --- a/src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts +++ b/src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts @@ -9,6 +9,7 @@ export interface ActiveReviewAssignment { currentPhaseEndDateString?: string challengeEndDate?: string | Date | null challengeEndDateString?: string + hasAsAIReview: boolean; timeLeft?: string timeLeftColor?: string timeLeftStatus?: string diff --git a/src/apps/review/src/lib/models/BackendMyReviewAssignment.model.ts b/src/apps/review/src/lib/models/BackendMyReviewAssignment.model.ts index 4de66927c..acfea7673 100644 --- a/src/apps/review/src/lib/models/BackendMyReviewAssignment.model.ts +++ b/src/apps/review/src/lib/models/BackendMyReviewAssignment.model.ts @@ -20,6 +20,7 @@ export interface BackendMyReviewAssignment { challengeEndDate: string | null currentPhaseName: string currentPhaseEndDate: string | null + hasAsAIReview: boolean; timeLeftInCurrentPhase: number | null resourceRoleName: string reviewProgress: number | null From 6c926846711da1584b9266372119fb8cde602009 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 20 Oct 2025 15:15:50 +0300 Subject: [PATCH 12/30] PM-1904 - show AI icon banner --- .../src/lib/assets/icons/icon-submission.svg | 10 ++++++++++ .../ActiveReviewsPage/ActiveReviewsPage.tsx | 15 +++++++++++++++ .../banner/NotificationBanner.module.scss | 3 +++ .../notification/banner/NotificationBanner.tsx | 8 ++++---- 4 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 src/apps/review/src/lib/assets/icons/icon-submission.svg diff --git a/src/apps/review/src/lib/assets/icons/icon-submission.svg b/src/apps/review/src/lib/assets/icons/icon-submission.svg new file mode 100644 index 000000000..4b96fe2b4 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-submission.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx index 6e66e7d82..af855da74 100644 --- a/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx @@ -17,6 +17,7 @@ import classNames from 'classnames' import { Pagination, TableLoading } from '~/apps/admin/src/lib' import { Sort } from '~/apps/admin/src/platform/gamification-admin/src/game-lib' import { Button, IconOutline, InputText } from '~/libs/ui' +import { NotificationContextType, useNotification } from '~/libs/shared' import { CHALLENGE_TYPE_SELECT_ALL_OPTION } from '../../../config/index.config' import { @@ -39,6 +40,7 @@ import { SelectOption } from '../../../lib/models/SelectOption.model' import { getAllowedTypeAbbreviationsByTrack } from '../../../lib/utils/challengeTypesByTrack' import styles from './ActiveReviewsPage.module.scss' +import { IconAiReview } from '../../../lib/assets/icons' interface Props { className?: string @@ -50,6 +52,8 @@ const DEFAULT_SORT: Sort = { } export const ActiveReviewsPage: FC = (props: Props) => { + const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() + const { loginUserInfo, }: ReviewAppContextModel = useContext(ReviewAppContext) @@ -193,6 +197,17 @@ export const ActiveReviewsPage: FC = (props: Props) => { }) }, [loadActiveReviews, sort]) + + useEffect(() => { + const notification = showBannerNotification({ + id: 'ai-review-icon-notification', + icon: , + message: `Challenges with this icon indicates that an ​​AI + review has been completed in particular phase.`, + }) + return () => notification && removeNotification(notification.id) + }, [showBannerNotification]) + return ( svg path { + fill: $tc-white; + } } diff --git a/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx index 66900057a..98aa008fd 100644 --- a/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.tsx @@ -21,11 +21,11 @@ const NotificationBanner: FC = props => { return (
- {props.icon || ( -
+
+ {props.icon || ( -
- )} + )} +
{props.content} From 9aef27f7398229f6fd613e8dc69a6b547a5f5d9a Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 20 Oct 2025 15:30:40 +0300 Subject: [PATCH 13/30] lint fix --- .../ActiveReviewsPage/ActiveReviewsPage.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx index af855da74..3e51b42ea 100644 --- a/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx @@ -38,9 +38,9 @@ import { import { ReviewAppContextModel } from '../../../lib/models' import { SelectOption } from '../../../lib/models/SelectOption.model' import { getAllowedTypeAbbreviationsByTrack } from '../../../lib/utils/challengeTypesByTrack' +import { IconAiReview } from '../../../lib/assets/icons' import styles from './ActiveReviewsPage.module.scss' -import { IconAiReview } from '../../../lib/assets/icons' interface Props { className?: string @@ -197,13 +197,12 @@ export const ActiveReviewsPage: FC = (props: Props) => { }) }, [loadActiveReviews, sort]) - useEffect(() => { const notification = showBannerNotification({ - id: 'ai-review-icon-notification', icon: , - message: `Challenges with this icon indicates that an ​​AI - review has been completed in particular phase.`, + id: 'ai-review-icon-notification', + message: `Challenges with this icon indicate that + one or more AI reviews will be conducted for each member submission.`, }) return () => notification && removeNotification(notification.id) }, [showBannerNotification]) From fc857e844838211e538ac33a29fddfeaaa20b847 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 23 Oct 2025 10:27:46 +0300 Subject: [PATCH 14/30] PM-1904 - fix typo & fix effect dependency --- .../lib/components/TableActiveReviews/TableActiveReviews.tsx | 2 +- src/apps/review/src/lib/hooks/useFetchActiveReviews.ts | 2 +- src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts | 2 +- .../review/src/lib/models/BackendMyReviewAssignment.model.ts | 2 +- .../ActiveReviewsPage/ActiveReviewsPage.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.tsx b/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.tsx index 329dd4bb8..014430141 100644 --- a/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.tsx +++ b/src/apps/review/src/lib/components/TableActiveReviews/TableActiveReviews.tsx @@ -255,7 +255,7 @@ export const TableActiveReviews: FC = (props: Props) => { label: 'Phase', propertyName: 'currentPhase', renderer: (data: ActiveReviewAssignment) => { - const Icon = data.hasAsAIReview ? IconAiReview : ( + const Icon = data.hasAIReview ? IconAiReview : ( phasesIcons[data.currentPhase.toLowerCase() as keyof typeof phasesIcons] ) diff --git a/src/apps/review/src/lib/hooks/useFetchActiveReviews.ts b/src/apps/review/src/lib/hooks/useFetchActiveReviews.ts index 73ed5d937..e12ee1d3a 100644 --- a/src/apps/review/src/lib/hooks/useFetchActiveReviews.ts +++ b/src/apps/review/src/lib/hooks/useFetchActiveReviews.ts @@ -141,7 +141,7 @@ export const transformAssignments = ( .local() .format(TABLE_DATE_FORMAT) : undefined, - hasAsAIReview: base.hasAsAIReview, + hasAIReview: base.hasAIReview, id: base.challengeId, index: currentIndex, name: base.challengeName, diff --git a/src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts b/src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts index 04b2553d3..0fd16bf73 100644 --- a/src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts +++ b/src/apps/review/src/lib/models/ActiveReviewAssignment.model.ts @@ -9,7 +9,7 @@ export interface ActiveReviewAssignment { currentPhaseEndDateString?: string challengeEndDate?: string | Date | null challengeEndDateString?: string - hasAsAIReview: boolean; + hasAIReview: boolean; timeLeft?: string timeLeftColor?: string timeLeftStatus?: string diff --git a/src/apps/review/src/lib/models/BackendMyReviewAssignment.model.ts b/src/apps/review/src/lib/models/BackendMyReviewAssignment.model.ts index acfea7673..7819ce591 100644 --- a/src/apps/review/src/lib/models/BackendMyReviewAssignment.model.ts +++ b/src/apps/review/src/lib/models/BackendMyReviewAssignment.model.ts @@ -20,7 +20,7 @@ export interface BackendMyReviewAssignment { challengeEndDate: string | null currentPhaseName: string currentPhaseEndDate: string | null - hasAsAIReview: boolean; + hasAIReview: boolean; timeLeftInCurrentPhase: number | null resourceRoleName: string reviewProgress: number | null diff --git a/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx index 3e51b42ea..d8d099bfc 100644 --- a/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ActiveReviewsPage/ActiveReviewsPage.tsx @@ -205,7 +205,7 @@ export const ActiveReviewsPage: FC = (props: Props) => { one or more AI reviews will be conducted for each member submission.`, }) return () => notification && removeNotification(notification.id) - }, [showBannerNotification]) + }, [showBannerNotification, removeNotification]) return ( Date: Tue, 28 Oct 2025 17:26:10 +0200 Subject: [PATCH 15/30] PM-1905 - ai reviews --- .../AiReviewsTable/AiReviewsTable.module.scss | 63 +++++++++++ .../AiReviewsTable/AiReviewsTable.tsx | 87 +++++++++++++++ .../lib/components/AiReviewsTable/index.ts | 1 + .../ChallengeDetailsContent.tsx | 6 + .../TabContentSubmissions.tsx | 14 +++ .../CollapsibleAiReviewsRow.module.scss | 21 ++++ .../CollapsibleAiReviewsRow.tsx | 44 ++++++++ .../CollapsibleAiReviewsRow/index.ts | 1 + .../SubmissionHistoryModal.module.scss | 18 +++ .../SubmissionHistoryModal.tsx | 103 ++++++++++++++---- src/apps/review/src/lib/hooks/index.ts | 1 + .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 94 ++++++++++++++++ .../src/lib/models/ChallengeInfo.model.ts | 2 +- 13 files changed, 430 insertions(+), 25 deletions(-) create mode 100644 src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss create mode 100644 src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx create mode 100644 src/apps/review/src/lib/components/AiReviewsTable/index.ts create mode 100644 src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.module.scss create mode 100644 src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx create mode 100644 src/apps/review/src/lib/components/CollapsibleAiReviewsRow/index.ts create mode 100644 src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss new file mode 100644 index 000000000..3d1f357ab --- /dev/null +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss @@ -0,0 +1,63 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + font-family: "Nunito Sans", sans-serif; + max-width: 100%; + overflow: hidden; +} + +.reviewsTable { + width: 100%; + border-collapse: collapse; + + th { + border-top: 1px solid #A8A8A8; + font-weight: bold; + background: #E0E4E8; + } + + th, td { + text-align: left; + font-size: 14px; + padding: $sp-2 $sp-4; + border-bottom: 1px solid #A8A8A8; + } + + .scoreCol { + text-align: right; + + color: #0D61BF; + } +} + +.aiReviewer { + display: flex; + align-items: center; + gap: $sp-2; + + .icon { + display: flex; + align-items: center; + flex: 0 0; + } + + .workflowName { + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.result { + display: flex; + align-items: center; + gap: $sp-2; + + :global(.icon) { + color: #C1294F; + &:global(.passed) { + color: $teal-160; + } + } +} diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx new file mode 100644 index 000000000..0b459f564 --- /dev/null +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -0,0 +1,87 @@ +import { FC, useMemo } from 'react' +import moment from 'moment' + +import { CheckIcon, MinusCircleIcon } from '@heroicons/react/outline' + +import { AiWorkflowRunsResponse, useFetchAiWorkflowsRuns } from '../../hooks' +import { IconAiReview } from '../../assets/icons' +import { TABLE_DATE_FORMAT } from '../../../config/index.config' + +import styles from './AiReviewsTable.module.scss' + +interface AiReviewsTableProps { + submissionId: string + reviewers: { aiWorkflowId: string }[] +} + +const AiReviewsTable: FC = props => { + const aiWorkflowIds = useMemo(() => props.reviewers.map(r => r.aiWorkflowId), [props.reviewers]) + const { runs, isLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(props.submissionId, aiWorkflowIds) + + return ( +
+ + + + + + + + + {!runs.length && isLoading && ( + + + + )} + + {runs.map(run => ( + + + + + + + ))} +
AI ReviewerReview DateScoreResult
Loading...
+
+ + + + + {run.workflow.name} + +
+
+ {run.status === 'SUCCESS' && ( + moment(run.completedAt) + .local() + .format(TABLE_DATE_FORMAT) + )} + + {run.status === 'SUCCESS' && run.score} + + {run.status === 'SUCCESS' && ( +
+ {run.score >= run.workflow.scorecard.minimumPassingScore + ? ( + <> + + {' '} + Passed + + ) + : ( + <> + + {' '} + Passed + + )} +
+ )} +
+
+ ) +} + +export default AiReviewsTable diff --git a/src/apps/review/src/lib/components/AiReviewsTable/index.ts b/src/apps/review/src/lib/components/AiReviewsTable/index.ts new file mode 100644 index 000000000..2a13d8a91 --- /dev/null +++ b/src/apps/review/src/lib/components/AiReviewsTable/index.ts @@ -0,0 +1 @@ +export { default as AiReviewsTable } from './AiReviewsTable' diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx index 3643e2994..d72fe3f0e 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx @@ -126,6 +126,7 @@ interface SubmissionTabParams { isDownloadingSubmission: useDownloadSubmissionProps['isLoading'] downloadSubmission: useDownloadSubmissionProps['downloadSubmission'] isActiveChallenge: boolean + aiReviewers: { aiWorkflowId: string }[] } const renderSubmissionTab = ({ @@ -137,6 +138,7 @@ const renderSubmissionTab = ({ isDownloadingSubmission, downloadSubmission, isActiveChallenge, + aiReviewers, }: SubmissionTabParams): JSX.Element => { const isSubmissionTab = selectedTabNormalized === 'submission' const isTopgearSubmissionTab = selectedTabNormalized === 'topgearsubmission' @@ -155,6 +157,7 @@ const renderSubmissionTab = ({ if (canShowSubmissionList) { return ( = (props: Props) => { if (SUBMISSION_TAB_KEYS.has(selectedTabNormalized)) { return renderSubmissionTab({ + aiReviewers: ( + challengeInfo?.reviewers?.filter(r => !!r.aiWorkflowId) as { aiWorkflowId: string }[] + ) ?? [], downloadSubmission, isActiveChallenge: props.isActiveChallenge, isDownloadingSubmission, diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx index 81816c9fa..91c7cd4e4 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx @@ -41,10 +41,12 @@ import { } from '../../utils' import type { SubmissionHistoryPartition } from '../../utils' import { TABLE_DATE_FORMAT } from '../../../config/index.config' +import { CollapsibleAiReviewsRow } from '../CollapsibleAiReviewsRow' import styles from './TabContentSubmissions.module.scss' interface Props { + aiReviewers?: { aiWorkflowId: string }[] submissions: BackendSubmission[] isLoading: boolean isDownloading: IsRemovingType @@ -340,6 +342,17 @@ export const TabContentSubmissions: FC = props => { }, type: 'element', }, + ...(!props.aiReviewers?.length ? [] : [{ + label: 'Reviewer', + propertyName: 'submittedDate', + renderer: (submission: BackendSubmission) => ( + + ), + type: 'element', + } as TableColumn]), ] if (shouldShowHistoryActions) { @@ -442,6 +455,7 @@ export const TabContentSubmissions: FC = props => { isDownloading={props.isDownloading} getRestriction={getHistoryRestriction} getSubmissionMeta={resolveSubmissionMeta} + aiReviewers={props.aiReviewers} /> ) diff --git a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.module.scss b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.module.scss new file mode 100644 index 000000000..036c19d79 --- /dev/null +++ b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.module.scss @@ -0,0 +1,21 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + width: 100%; + text-align: left; +} + +.reviewersDropown { + display: flex; + align-items: center; + gap: $sp-2; + + svg { + color: #767676; + } +} + +.table { + margin-top: $sp-2; + margin-left: -1 * $sp-4; +} diff --git a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx new file mode 100644 index 000000000..e21e330ea --- /dev/null +++ b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx @@ -0,0 +1,44 @@ +import { FC, useCallback, useState } from 'react' + +import { IconOutline } from '~/libs/ui' + +import { AiReviewsTable } from '../AiReviewsTable' + +import styles from './CollapsibleAiReviewsRow.module.scss' + +interface CollapsibleAiReviewsRowProps { + aiReviewers: { aiWorkflowId: string }[] + submissionId: string +} + +const CollapsibleAiReviewsRow: FC = props => { + const aiReviewersCount = props.aiReviewers.length + + const [isOpen, setIsOpen] = useState(false) + + const toggleOpen = useCallback(() => { + setIsOpen(wasOpen => !wasOpen) + }, []) + + return ( +
+ + {aiReviewersCount} + {' '} + AI Reviewer + {aiReviewersCount === 1 ? '' : 's'} + + + {isOpen && ( +
+ +
+ )} +
+ ) +} + +export default CollapsibleAiReviewsRow diff --git a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/index.ts b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/index.ts new file mode 100644 index 000000000..757542122 --- /dev/null +++ b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/index.ts @@ -0,0 +1 @@ +export { default as CollapsibleAiReviewsRow } from './CollapsibleAiReviewsRow' diff --git a/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.module.scss b/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.module.scss index 59fc9266f..6fc566bec 100644 --- a/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.module.scss +++ b/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.module.scss @@ -146,3 +146,21 @@ font-size: 14px; text-align: center; } + +.reviewersDropown { + display: flex; + align-items: center; + gap: $sp-2; + + svg { + color: #767676; + } +} + +.table .aiReviewersTableRow.aiReviewersTableRow { + padding: 0; +} + +.aiReviewersTable { + margin-top: -1px; +} diff --git a/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx b/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx index 71a53ae8b..8934a50dd 100644 --- a/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx +++ b/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx @@ -1,4 +1,4 @@ -import { FC, MouseEvent, useCallback, useMemo } from 'react' +import { FC, Fragment, MouseEvent, useCallback, useMemo, useState } from 'react' import { toast } from 'react-toastify' import classNames from 'classnames' import moment from 'moment' @@ -9,6 +9,7 @@ import { BaseModal, IconOutline, Tooltip } from '~/libs/ui' import { SubmissionInfo } from '../../models' import { TABLE_DATE_FORMAT } from '../../../config/index.config' +import { AiReviewsTable } from '../AiReviewsTable' import styles from './SubmissionHistoryModal.module.scss' @@ -29,6 +30,7 @@ export interface SubmissionHistoryModalProps { * when the provided submission entry is missing those fields. */ getSubmissionMeta?: (submissionId: string) => SubmissionInfo | undefined + aiReviewers?: { aiWorkflowId: string }[] } function getTimestamp(submission: SubmissionInfo): number { @@ -97,6 +99,9 @@ export const SubmissionHistoryModal: FC = (props: S .sort((a, b) => getTimestamp(b) - getTimestamp(a)), [props.submissions], ) + + const [toggledRows, setToggledRows] = useState(new Set()) + const resolvedMemberInfo = useMemo(() => { for (const submission of sortedSubmissions) { if (submission.userInfo?.memberHandle) { @@ -171,6 +176,19 @@ export const SubmissionHistoryModal: FC = (props: S .catch(() => undefined) }, [handleCopy]) + const toggleRow = useCallback((rowId: string) => { + setToggledRows(previous => { + const next = new Set(previous) + if (next.has(rowId)) { + next.delete(rowId) + } else { + next.add(rowId) + } + + return next + }) + }, []) + const renderHistoryRow = useCallback((submission: SubmissionInfo): JSX.Element => { const fallbackMeta = props.getSubmissionMeta?.(submission.id) ?? undefined const resolvedVirusScan = submission.virusScan ?? fallbackMeta?.virusScan @@ -222,33 +240,69 @@ export const SubmissionHistoryModal: FC = (props: S ) + function toggle(): void { + toggleRow(submission.id) + } + return ( - - - - {renderedDownloadButton} - {copyButton} - - - - {submittedDisplay} - - - {resolvedVirusScan === true ? ( - - - - ) : resolvedVirusScan === false ? ( - - + + + + + {renderedDownloadButton} + {copyButton} - ) : ( - - + + + {submittedDisplay} + + + {resolvedVirusScan === true ? ( + + + + ) : resolvedVirusScan === false ? ( + + + + ) : ( + - + )} + + {!!props.aiReviewers?.length && ( + + + {props.aiReviewers.length} + {' '} + AI Reviewer + {props.aiReviewers.length === 1 ? '' : 's'} + + + )} - - + + {toggledRows.has(submission.id) && ( + + +
+ +
+ + + )} +
) - }, [handleCopy, props.downloadSubmission, props.getRestriction, props.getSubmissionMeta, props.isDownloading]) + }, [ + handleCopy, + props.downloadSubmission, + props.getRestriction, + props.getSubmissionMeta, + props.isDownloading, + toggledRows, + ]) return ( = (props: S Submission ID Submitted Virus Scan + Reviewer diff --git a/src/apps/review/src/lib/hooks/index.ts b/src/apps/review/src/lib/hooks/index.ts index 38b71fef1..5d2b5455f 100644 --- a/src/apps/review/src/lib/hooks/index.ts +++ b/src/apps/review/src/lib/hooks/index.ts @@ -18,3 +18,4 @@ export * from './useScoreVisibility' export * from './useSubmissionDownloadAccess' export * from './useSubmissionHistory' export * from './useScorecardPassingScores' +export * from './useFetchAiWorkflowRuns' diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts new file mode 100644 index 000000000..a5849c13d --- /dev/null +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -0,0 +1,94 @@ +import { useEffect } from 'react' +import useSWR, { SWRResponse } from 'swr' + +import { EnvironmentConfig } from '~/config' +import { xhrGetAsync } from '~/libs/core' +import { handleError } from '~/libs/shared/lib/utils/handle-error' + +export interface AiWorkflowRun { + id: string; + completedAt: string; + status: string; + score: number; + workflow: { + name: string; + description: string; + scorecard: { + name: string; + minimumPassingScore: number; + } + } +} + +const TC_API_BASE_URL = EnvironmentConfig.API.V6 + +export interface AiWorkflowRunsResponse { + runs: AiWorkflowRun[] + isLoading: boolean +} + +export function useFetchAiWorkflowRuns( + workflowId: string, + submissionId: string, +): AiWorkflowRunsResponse { + // Use swr hooks for challenge info fetching + const { + data: runs = [], + error: fetchError, + isValidating: isLoading, + }: SWRResponse = useSWR( + `${TC_API_BASE_URL}/workflows/${workflowId}/runs?submissionId=${submissionId}`, + { + isPaused: () => !workflowId || !submissionId, + }, + ) + + // Show backend error when fetching challenge info + useEffect(() => { + if (fetchError) { + handleError(fetchError) + } + }, [fetchError]) + + return { + isLoading, + runs, + } +} + +export function useFetchAiWorkflowsRuns( + submissionId: string, + workflowIds: string[], +): AiWorkflowRunsResponse { + // Use swr hooks for challenge info fetching + const { + data: runs = [], + error: fetchError, + isValidating: isLoading, + }: SWRResponse = useSWR( + `${TC_API_BASE_URL}/workflows/${workflowIds.join(',')}/runs?submissionId=${submissionId}`, + { + fetcher: () => Promise.all( + workflowIds.map(workflowId => ( + xhrGetAsync( + `${TC_API_BASE_URL}/workflows/${workflowId}/runs?submissionId=${submissionId}`, + ) + )), + ) + .then(results => results.flat()), + isPaused: () => !workflowIds?.length || !submissionId, + }, + ) + + // Show backend error when fetching challenge info + useEffect(() => { + if (fetchError) { + handleError(fetchError) + } + }, [fetchError]) + + return { + isLoading, + runs, + } +} diff --git a/src/apps/review/src/lib/models/ChallengeInfo.model.ts b/src/apps/review/src/lib/models/ChallengeInfo.model.ts index 10f4a078e..b2b417230 100644 --- a/src/apps/review/src/lib/models/ChallengeInfo.model.ts +++ b/src/apps/review/src/lib/models/ChallengeInfo.model.ts @@ -72,7 +72,7 @@ export interface ChallengeInfo { basePayment: number incrementalPayment: number type: string - isAIReviewer: boolean + aiWorkflowId?: string; }[] currentPhaseObject?: BackendPhase metadata?: BackendMetadata[] From b4f607e687c76f7636f82408e156613d4bf542ab Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 29 Oct 2025 13:53:03 +0200 Subject: [PATCH 16/30] PM-1905 - mobile view --- .../AiReviewsTable/AiReviewsTable.module.scss | 27 +++++++ .../AiReviewsTable/AiReviewsTable.tsx | 80 +++++++++++++++++++ .../TabContentSubmissions.module.scss | 10 +++ .../TabContentSubmissions.tsx | 3 + .../CollapsibleAiReviewsRow.module.scss | 15 ++++ .../CollapsibleAiReviewsRow.tsx | 3 +- .../components/table/table-column.model.ts | 1 + 7 files changed, 138 insertions(+), 1 deletion(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss index 3d1f357ab..445b178e6 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss @@ -61,3 +61,30 @@ } } } + +.mobileCard { + border-top: 1px solid #A8A8A8; + margin-top: $sp-2; +} + +.mobileRow { + display: flex; + padding-top: $sp-2; + padding-left: $sp-4; + padding-right: $sp-4; + > * { + flex: 1 1 50%; + } +} +.label { + font-weight: bold; +} +.value { + + svg { + display: inline; + vertical-align: middle; + margin-right: $sp-1; + } + +} diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index 0b459f564..f86b6a853 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -2,6 +2,7 @@ import { FC, useMemo } from 'react' import moment from 'moment' import { CheckIcon, MinusCircleIcon } from '@heroicons/react/outline' +import { useWindowSize, WindowSize } from '~/libs/shared' import { AiWorkflowRunsResponse, useFetchAiWorkflowsRuns } from '../../hooks' import { IconAiReview } from '../../assets/icons' @@ -18,6 +19,85 @@ const AiReviewsTable: FC = props => { const aiWorkflowIds = useMemo(() => props.reviewers.map(r => r.aiWorkflowId), [props.reviewers]) const { runs, isLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(props.submissionId, aiWorkflowIds) + const windowSize: WindowSize = useWindowSize() + const isTablet = useMemo( + () => (windowSize.width ?? 0) <= 984, + [windowSize.width], + ) + + if (isTablet) { + return ( +
+ {!runs.length && isLoading && ( +
Loading...
+ )} + + {!runs.length && !isLoading && ( +
No reviews
+ )} + + {runs.map(run => ( +
+
+
Reviewer
+
+ + + + + {run.workflow.name} + +
+
+ +
+
Review Date
+
+ {run.status === 'SUCCESS' + ? moment(run.completedAt) + .local() + .format(TABLE_DATE_FORMAT) + : '-'} +
+
+ +
+
Score
+
+ {run.status === 'SUCCESS' ? run.score : '-'} +
+
+ +
+
Result
+
+ {run.status === 'SUCCESS' ? ( +
+ {run.score >= run.workflow.scorecard.minimumPassingScore ? ( + <> + + {' '} + Passed + + ) : ( + <> + + {' '} + Failed + + )} +
+ ) : ( + '-' + )} +
+
+
+ ))} +
+ ) + } + return (
diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.module.scss b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.module.scss index 15a635655..87cdbb723 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.module.scss +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.module.scss @@ -126,3 +126,13 @@ pointer-events: none; } } + +.aiReviewerRow { + @include ltelg { + tr:has(&) { + td:first-child { + display: none; + } + } + } +} diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx index 91c7cd4e4..bb388ffb0 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx @@ -343,7 +343,9 @@ export const TabContentSubmissions: FC = props => { type: 'element', }, ...(!props.aiReviewers?.length ? [] : [{ + className: styles.aiReviewerRow, label: 'Reviewer', + mobileColSpan: 2, propertyName: 'submittedDate', renderer: (submission: BackendSubmission) => ( = props => { }, { ...column, + colSpan: column.mobileColSpan, mobileType: 'last-value', }, ] as MobileTableColumn[], diff --git a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.module.scss b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.module.scss index 036c19d79..95a40e950 100644 --- a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.module.scss +++ b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.module.scss @@ -9,6 +9,12 @@ display: flex; align-items: center; gap: $sp-2; + cursor: pointer; + + @include ltelg { + justify-content: space-between; + font-weight: 600; + } svg { color: #767676; @@ -18,4 +24,13 @@ .table { margin-top: $sp-2; margin-left: -1 * $sp-4; + @include ltelg { + margin-top: 0; + margin-left: -1 * $sp-4; + margin-right: -1 * $sp-4; + } +} + +.rotated { + transform: rotate(180deg); } diff --git a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx index e21e330ea..857dcd39e 100644 --- a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx +++ b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx @@ -1,4 +1,5 @@ import { FC, useCallback, useState } from 'react' +import classNames from 'classnames' import { IconOutline } from '~/libs/ui' @@ -27,7 +28,7 @@ const CollapsibleAiReviewsRow: FC = props => { {' '} AI Reviewer {aiReviewersCount === 1 ? '' : 's'} - + {isOpen && (
diff --git a/src/libs/ui/lib/components/table/table-column.model.ts b/src/libs/ui/lib/components/table/table-column.model.ts index d0fb0d15c..a230cb27e 100644 --- a/src/libs/ui/lib/components/table/table-column.model.ts +++ b/src/libs/ui/lib/components/table/table-column.model.ts @@ -13,4 +13,5 @@ export interface TableColumn { readonly type: TableCellType readonly isSortable?: boolean readonly columnId?: string + readonly mobileColSpan?: number } From 67cd8cc29164d42803d49b6f61364b35308f1890 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 29 Oct 2025 15:15:56 +0200 Subject: [PATCH 17/30] PM-1905 - render virus scan as part of ai reviews --- .../AiReviewsTable/AiReviewsTable.module.scss | 22 +++- .../AiReviewsTable/AiReviewsTable.tsx | 109 +++++++++++++++--- .../TabContentSubmissions.tsx | 30 +---- .../CollapsibleAiReviewsRow.tsx | 7 +- .../SubmissionHistoryModal.tsx | 36 ++---- .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 16 ++- 6 files changed, 140 insertions(+), 80 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss index 445b178e6..db1611d2d 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss @@ -25,8 +25,6 @@ .scoreCol { text-align: right; - - color: #0D61BF; } } @@ -42,10 +40,12 @@ } .workflowName { - max-width: 200px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + > div:first-child { + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } } } @@ -59,6 +59,16 @@ &:global(.passed) { color: $teal-160; } + &:global(.pending) { + color: $black-20; + display: flex; + width: 16px; + height: 16px; + border-radius: 15px; + border: 1px solid; + align-items: center; + justify-content: center; + } } } diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index f86b6a853..e4b86b8b0 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -4,26 +4,65 @@ import moment from 'moment' import { CheckIcon, MinusCircleIcon } from '@heroicons/react/outline' import { useWindowSize, WindowSize } from '~/libs/shared' -import { AiWorkflowRunsResponse, useFetchAiWorkflowsRuns } from '../../hooks' +import { + AiWorkflowRun, + AiWorkflowRunsResponse, + AiWorkflowRunStatus, + useFetchAiWorkflowsRuns, + useRolePermissions, + UseRolePermissionsResult, +} from '../../hooks' import { IconAiReview } from '../../assets/icons' import { TABLE_DATE_FORMAT } from '../../../config/index.config' +import { BackendSubmission } from '../../models' import styles from './AiReviewsTable.module.scss' +import { IconOutline, Tooltip } from '~/libs/ui' +import { run } from 'node:test' interface AiReviewsTableProps { - submissionId: string + submission: Pick reviewers: { aiWorkflowId: string }[] } +const aiRunInProgress = (aiRun: Pick) => + [ + AiWorkflowRunStatus.INIT, + AiWorkflowRunStatus.QUEUED, + AiWorkflowRunStatus.DISPATCHED, + AiWorkflowRunStatus.IN_PROGRESS, + ].includes(aiRun.status) + +const aiRunFailed = (aiRun: Pick) => + [ + AiWorkflowRunStatus.FAILURE, + AiWorkflowRunStatus.CANCELLED, + ].includes(aiRun.status) + const AiReviewsTable: FC = props => { const aiWorkflowIds = useMemo(() => props.reviewers.map(r => r.aiWorkflowId), [props.reviewers]) - const { runs, isLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(props.submissionId, aiWorkflowIds) + const { runs, isLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(props.submission.id, aiWorkflowIds) const windowSize: WindowSize = useWindowSize() const isTablet = useMemo( () => (windowSize.width ?? 0) <= 984, [windowSize.width], ) + const { isAdmin }: UseRolePermissionsResult = useRolePermissions() + + const aiRuns = useMemo(() => [ + ...runs, + { + id: '-1', + completedAt: (props.submission as BackendSubmission).submittedDate, + status: AiWorkflowRunStatus.SUCCESS, + score: props.submission.virusScan === true ? 100 : 0, + workflow: { + name: 'Virus Scan', + description: '', + } + } as AiWorkflowRun + ].filter(r => isAdmin || !aiRunFailed(r)), [runs, props.submission]) if (isTablet) { return ( @@ -32,11 +71,7 @@ const AiReviewsTable: FC = props => {
Loading...
)} - {!runs.length && !isLoading && ( -
No reviews
- )} - - {runs.map(run => ( + {aiRuns.map(run => (
Reviewer
@@ -64,16 +99,20 @@ const AiReviewsTable: FC = props => {
Score
- {run.status === 'SUCCESS' ? run.score : '-'} + {run.status === 'SUCCESS' ? ( + run.workflow.scorecard ? ( + {run.score} + ) : run.score + ) : '-'}
Result
- {run.status === 'SUCCESS' ? ( + {run.status === 'SUCCESS' && (
- {run.score >= run.workflow.scorecard.minimumPassingScore ? ( + {run.score >= (run.workflow.scorecard?.minimumPassingScore ?? 0) ? ( <> {' '} @@ -87,8 +126,22 @@ const AiReviewsTable: FC = props => { )}
- ) : ( - '-' + )} + {aiRunInProgress(run) && ( +
+ + + + {' '} + To be filled +
+ )} + {aiRunFailed(run) && ( +
+ + + +
)}
@@ -114,7 +167,7 @@ const AiReviewsTable: FC = props => { )} - {runs.map(run => ( + {aiRuns.map(run => (
@@ -134,12 +189,16 @@ const AiReviewsTable: FC = props => { )} ))} diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx index bb388ffb0..c2f2acf3f 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx @@ -319,42 +319,18 @@ export const TabContentSubmissions: FC = props => { type: 'element', }, { - label: 'Virus Scan', - propertyName: 'virusScan', - renderer: (submission: BackendSubmission) => { - if (submission.virusScan === true) { - return ( - - - - ) - } - - if (submission.virusScan === false) { - return ( - - - - ) - } - - return - - }, - type: 'element', - }, - ...(!props.aiReviewers?.length ? [] : [{ className: styles.aiReviewerRow, label: 'Reviewer', mobileColSpan: 2, - propertyName: 'submittedDate', + propertyName: 'virusScan', renderer: (submission: BackendSubmission) => ( ), type: 'element', - } as TableColumn]), + }, ] if (shouldShowHistoryActions) { diff --git a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx index 857dcd39e..5d00a31c0 100644 --- a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx +++ b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx @@ -4,16 +4,17 @@ import classNames from 'classnames' import { IconOutline } from '~/libs/ui' import { AiReviewsTable } from '../AiReviewsTable' +import { BackendSubmission } from '../../models' import styles from './CollapsibleAiReviewsRow.module.scss' interface CollapsibleAiReviewsRowProps { aiReviewers: { aiWorkflowId: string }[] - submissionId: string + submission: BackendSubmission } const CollapsibleAiReviewsRow: FC = props => { - const aiReviewersCount = props.aiReviewers.length + const aiReviewersCount = props.aiReviewers.length + 1 const [isOpen, setIsOpen] = useState(false) @@ -34,7 +35,7 @@ const CollapsibleAiReviewsRow: FC = props => {
)} diff --git a/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx b/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx index 8934a50dd..9eaf6a1bd 100644 --- a/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx +++ b/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx @@ -100,6 +100,8 @@ export const SubmissionHistoryModal: FC = (props: S [props.submissions], ) + const aiReviewersCount = useMemo(() => (props.aiReviewers?.length ?? 0) + 1, [props.aiReviewers]); + const [toggledRows, setToggledRows] = useState(new Set()) const resolvedMemberInfo = useMemo(() => { @@ -256,30 +258,15 @@ export const SubmissionHistoryModal: FC = (props: S
- - {!!props.aiReviewers?.length && ( - - )} {toggledRows.has(submission.id) && ( @@ -287,7 +274,7 @@ export const SubmissionHistoryModal: FC = (props: S
@@ -321,7 +308,6 @@ export const SubmissionHistoryModal: FC = (props: S
- diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index a5849c13d..14d810b47 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -5,15 +5,27 @@ import { EnvironmentConfig } from '~/config' import { xhrGetAsync } from '~/libs/core' import { handleError } from '~/libs/shared/lib/utils/handle-error' +export enum AiWorkflowRunStatus { + INIT = 'INIT', + QUEUED = 'QUEUED', + DISPATCHED = 'DISPATCHED', + IN_PROGRESS = 'IN_PROGRESS', + CANCELLED = 'CANCELLED', + FAILURE = 'FAILURE', + COMPLETED = 'COMPLETED', + SUCCESS = 'SUCCESS', +} + export interface AiWorkflowRun { id: string; completedAt: string; - status: string; + status: AiWorkflowRunStatus; score: number; workflow: { name: string; description: string; - scorecard: { + scorecard?: { + id: string; name: string; minimumPassingScore: number; } From 4f63f3b74a639eb799df110330558e907e4e9186 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 29 Oct 2025 15:24:28 +0200 Subject: [PATCH 18/30] lint fixes --- .../AiReviewsTable/AiReviewsTable.tsx | 189 +++++++++--------- .../SubmissionHistoryModal.tsx | 2 +- 2 files changed, 96 insertions(+), 95 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index e4b86b8b0..f3000b183 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -3,6 +3,7 @@ import moment from 'moment' import { CheckIcon, MinusCircleIcon } from '@heroicons/react/outline' import { useWindowSize, WindowSize } from '~/libs/shared' +import { IconOutline, Tooltip } from '~/libs/ui' import { AiWorkflowRun, @@ -17,27 +18,23 @@ import { TABLE_DATE_FORMAT } from '../../../config/index.config' import { BackendSubmission } from '../../models' import styles from './AiReviewsTable.module.scss' -import { IconOutline, Tooltip } from '~/libs/ui' -import { run } from 'node:test' interface AiReviewsTableProps { submission: Pick reviewers: { aiWorkflowId: string }[] } -const aiRunInProgress = (aiRun: Pick) => - [ - AiWorkflowRunStatus.INIT, - AiWorkflowRunStatus.QUEUED, - AiWorkflowRunStatus.DISPATCHED, - AiWorkflowRunStatus.IN_PROGRESS, - ].includes(aiRun.status) +const aiRunInProgress = (aiRun: Pick): boolean => [ + AiWorkflowRunStatus.INIT, + AiWorkflowRunStatus.QUEUED, + AiWorkflowRunStatus.DISPATCHED, + AiWorkflowRunStatus.IN_PROGRESS, +].includes(aiRun.status) -const aiRunFailed = (aiRun: Pick) => - [ - AiWorkflowRunStatus.FAILURE, - AiWorkflowRunStatus.CANCELLED, - ].includes(aiRun.status) +const aiRunFailed = (aiRun: Pick): boolean => [ + AiWorkflowRunStatus.FAILURE, + AiWorkflowRunStatus.CANCELLED, +].includes(aiRun.status) const AiReviewsTable: FC = props => { const aiWorkflowIds = useMemo(() => props.reviewers.map(r => r.aiWorkflowId), [props.reviewers]) @@ -53,15 +50,15 @@ const AiReviewsTable: FC = props => { const aiRuns = useMemo(() => [ ...runs, { - id: '-1', completedAt: (props.submission as BackendSubmission).submittedDate, - status: AiWorkflowRunStatus.SUCCESS, + id: '-1', score: props.submission.virusScan === true ? 100 : 0, + status: AiWorkflowRunStatus.SUCCESS, workflow: { - name: 'Virus Scan', description: '', - } - } as AiWorkflowRun + name: 'Virus Scan', + }, + } as AiWorkflowRun, ].filter(r => isAdmin || !aiRunFailed(r)), [runs, props.submission]) if (isTablet) { @@ -154,86 +151,90 @@ const AiReviewsTable: FC = props => { return (
@@ -122,7 +175,9 @@ const AiReviewsTable: FC = props => { - {run.workflow.name} + + {run.workflow.name} +
- {run.status === 'SUCCESS' && run.score} + {run.status === 'SUCCESS' ? ( + run.workflow.scorecard ? ( + {run.score} + ) : run.score + ) : '-'} {run.status === 'SUCCESS' && (
- {run.score >= run.workflow.scorecard.minimumPassingScore + {run.score >= (run.workflow.scorecard?.minimumPassingScore ?? 0) ? ( <> @@ -156,6 +215,22 @@ const AiReviewsTable: FC = props => { )}
)} + {aiRunInProgress(run) && ( +
+ + + + {' '} + To be filled +
+ )} + {aiRunFailed(run) && ( +
+ + + +
+ )}
{submittedDisplay} - {resolvedVirusScan === true ? ( - - - - ) : resolvedVirusScan === false ? ( - - - - ) : ( - - - )} + + + {aiReviewersCount} + {' '} + AI Reviewer + {aiReviewersCount === 1 ? '' : 's'} + + - - {props.aiReviewers.length} - {' '} - AI Reviewer - {props.aiReviewers.length === 1 ? '' : 's'} - - -
Submission ID SubmittedVirus Scan Reviewer
- - - - - - - - {!runs.length && isLoading && ( + - + + + + - )} - - {aiRuns.map(run => ( - - - - - + + + {!runs.length && isLoading && ( + + + + )} + + {aiRuns.map(run => ( + + - - ))} + + + + + + ))} +
AI ReviewerReview DateScoreResult
Loading...AI ReviewerReview DateScoreResult
-
- - - - - - {run.workflow.name} - - -
-
- {run.status === 'SUCCESS' && ( - moment(run.completedAt) - .local() - .format(TABLE_DATE_FORMAT) - )} - - {run.status === 'SUCCESS' ? ( - run.workflow.scorecard ? ( - {run.score} - ) : run.score - ) : '-'} - - {run.status === 'SUCCESS' && ( -
- {run.score >= (run.workflow.scorecard?.minimumPassingScore ?? 0) - ? ( - <> - - {' '} - Passed - - ) - : ( - <> - - {' '} - Passed - - )} -
- )} - {aiRunInProgress(run) && ( -
- - +
Loading...
+
+ + - {' '} - To be filled -
- )} - {aiRunFailed(run) && ( -
- - + + + {run.workflow.name} +
- )} -
+ {run.status === 'SUCCESS' && ( + moment(run.completedAt) + .local() + .format(TABLE_DATE_FORMAT) + )} + + {run.status === 'SUCCESS' ? ( + run.workflow.scorecard ? ( + {run.score} + ) : run.score + ) : '-'} + + {run.status === 'SUCCESS' && ( +
+ {run.score >= (run.workflow.scorecard?.minimumPassingScore ?? 0) + ? ( + <> + + {' '} + Passed + + ) + : ( + <> + + {' '} + Passed + + )} +
+ )} + {aiRunInProgress(run) && ( +
+ + + + {' '} + To be filled +
+ )} + {aiRunFailed(run) && ( +
+ + + +
+ )} +
) diff --git a/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx b/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx index 9eaf6a1bd..eede31bf7 100644 --- a/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx +++ b/src/apps/review/src/lib/components/SubmissionHistoryModal/SubmissionHistoryModal.tsx @@ -100,7 +100,7 @@ export const SubmissionHistoryModal: FC = (props: S [props.submissions], ) - const aiReviewersCount = useMemo(() => (props.aiReviewers?.length ?? 0) + 1, [props.aiReviewers]); + const aiReviewersCount = useMemo(() => (props.aiReviewers?.length ?? 0) + 1, [props.aiReviewers]) const [toggledRows, setToggledRows] = useState(new Set()) From 9c25b0c370720903ec1b0b687eb319bc2bf38af7 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 29 Oct 2025 15:28:33 +0200 Subject: [PATCH 19/30] pr feedback --- .../review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index f3000b183..ed9c21b3e 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -210,7 +210,7 @@ const AiReviewsTable: FC = props => { <> {' '} - Passed + Failed )}
From ec25968911a16a49f7c514e6a9d8e80c149bf312 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 30 Oct 2025 09:31:14 +0200 Subject: [PATCH 20/30] Simplify routing system for review app --- src/apps/review/src/config/routes.config.ts | 2 +- .../ActiveReviewAssigments.tsx | 40 ----- .../ChallengeDetailContainer.tsx | 57 ------ .../active-review.routes.tsx | 29 +++ .../challenge-details.routes.tsx | 42 +++++ .../pages/active-review-assignements/index.ts | 2 + .../ai-scorecards/ai-scorecard.routes.tsx | 27 +++ .../review/src/pages/ai-scorecards/index.ts | 1 + .../pages/past-review-assignments/index.ts | 1 + .../past-review.routes.tsx | 28 +++ .../pages/scorecards/ScorecardsContainer.tsx | 40 ----- src/apps/review/src/pages/scorecards/index.ts | 1 + .../src/pages/scorecards/scorecard.routes.tsx | 65 +++++++ src/apps/review/src/review-app.routes.tsx | 167 +----------------- .../core/lib/router/get-routes-container.tsx | 34 ++++ src/libs/core/lib/router/index.ts | 1 + 16 files changed, 240 insertions(+), 297 deletions(-) delete mode 100644 src/apps/review/src/pages/active-review-assignements/ActiveReviewAssigments.tsx delete mode 100644 src/apps/review/src/pages/active-review-assignements/ChallengeDetailContainer.tsx create mode 100644 src/apps/review/src/pages/active-review-assignements/active-review.routes.tsx create mode 100644 src/apps/review/src/pages/active-review-assignements/challenge-details.routes.tsx create mode 100644 src/apps/review/src/pages/active-review-assignements/index.ts create mode 100644 src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx create mode 100644 src/apps/review/src/pages/ai-scorecards/index.ts create mode 100644 src/apps/review/src/pages/past-review-assignments/index.ts create mode 100644 src/apps/review/src/pages/past-review-assignments/past-review.routes.tsx delete mode 100644 src/apps/review/src/pages/scorecards/ScorecardsContainer.tsx create mode 100644 src/apps/review/src/pages/scorecards/index.ts create mode 100644 src/apps/review/src/pages/scorecards/scorecard.routes.tsx create mode 100644 src/libs/core/lib/router/get-routes-container.tsx diff --git a/src/apps/review/src/config/routes.config.ts b/src/apps/review/src/config/routes.config.ts index 2eae58a2c..a5e81f61b 100644 --- a/src/apps/review/src/config/routes.config.ts +++ b/src/apps/review/src/config/routes.config.ts @@ -12,6 +12,6 @@ export const activeReviewAssignmentsRouteId = 'active-challenges' export const openOpportunitiesRouteId = 'open-opportunities' export const pastReviewAssignmentsRouteId = 'past-challenges' export const challengeDetailRouteId = ':challengeId' -export const pastChallengeDetailContainerRouteId = 'past-challenge-details' export const scorecardRouteId = 'scorecard' +export const aiScorecardRouteId = 'ai-scorecard' export const viewScorecardRouteId = ':scorecardId' diff --git a/src/apps/review/src/pages/active-review-assignements/ActiveReviewAssigments.tsx b/src/apps/review/src/pages/active-review-assignements/ActiveReviewAssigments.tsx deleted file mode 100644 index a38aeded4..000000000 --- a/src/apps/review/src/pages/active-review-assignements/ActiveReviewAssigments.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * The router outlet. - */ - -import { FC, PropsWithChildren, useContext, useEffect, useMemo } from 'react' -import { Outlet, Routes, useLocation } from 'react-router-dom' - -import { routerContext, RouterContextData } from '~/libs/core' - -import { reviewRoutes } from '../../review-app.routes' -import { activeReviewAssignmentsRouteId } from '../../config/routes.config' - -export const ActiveReviewAssigments: FC = () => { - const location = useLocation() - const childRoutes = useChildRoutes() - - useEffect(() => { - window.scrollTo(0, 0) - }, [location.pathname]) - - return ( - <> - - {childRoutes} - - ) -} - -function useChildRoutes(): Array | undefined { - const { getRouteElement }: RouterContextData = useContext(routerContext) - const childRoutes = useMemo( - () => reviewRoutes[0].children - ?.find(r => r.id === activeReviewAssignmentsRouteId) - ?.children?.map(getRouteElement), - [getRouteElement], - ) - return childRoutes -} - -export default ActiveReviewAssigments diff --git a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailContainer.tsx b/src/apps/review/src/pages/active-review-assignements/ChallengeDetailContainer.tsx deleted file mode 100644 index 834324bf6..000000000 --- a/src/apps/review/src/pages/active-review-assignements/ChallengeDetailContainer.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/** - * The router outlet. - */ - -import { FC, useContext, useMemo } from 'react' -import { Outlet, Routes } from 'react-router-dom' - -import { routerContext, RouterContextData } from '~/libs/core' - -import { reviewRoutes } from '../../review-app.routes' -import { activeReviewAssignmentsRouteId, challengeDetailRouteId } from '../../config/routes.config' -import { ChallengeDetailContextProvider } from '../../lib' - -interface Props { - parentRouteId?: string - detailRouteId?: string -} - -export const ChallengeDetailContainer: FC = (props: Props) => { - const parentRouteId = props.parentRouteId - ?? activeReviewAssignmentsRouteId - const detailRouteId = props.detailRouteId - ?? challengeDetailRouteId - const childRoutes = useChildRoutes(parentRouteId, detailRouteId) - - return ( - - - {childRoutes} - - ) -} - -/** - * Get child routes of challenge detail page - * @returns child routes - */ -function useChildRoutes( - parentRouteId: string, - detailRouteId: string, -): Array | undefined { - const { getRouteElement }: RouterContextData = useContext(routerContext) - const childRoutes = useMemo( - () => reviewRoutes[0].children - ?.find(r => r.id === parentRouteId) - ?.children?.find(r => r.id === detailRouteId) - ?.children?.map(getRouteElement), - [ - detailRouteId, - getRouteElement, - parentRouteId, - ], - ) - return childRoutes -} - -export default ChallengeDetailContainer diff --git a/src/apps/review/src/pages/active-review-assignements/active-review.routes.tsx b/src/apps/review/src/pages/active-review-assignements/active-review.routes.tsx new file mode 100644 index 000000000..b28a89234 --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/active-review.routes.tsx @@ -0,0 +1,29 @@ +import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core'; + +import { activeReviewAssignmentsRouteId, challengeDetailRouteId } from '../../config/routes.config'; + +import { challengeDetailsRoutes } from './challenge-details.routes'; + +const ActiveReviewsPage: LazyLoadedComponent = lazyLoad( + () => import('./ActiveReviewsPage'), + 'ActiveReviewsPage', +) + +export const activeReviewChildRoutes = [ + { + authRequired: true, + element: , + id: 'active-reviews-page', + route: '', + }, + ...challengeDetailsRoutes, +]; + +export const activeReviewRoutes = [ + { + children: [ ...activeReviewChildRoutes ], + element: getRoutesContainer(activeReviewChildRoutes), + id: activeReviewAssignmentsRouteId, + route: activeReviewAssignmentsRouteId, + } +] diff --git a/src/apps/review/src/pages/active-review-assignements/challenge-details.routes.tsx b/src/apps/review/src/pages/active-review-assignements/challenge-details.routes.tsx new file mode 100644 index 000000000..fc5c0c41d --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/challenge-details.routes.tsx @@ -0,0 +1,42 @@ +import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core'; + +import { challengeDetailRouteId } from '../../config/routes.config'; +import { aiScorecardRoutes } from '../ai-scorecards'; + +const ChallengeDetailContextProvider: LazyLoadedComponent = lazyLoad( + () => import('../../lib/contexts/ChallengeDetailContextProvider'), + 'ChallengeDetailContextProvider' +) +const ScorecardDetailsPage: LazyLoadedComponent = lazyLoad( + () => import('./ScorecardDetailsPage'), + 'ScorecardDetailsPage', +) + +const ChallengeDetailsPage: LazyLoadedComponent = lazyLoad( + () => import('./ChallengeDetailsPage'), + 'ChallengeDetailsPage', +) + +export const challengeDetailsChildRoutes = [ + { + element: , + id: 'challenge-details-page', + route: 'challenge-details', + }, + { + element: , + id: 'scorecard-details-page', + route: 'review/:reviewId', + }, + ...aiScorecardRoutes, +] + +export const challengeDetailsRoutes = [ + { + authRequired: true, + children: challengeDetailsChildRoutes, + element: getRoutesContainer(challengeDetailsChildRoutes, ChallengeDetailContextProvider), + id: challengeDetailRouteId, + route: challengeDetailRouteId, + } +] diff --git a/src/apps/review/src/pages/active-review-assignements/index.ts b/src/apps/review/src/pages/active-review-assignements/index.ts new file mode 100644 index 000000000..416c8fd76 --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/index.ts @@ -0,0 +1,2 @@ +export * from './active-review.routes' +export * from './challenge-details.routes' diff --git a/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx b/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx new file mode 100644 index 000000000..247a66274 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx @@ -0,0 +1,27 @@ +import { getRoutesContainer, PlatformRoute, UserRole } from '~/libs/core' + +import { aiScorecardRouteId } from '../../config/routes.config' + +export const aiScorecardChildRoutes: ReadonlyArray = [ + { + authRequired: false, + element:
test
, + id: 'view-ai-scorecard-page', + route: ':scorecardId', + }, + +] + +// const AiScorecardsContainer = getRoutesContainer(aiScorecardChildRoutes); + +export const aiScorecardRoutes: ReadonlyArray = [ + { + children: [ ...aiScorecardChildRoutes ], + element: getRoutesContainer(aiScorecardChildRoutes), + id: aiScorecardRouteId, + rolesRequired: [ + // UserRole.administrator, + ], + route: aiScorecardRouteId, + } +] diff --git a/src/apps/review/src/pages/ai-scorecards/index.ts b/src/apps/review/src/pages/ai-scorecards/index.ts new file mode 100644 index 000000000..aec1c4968 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/index.ts @@ -0,0 +1 @@ +export * from './ai-scorecard.routes' diff --git a/src/apps/review/src/pages/past-review-assignments/index.ts b/src/apps/review/src/pages/past-review-assignments/index.ts new file mode 100644 index 000000000..a55eedde5 --- /dev/null +++ b/src/apps/review/src/pages/past-review-assignments/index.ts @@ -0,0 +1 @@ +export * from './past-review.routes' diff --git a/src/apps/review/src/pages/past-review-assignments/past-review.routes.tsx b/src/apps/review/src/pages/past-review-assignments/past-review.routes.tsx new file mode 100644 index 000000000..bb72edd4b --- /dev/null +++ b/src/apps/review/src/pages/past-review-assignments/past-review.routes.tsx @@ -0,0 +1,28 @@ +import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core'; + +import { pastReviewAssignmentsRouteId } from '../../config/routes.config'; +import { challengeDetailsRoutes } from '../active-review-assignements'; + +const PastReviewsPage: LazyLoadedComponent = lazyLoad( + () => import('./PastReviewsPage'), + 'PastReviewsPage', +) + +export const pastReviewChildRoutes = [ + { + authRequired: true, + element: , + id: 'past-reviews-page', + route: '', + }, + ...challengeDetailsRoutes, +]; + +export const pastReviewRoutes = [ + { + children: pastReviewChildRoutes, + element: getRoutesContainer(pastReviewChildRoutes), + id: pastReviewAssignmentsRouteId, + route: pastReviewAssignmentsRouteId, + } +] diff --git a/src/apps/review/src/pages/scorecards/ScorecardsContainer.tsx b/src/apps/review/src/pages/scorecards/ScorecardsContainer.tsx deleted file mode 100644 index d6a15b92c..000000000 --- a/src/apps/review/src/pages/scorecards/ScorecardsContainer.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * The router outlet. - */ - -import { FC, PropsWithChildren, useContext, useEffect, useMemo } from 'react' -import { Outlet, Routes, useLocation } from 'react-router-dom' - -import { routerContext, RouterContextData } from '~/libs/core' - -import { reviewRoutes } from '../../review-app.routes' -import { scorecardRouteId } from '../../config/routes.config' - -export const ScorecardsContainer: FC = () => { - const location = useLocation() - const childRoutes = useChildRoutes() - - useEffect(() => { - window.scrollTo(0, 0) - }, [location.pathname]) - - return ( - <> - - {childRoutes} - - ) -} - -function useChildRoutes(): Array | undefined { - const { getRouteElement }: RouterContextData = useContext(routerContext) - const childRoutes = useMemo( - () => reviewRoutes[0].children - ?.find(r => r.id === scorecardRouteId) - ?.children?.map(getRouteElement), - [getRouteElement], - ) - return childRoutes -} - -export default ScorecardsContainer diff --git a/src/apps/review/src/pages/scorecards/index.ts b/src/apps/review/src/pages/scorecards/index.ts new file mode 100644 index 000000000..99d2b3f86 --- /dev/null +++ b/src/apps/review/src/pages/scorecards/index.ts @@ -0,0 +1 @@ +export * from './scorecard.routes' diff --git a/src/apps/review/src/pages/scorecards/scorecard.routes.tsx b/src/apps/review/src/pages/scorecards/scorecard.routes.tsx new file mode 100644 index 000000000..a5ca54964 --- /dev/null +++ b/src/apps/review/src/pages/scorecards/scorecard.routes.tsx @@ -0,0 +1,65 @@ +import { getRoutesContainer, lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/core' +import { scorecardRouteId } from '../../config/routes.config' + +const ScorecardsListPage: LazyLoadedComponent = lazyLoad( + () => import('./ScorecardsListPage'), + 'ScorecardsListPage', +) + +const ViewScorecardPage: LazyLoadedComponent = lazyLoad( + () => import('./ViewScorecardPage'), + 'ViewScorecardPage', +) +const EditScorecardPage: LazyLoadedComponent = lazyLoad( + () => import('./EditScorecardPage'), + 'EditScorecardPage', +) + +export const scorecardChildRoutes: ReadonlyArray = [ + { + authRequired: true, + element: , + id: 'list-scorecards-page', + rolesRequired: [UserRole.administrator], + route: '', + }, + { + authRequired: true, + element: , + id: 'edit-scorecard-page', + rolesRequired: [ + UserRole.administrator, + ], + route: ':scorecardId/edit', + }, + { + authRequired: true, + element: , + id: 'new-scorecard-page', + rolesRequired: [ + UserRole.administrator, + ], + route: 'new', + }, + { + authRequired: false, + element: , + id: 'view-scorecard-page', + route: ':scorecardId', + }, + +] + +// const ScorecardsContainer = getRoutesContainer(scorecardChildRoutes) + +export const scorecardRoutes: ReadonlyArray = [ + { + children: [...scorecardChildRoutes], + element: getRoutesContainer(scorecardChildRoutes), + id: scorecardRouteId, + rolesRequired: [ + UserRole.administrator, + ], + route: scorecardRouteId, + } +] diff --git a/src/apps/review/src/review-app.routes.tsx b/src/apps/review/src/review-app.routes.tsx index cd087d5a9..160c2d144 100644 --- a/src/apps/review/src/review-app.routes.tsx +++ b/src/apps/review/src/review-app.routes.tsx @@ -7,65 +7,19 @@ import { LazyLoadedComponent, PlatformRoute, Rewrite, - UserRole, } from '~/libs/core' import { activeReviewAssignmentsRouteId, - challengeDetailRouteId, - pastChallengeDetailContainerRouteId, - pastReviewAssignmentsRouteId, rootRoute, - scorecardRouteId, } from './config/routes.config' +import { scorecardRoutes } from './pages/scorecards' +import { aiScorecardRoutes } from './pages/ai-scorecards' +import { activeReviewRoutes } from './pages/active-review-assignements' +import { pastReviewRoutes } from './pages/past-review-assignments' const ReviewApp: LazyLoadedComponent = lazyLoad(() => import('./ReviewApp')) -const ActiveReviewAssigments: LazyLoadedComponent = lazyLoad( - () => import('./pages/active-review-assignements/ActiveReviewAssigments'), -) -const ChallengeDetailContainer: LazyLoadedComponent = lazyLoad( - () => import('./pages/active-review-assignements/ChallengeDetailContainer'), -) -const ActiveReviewsPage: LazyLoadedComponent = lazyLoad( - () => import('./pages/active-review-assignements/ActiveReviewsPage'), - 'ActiveReviewsPage', -) -const PastReviewAssignments: LazyLoadedComponent = lazyLoad( - () => import('./pages/past-review-assignments/PastReviewAssignments'), -) -const PastReviewsPage: LazyLoadedComponent = lazyLoad( - () => import('./pages/past-review-assignments/PastReviewsPage'), - 'PastReviewsPage', -) -const ChallengeDetailsPage: LazyLoadedComponent = lazyLoad( - () => import('./pages/active-review-assignements/ChallengeDetailsPage'), - 'ChallengeDetailsPage', -) -const ScorecardDetailsPage: LazyLoadedComponent = lazyLoad( - () => import('./pages/active-review-assignements/ScorecardDetailsPage'), - 'ScorecardDetailsPage', -) - -const ScorecardsContainer: LazyLoadedComponent = lazyLoad( - () => import('./pages/scorecards/ScorecardsContainer'), - 'ScorecardsContainer', -) - -const ScorecardsListPage: LazyLoadedComponent = lazyLoad( - () => import('./pages/scorecards/ScorecardsListPage'), - 'ScorecardsListPage', -) - -const ViewScorecardPage: LazyLoadedComponent = lazyLoad( - () => import('./pages/scorecards/ViewScorecardPage'), - 'ViewScorecardPage', -) -const EditScorecardPage: LazyLoadedComponent = lazyLoad( - () => import('./pages/scorecards/EditScorecardPage'), - 'EditScorecardPage', -) - const activeChallengeDetailsRewriteTarget: string = `${rootRoute || ''}/${activeReviewAssignmentsRouteId}/:challengeId/challenge-details` @@ -103,116 +57,11 @@ export const reviewRoutes: ReadonlyArray = [ route: ':challengeId', }, // Active Challenges Module - { - children: [ - { - authRequired: true, - element: , - id: 'active-reviews-page', - route: '', - }, - { - authRequired: true, - children: [ - { - element: , - id: 'challenge-details-page', - route: 'challenge-details', - }, - { - element: , - id: 'scorecard-details-page', - route: 'review/:reviewId', - }, - ], - element: , - id: challengeDetailRouteId, - route: challengeDetailRouteId, - }, - ], - element: , - id: activeReviewAssignmentsRouteId, - route: activeReviewAssignmentsRouteId, - }, + ...activeReviewRoutes, // Past Challenges Module - { - children: [ - { - authRequired: true, - element: , - id: 'past-reviews-page', - route: '', - }, - { - authRequired: true, - children: [ - { - element: , - id: 'past-challenge-details-page', - route: 'challenge-details', - }, - { - element: , - id: 'past-scorecard-details-page', - route: 'review/:reviewId', - }, - ], - element: ( - - ), - id: pastChallengeDetailContainerRouteId, - route: challengeDetailRouteId, - }, - ], - element: , - id: pastReviewAssignmentsRouteId, - route: pastReviewAssignmentsRouteId, - }, - { - children: [ - { - authRequired: true, - element: , - id: 'list-scorecards-page', - rolesRequired: [UserRole.administrator], - route: '', - }, - { - authRequired: true, - element: , - id: 'edit-scorecard-page', - rolesRequired: [ - UserRole.administrator, - ], - route: ':scorecardId/edit', - }, - { - authRequired: true, - element: , - id: 'new-scorecard-page', - rolesRequired: [ - UserRole.administrator, - ], - route: 'new', - }, - { - authRequired: false, - element: , - id: 'view-scorecard-page', - route: ':scorecardId', - }, - - ], - element: , - id: scorecardRouteId, - rolesRequired: [ - UserRole.administrator, - ], - route: scorecardRouteId, - }, + ...pastReviewRoutes, + ...scorecardRoutes, + ...aiScorecardRoutes, ], domain: AppSubdomain.review, element: , diff --git a/src/libs/core/lib/router/get-routes-container.tsx b/src/libs/core/lib/router/get-routes-container.tsx new file mode 100644 index 000000000..23b60d17c --- /dev/null +++ b/src/libs/core/lib/router/get-routes-container.tsx @@ -0,0 +1,34 @@ +/** + * The router outlet. + */ + +import { FC, Fragment, PropsWithChildren, useContext, useEffect, useMemo } from 'react' +import { Outlet, Routes, useLocation } from 'react-router-dom' + +import { PlatformRoute } from './platform-route.model' +import { routerContext, RouterContextData } from './router-context' + +export function getRoutesContainer(childRoutes: ReadonlyArray, contextContainer?: FC): JSX.Element { + const ContextContainer = contextContainer ?? Fragment + const Container = () => { + const location = useLocation() + const { getRouteElement }: RouterContextData = useContext(routerContext) + const childRoutesWithContext = useMemo( + () => childRoutes.map(getRouteElement), + [getRouteElement], + ) + + useEffect(() => { + window.scrollTo(0, 0) + }, [location.pathname]) + + return ( + + + {childRoutesWithContext} + + ) + } + + return +} diff --git a/src/libs/core/lib/router/index.ts b/src/libs/core/lib/router/index.ts index f5ccd7055..e4fc30157 100644 --- a/src/libs/core/lib/router/index.ts +++ b/src/libs/core/lib/router/index.ts @@ -1,3 +1,4 @@ +export * from './get-routes-container' export * from './router-context' export * from './routes-functions' export * from './platform-route.model' From 6abe3215580ce7289aff26d28e786fa969811690 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 30 Oct 2025 13:41:15 +0200 Subject: [PATCH 21/30] PM-2136 - ai workflow runs sidebar switcher --- .../AiReviewsTable/AiReviewsTable.module.scss | 23 ---- .../AiReviewsTable/AiReviewsTable.tsx | 94 ++------------- .../AiWorkflowRunStatus.module.scss | 39 ++++++ .../AiReviewsTable/AiWorkflowRunStatus.tsx | 43 +++++++ .../AiReviewsTable/StatusLabel.module.scss | 39 ++++++ .../components/AiReviewsTable/StatusLabel.tsx | 31 +++++ .../lib/components/AiReviewsTable/index.ts | 1 + .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 65 ++++------ .../lib/models/AiScorecardContext.model.ts | 13 ++ src/apps/review/src/lib/models/index.ts | 1 + .../AiScorecardContextProvider.tsx | 71 +++++++++++ .../ai-scorecards/AiScorecardContext/index.ts | 1 + .../AiScorecardViewer.module.scss | 18 +++ .../AiScorecardViewer/AiScorecardViewer.tsx | 49 ++++++++ .../ai-scorecards/AiScorecardViewer/index.ts | 1 + .../ai-scorecards/ai-scorecard.routes.tsx | 14 ++- .../AiWorkflowsSidebar.module.scss | 112 ++++++++++++++++++ .../AiWorkflowsSidebar/AiWorkflowsSidebar.tsx | 57 +++++++++ .../components/AiWorkflowsSidebar/index.ts | 1 + .../ScorecardHeader.module.scss | 0 .../ScorecardHeader/ScorecardHeader.tsx | 20 ++++ .../components/ScorecardHeader/index.ts | 1 + .../pages/ai-scorecards/components/index.ts | 1 + src/apps/review/src/review-app.routes.tsx | 1 - 24 files changed, 540 insertions(+), 156 deletions(-) create mode 100644 src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.module.scss create mode 100644 src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx create mode 100644 src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.module.scss create mode 100644 src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.tsx create mode 100644 src/apps/review/src/lib/models/AiScorecardContext.model.ts create mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx create mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardContext/index.ts create mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss create mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx create mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/index.ts create mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss create mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx create mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/index.ts create mode 100644 src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss create mode 100644 src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx create mode 100644 src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/index.ts create mode 100644 src/apps/review/src/pages/ai-scorecards/components/index.ts diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss index db1611d2d..06fa68e1a 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss @@ -49,29 +49,6 @@ } } -.result { - display: flex; - align-items: center; - gap: $sp-2; - - :global(.icon) { - color: #C1294F; - &:global(.passed) { - color: $teal-160; - } - &:global(.pending) { - color: $black-20; - display: flex; - width: 16px; - height: 16px; - border-radius: 15px; - border: 1px solid; - align-items: center; - justify-content: center; - } - } -} - .mobileCard { border-top: 1px solid #A8A8A8; margin-top: $sp-2; diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index ed9c21b3e..e85c0afe8 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -1,22 +1,20 @@ import { FC, useMemo } from 'react' import moment from 'moment' -import { CheckIcon, MinusCircleIcon } from '@heroicons/react/outline' import { useWindowSize, WindowSize } from '~/libs/shared' -import { IconOutline, Tooltip } from '~/libs/ui' +import { Tooltip } from '~/libs/ui' import { AiWorkflowRun, AiWorkflowRunsResponse, - AiWorkflowRunStatus, + AiWorkflowRunStatusEnum, useFetchAiWorkflowsRuns, - useRolePermissions, - UseRolePermissionsResult, } from '../../hooks' import { IconAiReview } from '../../assets/icons' import { TABLE_DATE_FORMAT } from '../../../config/index.config' import { BackendSubmission } from '../../models' +import { AiWorkflowRunStatus } from './AiWorkflowRunStatus' import styles from './AiReviewsTable.module.scss' interface AiReviewsTableProps { @@ -24,17 +22,6 @@ interface AiReviewsTableProps { reviewers: { aiWorkflowId: string }[] } -const aiRunInProgress = (aiRun: Pick): boolean => [ - AiWorkflowRunStatus.INIT, - AiWorkflowRunStatus.QUEUED, - AiWorkflowRunStatus.DISPATCHED, - AiWorkflowRunStatus.IN_PROGRESS, -].includes(aiRun.status) - -const aiRunFailed = (aiRun: Pick): boolean => [ - AiWorkflowRunStatus.FAILURE, - AiWorkflowRunStatus.CANCELLED, -].includes(aiRun.status) const AiReviewsTable: FC = props => { const aiWorkflowIds = useMemo(() => props.reviewers.map(r => r.aiWorkflowId), [props.reviewers]) @@ -45,7 +32,6 @@ const AiReviewsTable: FC = props => { () => (windowSize.width ?? 0) <= 984, [windowSize.width], ) - const { isAdmin }: UseRolePermissionsResult = useRolePermissions() const aiRuns = useMemo(() => [ ...runs, @@ -53,13 +39,13 @@ const AiReviewsTable: FC = props => { completedAt: (props.submission as BackendSubmission).submittedDate, id: '-1', score: props.submission.virusScan === true ? 100 : 0, - status: AiWorkflowRunStatus.SUCCESS, + status: AiWorkflowRunStatusEnum.SUCCESS, workflow: { description: '', name: 'Virus Scan', }, } as AiWorkflowRun, - ].filter(r => isAdmin || !aiRunFailed(r)), [runs, props.submission]) + ], [runs, props.submission]) if (isTablet) { return ( @@ -107,39 +93,7 @@ const AiReviewsTable: FC = props => {
Result
- {run.status === 'SUCCESS' && ( -
- {run.score >= (run.workflow.scorecard?.minimumPassingScore ?? 0) ? ( - <> - - {' '} - Passed - - ) : ( - <> - - {' '} - Failed - - )} -
- )} - {aiRunInProgress(run) && ( -
- - - - {' '} - To be filled -
- )} - {aiRunFailed(run) && ( -
- - - -
- )} +
@@ -196,41 +150,7 @@ const AiReviewsTable: FC = props => { ) : '-'} - {run.status === 'SUCCESS' && ( -
- {run.score >= (run.workflow.scorecard?.minimumPassingScore ?? 0) - ? ( - <> - - {' '} - Passed - - ) - : ( - <> - - {' '} - Failed - - )} -
- )} - {aiRunInProgress(run) && ( -
- - - - {' '} - To be filled -
- )} - {aiRunFailed(run) && ( -
- - - -
- )} + ))} diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.module.scss b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.module.scss new file mode 100644 index 000000000..b99473257 --- /dev/null +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.module.scss @@ -0,0 +1,39 @@ +@import '@libs/ui/styles/includes'; + +.result { + display: flex; + align-items: center; + gap: $sp-2; +} + +.icon { + display: flex; + width: 20px; + height: 20px; + border-radius: 10px; + border: 1px solid #e9ecef; + background: #fff; + align-items: center; + justify-content: center; + + &.failed, + &.failed-score { + color: #C1294F; + } + + &.passed { + color: $teal-160; + } + + &.pending { + color: $black-20; + border-color: $black-20; + } +} + +.score { + font-size: 14px; + .failed-score { + color: #C1294F; + } +} diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx new file mode 100644 index 000000000..3b33a3717 --- /dev/null +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx @@ -0,0 +1,43 @@ +import { FC, ReactNode, useCallback, useMemo } from 'react' +import classNames from 'classnames' + +import { IconOutline } from '~/libs/ui' + +import { aiRunFailed, aiRunInProgress, AiWorkflowRun, AiWorkflowRunStatusEnum } from '../../hooks' + +import StatusLabel from './StatusLabel' + +interface AiWorkflowRunStatusProps { + run: Pick + hideLabel?: boolean + showScore?: boolean +} + +export const AiWorkflowRunStatus: FC = props => { + const isInProgress = useMemo(() => aiRunInProgress(props.run), [props.run.status]) + const isFailed = useMemo(() => aiRunFailed(props.run), [props.run.status]) + const isPassing = props.run.status === 'SUCCESS' && props.run.score >= (props.run.workflow.scorecard?.minimumPassingScore ?? 0) + const status = isInProgress ? 'pending' : isFailed ? 'failed' : ( + isPassing ? 'passed' : 'failed-score' + ); + + const score = props.showScore ? props.run.score : undefined; + + return ( + <> + {props.run.status === 'SUCCESS' && isPassing && ( + } hideLabel={props.hideLabel} label='Passed' status={status} score={score} /> + )} + {props.run.status === 'SUCCESS' && !isPassing && ( + } hideLabel={props.hideLabel} label='Failed' status={status} score={score} /> + )} + {isInProgress && ( + } hideLabel={props.hideLabel} label='To be filled' status={status} score={score} /> + )} + {isFailed && ( + } status={status} score={score} /> + )} + + ) +} + diff --git a/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.module.scss b/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.module.scss new file mode 100644 index 000000000..1c89769cd --- /dev/null +++ b/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.module.scss @@ -0,0 +1,39 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + display: flex; + align-items: center; + gap: $sp-2; +} + +.icon { + display: flex; + width: 20px; + height: 20px; + border-radius: 10px; + border: 1px solid #e9ecef; + background: #fff; + align-items: center; + justify-content: center; + + &.failed, + &.failed-score { + color: #C1294F; + } + + &.passed { + color: $teal-160; + } + + &.pending { + color: #e9ecef; + border-color: #e9ecef; + } +} + +.score { + font-size: 14px; + .failed-score { + color: #C1294F; + } +} diff --git a/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.tsx b/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.tsx new file mode 100644 index 000000000..39e6389c3 --- /dev/null +++ b/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.tsx @@ -0,0 +1,31 @@ +import { FC, ReactNode } from 'react' + +import styles from './StatusLabel.module.scss' +import classNames from 'classnames' + +interface StatusLabelProps { + icon: ReactNode + hideLabel?: boolean + label?: string + score?: number + status: 'pending' | 'failed' | 'passed' | 'failed-score' +} + +const StatusLabel: FC = props => { + + return ( +
+ {props.score && ( + {props.score} + )} + {props.icon && ( + + {props.icon} + + )} + {!props.hideLabel && props.label} +
+ ) +} + +export default StatusLabel diff --git a/src/apps/review/src/lib/components/AiReviewsTable/index.ts b/src/apps/review/src/lib/components/AiReviewsTable/index.ts index 2a13d8a91..9e371fd40 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/index.ts +++ b/src/apps/review/src/lib/components/AiReviewsTable/index.ts @@ -1 +1,2 @@ export { default as AiReviewsTable } from './AiReviewsTable' +export * from './AiWorkflowRunStatus' diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 14d810b47..6b1acb130 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -5,7 +5,10 @@ import { EnvironmentConfig } from '~/config' import { xhrGetAsync } from '~/libs/core' import { handleError } from '~/libs/shared/lib/utils/handle-error' -export enum AiWorkflowRunStatus { +import { useRolePermissions, UseRolePermissionsResult } from './useRolePermissions' +import { Scorecard } from '../models' + +export enum AiWorkflowRunStatusEnum { INIT = 'INIT', QUEUED = 'QUEUED', DISPATCHED = 'DISPATCHED', @@ -16,20 +19,19 @@ export enum AiWorkflowRunStatus { SUCCESS = 'SUCCESS', } +export interface AiWorkflow { + id: string; + name: string; + description: string; + scorecard?: Scorecard +} + export interface AiWorkflowRun { id: string; completedAt: string; - status: AiWorkflowRunStatus; + status: AiWorkflowRunStatusEnum; score: number; - workflow: { - name: string; - description: string; - scorecard?: { - id: string; - name: string; - minimumPassingScore: number; - } - } + workflow: AiWorkflow } const TC_API_BASE_URL = EnvironmentConfig.API.V6 @@ -39,39 +41,24 @@ export interface AiWorkflowRunsResponse { isLoading: boolean } -export function useFetchAiWorkflowRuns( - workflowId: string, - submissionId: string, -): AiWorkflowRunsResponse { - // Use swr hooks for challenge info fetching - const { - data: runs = [], - error: fetchError, - isValidating: isLoading, - }: SWRResponse = useSWR( - `${TC_API_BASE_URL}/workflows/${workflowId}/runs?submissionId=${submissionId}`, - { - isPaused: () => !workflowId || !submissionId, - }, - ) - - // Show backend error when fetching challenge info - useEffect(() => { - if (fetchError) { - handleError(fetchError) - } - }, [fetchError]) +export const aiRunInProgress = (aiRun: Pick): boolean => [ + AiWorkflowRunStatusEnum.INIT, + AiWorkflowRunStatusEnum.QUEUED, + AiWorkflowRunStatusEnum.DISPATCHED, + AiWorkflowRunStatusEnum.IN_PROGRESS, +].includes(aiRun.status) - return { - isLoading, - runs, - } -} +export const aiRunFailed = (aiRun: Pick): boolean => [ + AiWorkflowRunStatusEnum.FAILURE, + AiWorkflowRunStatusEnum.CANCELLED, +].includes(aiRun.status) export function useFetchAiWorkflowsRuns( submissionId: string, workflowIds: string[], ): AiWorkflowRunsResponse { + const { isAdmin }: UseRolePermissionsResult = useRolePermissions() + // Use swr hooks for challenge info fetching const { data: runs = [], @@ -101,6 +88,6 @@ export function useFetchAiWorkflowsRuns( return { isLoading, - runs, + runs: runs.filter(r => isAdmin || !aiRunFailed(r)), } } diff --git a/src/apps/review/src/lib/models/AiScorecardContext.model.ts b/src/apps/review/src/lib/models/AiScorecardContext.model.ts new file mode 100644 index 000000000..45d6142f6 --- /dev/null +++ b/src/apps/review/src/lib/models/AiScorecardContext.model.ts @@ -0,0 +1,13 @@ +import { AiWorkflow, AiWorkflowRun } from '../hooks' + +import { Scorecard } from './Scorecard.model' +import { ChallengeDetailContextModel } from './ChallengeDetailContextModel.model' + +export interface AiScorecardContextModel extends ChallengeDetailContextModel { + isLoading: boolean + submissionId: string + workflowId: string + workflow?: AiWorkflow + scorecard?: Scorecard + workflowRuns: AiWorkflowRun[] +} diff --git a/src/apps/review/src/lib/models/index.ts b/src/apps/review/src/lib/models/index.ts index 2f62d228e..ff5ead5a7 100644 --- a/src/apps/review/src/lib/models/index.ts +++ b/src/apps/review/src/lib/models/index.ts @@ -1,3 +1,4 @@ +export * from './AiScorecardContext.model' export * from './ChallengeInfo.model' export * from './SubmissionInfo.model' export * from './ReviewInfo.model' diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx new file mode 100644 index 000000000..822d5a7f0 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx @@ -0,0 +1,71 @@ +/** + * Context provider for challenge detail page + */ +import { Context, createContext, FC, PropsWithChildren, useContext, useMemo } from 'react' +import { useParams } from 'react-router-dom' + +import { ChallengeDetailContext } from '../../../lib' +import { AiScorecardContextModel, ChallengeDetailContextModel } from '../../../lib/models' + +import { AiWorkflowRunsResponse, useFetchAiWorkflowsRuns } from '../../../lib/hooks' + +export const AiScorecardContext: Context + = createContext({} as AiScorecardContextModel) + +export const AiScorecardContextProvider: FC = props => { + const { workflowId = '', submissionId = '' }: { + submissionId?: string, + workflowId?: string, + } = useParams<{ + submissionId: string, + workflowId: string, + }>() + + const challengeDetailsCtx = useContext(ChallengeDetailContext) + const { challengeInfo }: ChallengeDetailContextModel = challengeDetailsCtx; + const aiReviewers = useMemo(() => (challengeInfo?.reviewers ?? []).filter(r => !!r.aiWorkflowId), [challengeInfo?.reviewers]) + const aiWorkflowIds = useMemo(() => aiReviewers?.map(r => r.aiWorkflowId as string), [aiReviewers]) + + const { runs: workflowRuns, isLoading: aiWorkflowRunsLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(submissionId, aiWorkflowIds) + + const isLoadingCtxData = + challengeDetailsCtx.isLoadingChallengeInfo && + challengeDetailsCtx.isLoadingChallengeResources && + challengeDetailsCtx.isLoadingChallengeSubmissions && + aiWorkflowRunsLoading + + const workflow = useMemo(() => ( + workflowRuns.map(r => r.workflow).find(w => w.id === workflowId) + ), [workflowRuns, workflowId]) + + const scorecard = useMemo(() => workflow?.scorecard, [workflow]) + + const value = useMemo( + () => ({ + ...challengeDetailsCtx, + submissionId, + workflowId, + workflowRuns, + workflow, + scorecard, + isLoading: isLoadingCtxData, + }), + [ + challengeDetailsCtx, + submissionId, + workflowId, + workflowRuns, + isLoadingCtxData, + workflow, + scorecard, + ], + ) + + return ( + + {props.children} + + ) +} + +export const useAiScorecardContext = () => useContext(AiScorecardContext) diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/index.ts b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/index.ts new file mode 100644 index 000000000..03eae189c --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/index.ts @@ -0,0 +1 @@ +export * from './AiScorecardContextProvider' diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss new file mode 100644 index 000000000..e65bd326f --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss @@ -0,0 +1,18 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; +} + +.contentWrap { + display: flex; + flex-direction: row; + gap: $sp-10; + + > .sidebar { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 256px; + } +} diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx new file mode 100644 index 000000000..e05cbab37 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx @@ -0,0 +1,49 @@ +import { FC, useEffect, useMemo } from 'react' + +import styles from './AiScorecardViewer.module.scss' +import { ScorecardHeader } from '../components/ScorecardHeader' +import { NotificationContextType, useNotification } from '~/libs/shared' +import { IconAiReview } from '../../../lib/assets/icons' +import { PageWrapper } from '../../../lib' +import { useAiScorecardContext } from '../AiScorecardContext' +import { AiScorecardContextModel } from '../../../lib/models' +import { AiWorkflowsSidebar } from '../components/AiWorkflowsSidebar' + +interface AiScorecardViewerProps { +} + +const AiScorecardViewer: FC = props => { + + const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() + const { challengeInfo, workflowRuns }: AiScorecardContextModel = useAiScorecardContext() + + const breadCrumb = useMemo( + () => [{ index: 1, label: 'My Active Challenges' }], + [], + ) + + useEffect(() => { + const notification = showBannerNotification({ + icon: , + id: 'ai-review-icon-notification', + message: `Challenges with this icon indicate that + one or more AI reviews will be conducted for each member submission.`, + }) + return () => notification && removeNotification(notification.id) + }, [showBannerNotification, removeNotification]) + + return ( + +
+ + +
+
+ ) +} + +export default AiScorecardViewer diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/index.ts b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/index.ts new file mode 100644 index 000000000..bc2998589 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/index.ts @@ -0,0 +1 @@ +export { default as AiScorecardViewer } from './AiScorecardViewer' diff --git a/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx b/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx index 247a66274..1030df850 100644 --- a/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx +++ b/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx @@ -1,15 +1,17 @@ -import { getRoutesContainer, PlatformRoute, UserRole } from '~/libs/core' +import { getRoutesContainer, lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/core' import { aiScorecardRouteId } from '../../config/routes.config' +const AiScorecardViewer: LazyLoadedComponent = lazyLoad(() => import('./AiScorecardViewer'), 'AiScorecardViewer') +const AiScorecardContextProvider: LazyLoadedComponent = lazyLoad(() => import('./AiScorecardContext'), 'AiScorecardContextProvider') + export const aiScorecardChildRoutes: ReadonlyArray = [ { authRequired: false, - element:
test
, + element: , id: 'view-ai-scorecard-page', - route: ':scorecardId', + route: '', }, - ] // const AiScorecardsContainer = getRoutesContainer(aiScorecardChildRoutes); @@ -17,11 +19,11 @@ export const aiScorecardChildRoutes: ReadonlyArray = [ export const aiScorecardRoutes: ReadonlyArray = [ { children: [ ...aiScorecardChildRoutes ], - element: getRoutesContainer(aiScorecardChildRoutes), + element: getRoutesContainer(aiScorecardChildRoutes, AiScorecardContextProvider), id: aiScorecardRouteId, rolesRequired: [ // UserRole.administrator, ], - route: aiScorecardRouteId, + route: `${aiScorecardRouteId}/:submissionId/:workflowId`, } ] diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss new file mode 100644 index 000000000..e55156475 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss @@ -0,0 +1,112 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + +} + +.runsWrap { + > ul { + padding: 0; + margin: 0; + list-style: none; + + li { + position: relative; + margin-top: $sp-1; + &:first-child { + margin-top: 0; + } + + > a { + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + opacity: 0; + } + } + } +} + +.runEntry { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + padding: $sp-4; + background-color: #f9fafa; + cursor: pointer; + + font-family: "Nunito Sans", sans-serif; + font-size: 16px; + line-height: 22px; + + &.active { + background-color: #E9ECEF; + .workflowName{ + font-weight: bold; + } + &:before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background-color: $teal-160; + } + } + + &:hover { + background-color: lighten(#E9ECEF, 2.5%); + } + + &:active:hover { + background-color: darken(#E9ECEF, 5%); + } + + > span { + display: flex; + flex-direction: row; + align-items: center; + gap: $sp-2; + } +} + +.workflowName { + max-width: 139px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.legend { + background-color: #f9fafa; + padding: $sp-4; + margin-top: $sp-4; + + + font-family: "Nunito Sans", sans-serif; + font-size: 16px; + line-height: 22px; + color: #0A0A0A; + + &Label { + font-weight: bold; + } + + ul { + padding: 0; + margin: $sp-2 0 0; + list-style: none; + + li { + margin-top: $sp-1; + &:first-child { + margin-top: 0; + } + } + } +} diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx new file mode 100644 index 000000000..e1f38ac91 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx @@ -0,0 +1,57 @@ +import { FC, useMemo } from 'react' + +import styles from './AiWorkflowsSidebar.module.scss' +import { useAiScorecardContext } from '../../AiScorecardContext' +import { AiScorecardContextModel } from '~/apps/review/src/lib/models' +import { AiWorkflowRunStatus } from '~/apps/review/src/lib/components/AiReviewsTable' +import classNames from 'classnames' +import { IconAiReview } from '~/apps/review/src/lib/assets/icons' +import StatusLabel from '~/apps/review/src/lib/components/AiReviewsTable/StatusLabel' +import { IconOutline } from '~/libs/ui' +import { Link } from 'react-router-dom' + +interface AiWorkflowsSidebarProps { + className?: string +} + +const AiWorkflowsSidebar: FC = props => { + const { workflowRuns, workflowId, submissionId }: AiScorecardContextModel = useAiScorecardContext() + + return ( +
+
+
    + {workflowRuns.map(workflowRun => ( +
  • + + + + {workflowRun.workflow.name} + + +
  • + ))} +
+
+ +
+
+ Legend +
+
    +
  • + } label='Passed' status="passed" /> +
  • +
  • + } label='Failed' status="failed" /> +
  • +
  • + } label='To be filled' status="pending" /> +
  • +
+
+
+ ) +} + +export default AiWorkflowsSidebar diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/index.ts b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/index.ts new file mode 100644 index 000000000..1c0991661 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/index.ts @@ -0,0 +1 @@ +export { default as AiWorkflowsSidebar } from './AiWorkflowsSidebar' diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx new file mode 100644 index 000000000..3e6647e95 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx @@ -0,0 +1,20 @@ +import { FC } from 'react' + +import styles from './ScorecardHeader.module.scss' +import { useAiScorecardContext } from '../../AiScorecardContext' +import { AiScorecardContextModel } from '~/apps/review/src/lib/models' + +interface ScorecardHeaderProps { +} + +const ScorecardHeader: FC = props => { + const { workflow, scorecard }: AiScorecardContextModel = useAiScorecardContext() + + return ( +
+ {workflow?.name} +
+ ) +} + +export default ScorecardHeader diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/index.ts b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/index.ts new file mode 100644 index 000000000..5cafe1a64 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/index.ts @@ -0,0 +1 @@ +export { default as ScorecardHeader } from './ScorecardHeader' diff --git a/src/apps/review/src/pages/ai-scorecards/components/index.ts b/src/apps/review/src/pages/ai-scorecards/components/index.ts new file mode 100644 index 000000000..3bbdff305 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/index.ts @@ -0,0 +1 @@ +export * from './AiWorkflowsSidebar' diff --git a/src/apps/review/src/review-app.routes.tsx b/src/apps/review/src/review-app.routes.tsx index 160c2d144..0d4990756 100644 --- a/src/apps/review/src/review-app.routes.tsx +++ b/src/apps/review/src/review-app.routes.tsx @@ -61,7 +61,6 @@ export const reviewRoutes: ReadonlyArray = [ // Past Challenges Module ...pastReviewRoutes, ...scorecardRoutes, - ...aiScorecardRoutes, ], domain: AppSubdomain.review, element: , From 5dbb05d4b7bd72da095e5f0e7f3779b547d1dbae Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 30 Oct 2025 17:34:23 +0200 Subject: [PATCH 22/30] PM-2136 - AI workflows sidebar - mobile view --- .../AiReviewsTable/StatusLabel.module.scss | 2 +- .../lib/models/AiScorecardContext.model.ts | 1 + .../AiScorecardContextProvider.tsx | 8 +- .../AiScorecardViewer.module.scss | 10 ++ .../AiWorkflowsSidebar.module.scss | 91 ++++++++++++++++++- .../AiWorkflowsSidebar/AiWorkflowsSidebar.tsx | 91 ++++++++++++------- 6 files changed, 166 insertions(+), 37 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.module.scss b/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.module.scss index 1c89769cd..1857d285f 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.module.scss +++ b/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.module.scss @@ -33,7 +33,7 @@ .score { font-size: 14px; - .failed-score { + &.failed-score { color: #C1294F; } } diff --git a/src/apps/review/src/lib/models/AiScorecardContext.model.ts b/src/apps/review/src/lib/models/AiScorecardContext.model.ts index 45d6142f6..46ba4ccb7 100644 --- a/src/apps/review/src/lib/models/AiScorecardContext.model.ts +++ b/src/apps/review/src/lib/models/AiScorecardContext.model.ts @@ -8,6 +8,7 @@ export interface AiScorecardContextModel extends ChallengeDetailContextModel { submissionId: string workflowId: string workflow?: AiWorkflow + workflowRun?: AiWorkflowRun scorecard?: Scorecard workflowRuns: AiWorkflowRun[] } diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx index 822d5a7f0..0e819f07b 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx @@ -34,10 +34,8 @@ export const AiScorecardContextProvider: FC = props => { challengeDetailsCtx.isLoadingChallengeSubmissions && aiWorkflowRunsLoading - const workflow = useMemo(() => ( - workflowRuns.map(r => r.workflow).find(w => w.id === workflowId) - ), [workflowRuns, workflowId]) - + const workflowRun = useMemo(() => workflowRuns.find(w => w.workflow.id === workflowId), [workflowRuns, workflowId]) + const workflow = useMemo(() => workflowRun?.workflow, [workflowRuns, workflowId]) const scorecard = useMemo(() => workflow?.scorecard, [workflow]) const value = useMemo( @@ -46,6 +44,7 @@ export const AiScorecardContextProvider: FC = props => { submissionId, workflowId, workflowRuns, + workflowRun, workflow, scorecard, isLoading: isLoadingCtxData, @@ -55,6 +54,7 @@ export const AiScorecardContextProvider: FC = props => { submissionId, workflowId, workflowRuns, + workflowRun, isLoadingCtxData, workflow, scorecard, diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss index e65bd326f..8ce7e7e01 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss @@ -15,4 +15,14 @@ flex-shrink: 0; flex-basis: 256px; } + + @include ltelg { + flex-direction: column; + gap: $sp-6; + + > .sidebar { + flex-basis: auto; + width: 100%; + } + } } diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss index e55156475..8148ee7ec 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss +++ b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss @@ -30,14 +30,16 @@ } } + .runEntry { display: flex; flex-direction: row; align-items: center; - justify-content: space-between; + gap: $sp-4; padding: $sp-4; background-color: #f9fafa; cursor: pointer; + position: relative; font-family: "Nunito Sans", sans-serif; font-size: 16px; @@ -80,6 +82,13 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + &Wrap { + flex: 1 1 auto; + } + @include ltelg { + max-width: calc(90vw - 140px); + } } .legend { @@ -110,3 +119,83 @@ } } } + +.mobileTrigger { + display: flex; + align-items: center; + gap: $sp-4; + cursor: pointer; + + .runEntry { + flex: 1 1 auto; + } + + .workflowName { + @include ltelg { + max-width: calc(90vw - 205px); + } + } + + @include gtexl { + display: none; + } +} + +.mobileMenuIcon { + display: flex; + width: 24px; + height: 24px; + align-items: center; + justify-content: center; + padding-left: $sp-4; + border-left: 1px solid #D1DAE4; + flex: 0 0 38px; + + svg { + display: block; + } +} + +.contentsWrap { + @include ltelg { + display: none; + position: relative; + + flex-direction: column; + background: #fff; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: 1000; + padding: $sp-4; + overflow: auto; + + &.open { + display: flex; + } + + .runsWrap { + margin-bottom: $sp-4; + } + + .legend { + margin-top: auto; + } + } +} + +.mobileCloseicon { + display: flex; + height: 24px; + align-items: center; + justify-content: flex-end; + cursor: pointer; + margin-bottom: $sp-4; + + svg { + display: block; + color: #000; + } +} diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx index e1f38ac91..f656d0f3f 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx +++ b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx @@ -1,4 +1,4 @@ -import { FC, useMemo } from 'react' +import { FC, useCallback, useMemo, useState } from 'react' import styles from './AiWorkflowsSidebar.module.scss' import { useAiScorecardContext } from '../../AiScorecardContext' @@ -7,7 +7,7 @@ import { AiWorkflowRunStatus } from '~/apps/review/src/lib/components/AiReviewsT import classNames from 'classnames' import { IconAiReview } from '~/apps/review/src/lib/assets/icons' import StatusLabel from '~/apps/review/src/lib/components/AiReviewsTable/StatusLabel' -import { IconOutline } from '~/libs/ui' +import { IconOutline, IconSolid } from '~/libs/ui' import { Link } from 'react-router-dom' interface AiWorkflowsSidebarProps { @@ -15,40 +15,69 @@ interface AiWorkflowsSidebarProps { } const AiWorkflowsSidebar: FC = props => { - const { workflowRuns, workflowId, submissionId }: AiScorecardContextModel = useAiScorecardContext() + const [isMobileOpen, setIsMobileOpen] = useState(false); + const { workflow, workflowRun, workflowRuns, workflowId, submissionId }: AiScorecardContextModel = useAiScorecardContext() + + const toggleOpen = useCallback(() => { + setIsMobileOpen(wasOpen => !wasOpen); + }, []); + + const close = useCallback(() => { + setIsMobileOpen(false); + }, []); return (
-
-
    - {workflowRuns.map(workflowRun => ( -
  • - - - - {workflowRun.workflow.name} - - -
  • - ))} -
-
+ {workflow && workflowRun && ( +
+
+ + + {workflow.name} + + +
+ +
+
+
+ )} -
-
- Legend +
+
+ +
+
+
    + {workflowRuns.map(workflowRun => ( +
  • + + + + {workflowRun.workflow.name} + + +
  • + ))} +
+
+ +
+
+ Legend +
+
    +
  • + } label='Passed' status="passed" /> +
  • +
  • + } label='Failed' status="failed" /> +
  • +
  • + } label='To be filled' status="pending" /> +
  • +
-
    -
  • - } label='Passed' status="passed" /> -
  • -
  • - } label='Failed' status="failed" /> -
  • -
  • - } label='To be filled' status="pending" /> -
  • -
) From 83019ed6d66d659d03def262eb67e2725cb2f66a Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 30 Oct 2025 17:38:46 +0200 Subject: [PATCH 23/30] PM-2136 - hide close button on desktop --- .../AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss index 8148ee7ec..54197a0e6 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss +++ b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss @@ -198,4 +198,8 @@ display: block; color: #000; } + + @include gtexl { + display: none; + } } From 38e15cd94f29dbe9f468c882b718397791ed3f35 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 30 Oct 2025 21:42:05 +0200 Subject: [PATCH 24/30] lint fixes --- .../AiReviewsTable/AiReviewsTable.tsx | 1 - .../AiReviewsTable/AiWorkflowRunStatus.tsx | 45 +++++++++--- .../components/AiReviewsTable/StatusLabel.tsx | 31 ++++---- .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 3 +- .../active-review.routes.tsx | 12 ++-- .../challenge-details.routes.tsx | 10 +-- .../AiScorecardContextProvider.tsx | 43 ++++++----- .../AiScorecardViewer/AiScorecardViewer.tsx | 12 ++-- .../ai-scorecards/ai-scorecard.routes.tsx | 19 +++-- .../AiWorkflowsSidebar/AiWorkflowsSidebar.tsx | 71 +++++++++++++------ .../ScorecardHeader/ScorecardHeader.tsx | 11 ++- .../past-review.routes.tsx | 10 +-- .../src/pages/scorecards/scorecard.routes.tsx | 3 +- src/apps/review/src/review-app.routes.tsx | 1 - .../core/lib/router/get-routes-container.tsx | 4 +- 15 files changed, 167 insertions(+), 109 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index e85c0afe8..49b4d9f94 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -22,7 +22,6 @@ interface AiReviewsTableProps { reviewers: { aiWorkflowId: string }[] } - const AiReviewsTable: FC = props => { const aiWorkflowIds = useMemo(() => props.reviewers.map(r => r.aiWorkflowId), [props.reviewers]) const { runs, isLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(props.submission.id, aiWorkflowIds) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx index 3b33a3717..0534ae0fc 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx @@ -1,9 +1,8 @@ -import { FC, ReactNode, useCallback, useMemo } from 'react' -import classNames from 'classnames' +import { FC, useMemo } from 'react' import { IconOutline } from '~/libs/ui' -import { aiRunFailed, aiRunInProgress, AiWorkflowRun, AiWorkflowRunStatusEnum } from '../../hooks' +import { aiRunFailed, aiRunInProgress, AiWorkflowRun } from '../../hooks' import StatusLabel from './StatusLabel' @@ -16,28 +15,52 @@ interface AiWorkflowRunStatusProps { export const AiWorkflowRunStatus: FC = props => { const isInProgress = useMemo(() => aiRunInProgress(props.run), [props.run.status]) const isFailed = useMemo(() => aiRunFailed(props.run), [props.run.status]) - const isPassing = props.run.status === 'SUCCESS' && props.run.score >= (props.run.workflow.scorecard?.minimumPassingScore ?? 0) + const isPassing = ( + props.run.status === 'SUCCESS' + && props.run.score >= (props.run.workflow.scorecard?.minimumPassingScore ?? 0) + ) const status = isInProgress ? 'pending' : isFailed ? 'failed' : ( isPassing ? 'passed' : 'failed-score' - ); + ) - const score = props.showScore ? props.run.score : undefined; + const score = props.showScore ? props.run.score : undefined return ( <> {props.run.status === 'SUCCESS' && isPassing && ( - } hideLabel={props.hideLabel} label='Passed' status={status} score={score} /> + } + hideLabel={props.hideLabel} + label='Passed' + status={status} + score={score} + /> )} {props.run.status === 'SUCCESS' && !isPassing && ( - } hideLabel={props.hideLabel} label='Failed' status={status} score={score} /> + } + hideLabel={props.hideLabel} + label='Failed' + status={status} + score={score} + /> )} {isInProgress && ( - } hideLabel={props.hideLabel} label='To be filled' status={status} score={score} /> + } + hideLabel={props.hideLabel} + label='To be filled' + status={status} + score={score} + /> )} {isFailed && ( - } status={status} score={score} /> + } + status={status} + score={score} + /> )} ) } - diff --git a/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.tsx b/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.tsx index 39e6389c3..011649ebd 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/StatusLabel.tsx @@ -1,7 +1,7 @@ import { FC, ReactNode } from 'react' +import classNames from 'classnames' import styles from './StatusLabel.module.scss' -import classNames from 'classnames' interface StatusLabelProps { icon: ReactNode @@ -11,21 +11,18 @@ interface StatusLabelProps { status: 'pending' | 'failed' | 'passed' | 'failed-score' } -const StatusLabel: FC = props => { - - return ( -
- {props.score && ( - {props.score} - )} - {props.icon && ( - - {props.icon} - - )} - {!props.hideLabel && props.label} -
- ) -} +const StatusLabel: FC = props => ( +
+ {props.score && ( + {props.score} + )} + {props.icon && ( + + {props.icon} + + )} + {!props.hideLabel && props.label} +
+) export default StatusLabel diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 6b1acb130..1e3a69717 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -5,9 +5,10 @@ import { EnvironmentConfig } from '~/config' import { xhrGetAsync } from '~/libs/core' import { handleError } from '~/libs/shared/lib/utils/handle-error' -import { useRolePermissions, UseRolePermissionsResult } from './useRolePermissions' import { Scorecard } from '../models' +import { useRolePermissions, UseRolePermissionsResult } from './useRolePermissions' + export enum AiWorkflowRunStatusEnum { INIT = 'INIT', QUEUED = 'QUEUED', diff --git a/src/apps/review/src/pages/active-review-assignements/active-review.routes.tsx b/src/apps/review/src/pages/active-review-assignements/active-review.routes.tsx index b28a89234..2dd316868 100644 --- a/src/apps/review/src/pages/active-review-assignements/active-review.routes.tsx +++ b/src/apps/review/src/pages/active-review-assignements/active-review.routes.tsx @@ -1,8 +1,8 @@ -import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core'; +import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core' -import { activeReviewAssignmentsRouteId, challengeDetailRouteId } from '../../config/routes.config'; +import { activeReviewAssignmentsRouteId } from '../../config/routes.config' -import { challengeDetailsRoutes } from './challenge-details.routes'; +import { challengeDetailsRoutes } from './challenge-details.routes' const ActiveReviewsPage: LazyLoadedComponent = lazyLoad( () => import('./ActiveReviewsPage'), @@ -17,13 +17,13 @@ export const activeReviewChildRoutes = [ route: '', }, ...challengeDetailsRoutes, -]; +] export const activeReviewRoutes = [ { - children: [ ...activeReviewChildRoutes ], + children: [...activeReviewChildRoutes], element: getRoutesContainer(activeReviewChildRoutes), id: activeReviewAssignmentsRouteId, route: activeReviewAssignmentsRouteId, - } + }, ] diff --git a/src/apps/review/src/pages/active-review-assignements/challenge-details.routes.tsx b/src/apps/review/src/pages/active-review-assignements/challenge-details.routes.tsx index fc5c0c41d..e6a61eb14 100644 --- a/src/apps/review/src/pages/active-review-assignements/challenge-details.routes.tsx +++ b/src/apps/review/src/pages/active-review-assignements/challenge-details.routes.tsx @@ -1,11 +1,11 @@ -import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core'; +import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core' -import { challengeDetailRouteId } from '../../config/routes.config'; -import { aiScorecardRoutes } from '../ai-scorecards'; +import { challengeDetailRouteId } from '../../config/routes.config' +import { aiScorecardRoutes } from '../ai-scorecards' const ChallengeDetailContextProvider: LazyLoadedComponent = lazyLoad( () => import('../../lib/contexts/ChallengeDetailContextProvider'), - 'ChallengeDetailContextProvider' + 'ChallengeDetailContextProvider', ) const ScorecardDetailsPage: LazyLoadedComponent = lazyLoad( () => import('./ScorecardDetailsPage'), @@ -38,5 +38,5 @@ export const challengeDetailsRoutes = [ element: getRoutesContainer(challengeDetailsChildRoutes, ChallengeDetailContextProvider), id: challengeDetailRouteId, route: challengeDetailRouteId, - } + }, ] diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx index 0e819f07b..5f6e4169f 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx @@ -6,7 +6,6 @@ import { useParams } from 'react-router-dom' import { ChallengeDetailContext } from '../../../lib' import { AiScorecardContextModel, ChallengeDetailContextModel } from '../../../lib/models' - import { AiWorkflowRunsResponse, useFetchAiWorkflowsRuns } from '../../../lib/hooks' export const AiScorecardContext: Context @@ -22,42 +21,48 @@ export const AiScorecardContextProvider: FC = props => { }>() const challengeDetailsCtx = useContext(ChallengeDetailContext) - const { challengeInfo }: ChallengeDetailContextModel = challengeDetailsCtx; - const aiReviewers = useMemo(() => (challengeInfo?.reviewers ?? []).filter(r => !!r.aiWorkflowId), [challengeInfo?.reviewers]) + const { challengeInfo }: ChallengeDetailContextModel = challengeDetailsCtx + const aiReviewers = useMemo(() => ( + (challengeInfo?.reviewers ?? []).filter(r => !!r.aiWorkflowId) + ), [challengeInfo?.reviewers]) const aiWorkflowIds = useMemo(() => aiReviewers?.map(r => r.aiWorkflowId as string), [aiReviewers]) - const { runs: workflowRuns, isLoading: aiWorkflowRunsLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(submissionId, aiWorkflowIds) + const { runs: workflowRuns, isLoading: aiWorkflowRunsLoading }: AiWorkflowRunsResponse + = useFetchAiWorkflowsRuns(submissionId, aiWorkflowIds) - const isLoadingCtxData = - challengeDetailsCtx.isLoadingChallengeInfo && - challengeDetailsCtx.isLoadingChallengeResources && - challengeDetailsCtx.isLoadingChallengeSubmissions && - aiWorkflowRunsLoading + const isLoadingCtxData + = challengeDetailsCtx.isLoadingChallengeInfo + && challengeDetailsCtx.isLoadingChallengeResources + && challengeDetailsCtx.isLoadingChallengeSubmissions + && aiWorkflowRunsLoading - const workflowRun = useMemo(() => workflowRuns.find(w => w.workflow.id === workflowId), [workflowRuns, workflowId]) + const workflowRun = useMemo( + () => workflowRuns.find(w => w.workflow.id === workflowId), + [workflowRuns, workflowId], + ) const workflow = useMemo(() => workflowRun?.workflow, [workflowRuns, workflowId]) const scorecard = useMemo(() => workflow?.scorecard, [workflow]) const value = useMemo( () => ({ ...challengeDetailsCtx, + isLoading: isLoadingCtxData, + scorecard, submissionId, + workflow, workflowId, - workflowRuns, workflowRun, - workflow, - scorecard, - isLoading: isLoadingCtxData, + workflowRuns, }), [ challengeDetailsCtx, + isLoadingCtxData, + scorecard, submissionId, + workflow, workflowId, - workflowRuns, workflowRun, - isLoadingCtxData, - workflow, - scorecard, + workflowRuns, ], ) @@ -68,4 +73,4 @@ export const AiScorecardContextProvider: FC = props => { ) } -export const useAiScorecardContext = () => useContext(AiScorecardContext) +export const useAiScorecardContext = (): AiScorecardContextModel => useContext(AiScorecardContext) diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx index e05cbab37..7f5ef87da 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx @@ -1,21 +1,19 @@ import { FC, useEffect, useMemo } from 'react' -import styles from './AiScorecardViewer.module.scss' -import { ScorecardHeader } from '../components/ScorecardHeader' import { NotificationContextType, useNotification } from '~/libs/shared' + +import { ScorecardHeader } from '../components/ScorecardHeader' import { IconAiReview } from '../../../lib/assets/icons' import { PageWrapper } from '../../../lib' import { useAiScorecardContext } from '../AiScorecardContext' import { AiScorecardContextModel } from '../../../lib/models' import { AiWorkflowsSidebar } from '../components/AiWorkflowsSidebar' -interface AiScorecardViewerProps { -} - -const AiScorecardViewer: FC = props => { +import styles from './AiScorecardViewer.module.scss' +const AiScorecardViewer: FC = () => { const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() - const { challengeInfo, workflowRuns }: AiScorecardContextModel = useAiScorecardContext() + const { challengeInfo }: AiScorecardContextModel = useAiScorecardContext() const breadCrumb = useMemo( () => [{ index: 1, label: 'My Active Challenges' }], diff --git a/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx b/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx index 1030df850..a911150f2 100644 --- a/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx +++ b/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx @@ -1,9 +1,16 @@ -import { getRoutesContainer, lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/core' +import { getRoutesContainer, lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' import { aiScorecardRouteId } from '../../config/routes.config' -const AiScorecardViewer: LazyLoadedComponent = lazyLoad(() => import('./AiScorecardViewer'), 'AiScorecardViewer') -const AiScorecardContextProvider: LazyLoadedComponent = lazyLoad(() => import('./AiScorecardContext'), 'AiScorecardContextProvider') +const AiScorecardViewer: LazyLoadedComponent = lazyLoad( + () => import('./AiScorecardViewer'), + 'AiScorecardViewer', +) + +const AiScorecardContextProvider: LazyLoadedComponent = lazyLoad( + () => import('./AiScorecardContext'), + 'AiScorecardContextProvider', +) export const aiScorecardChildRoutes: ReadonlyArray = [ { @@ -14,16 +21,14 @@ export const aiScorecardChildRoutes: ReadonlyArray = [ }, ] -// const AiScorecardsContainer = getRoutesContainer(aiScorecardChildRoutes); - export const aiScorecardRoutes: ReadonlyArray = [ { - children: [ ...aiScorecardChildRoutes ], + children: [...aiScorecardChildRoutes], element: getRoutesContainer(aiScorecardChildRoutes, AiScorecardContextProvider), id: aiScorecardRouteId, rolesRequired: [ // UserRole.administrator, ], route: `${aiScorecardRouteId}/:submissionId/:workflowId`, - } + }, ] diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx index f656d0f3f..724c6e973 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx +++ b/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx @@ -1,30 +1,38 @@ -import { FC, useCallback, useMemo, useState } from 'react' +import { FC, useCallback, useState } from 'react' +import { Link } from 'react-router-dom' +import classNames from 'classnames' -import styles from './AiWorkflowsSidebar.module.scss' -import { useAiScorecardContext } from '../../AiScorecardContext' import { AiScorecardContextModel } from '~/apps/review/src/lib/models' import { AiWorkflowRunStatus } from '~/apps/review/src/lib/components/AiReviewsTable' -import classNames from 'classnames' import { IconAiReview } from '~/apps/review/src/lib/assets/icons' -import StatusLabel from '~/apps/review/src/lib/components/AiReviewsTable/StatusLabel' import { IconOutline, IconSolid } from '~/libs/ui' -import { Link } from 'react-router-dom' +import StatusLabel from '~/apps/review/src/lib/components/AiReviewsTable/StatusLabel' + +import { useAiScorecardContext } from '../../AiScorecardContext' + +import styles from './AiWorkflowsSidebar.module.scss' interface AiWorkflowsSidebarProps { className?: string } const AiWorkflowsSidebar: FC = props => { - const [isMobileOpen, setIsMobileOpen] = useState(false); - const { workflow, workflowRun, workflowRuns, workflowId, submissionId }: AiScorecardContextModel = useAiScorecardContext() + const [isMobileOpen, setIsMobileOpen] = useState(false) + const { + workflow, + workflowRun, + workflowRuns, + workflowId, + submissionId, + }: AiScorecardContextModel = useAiScorecardContext() const toggleOpen = useCallback(() => { - setIsMobileOpen(wasOpen => !wasOpen); - }, []); + setIsMobileOpen(wasOpen => !wasOpen) + }, []) const close = useCallback(() => { - setIsMobileOpen(false); - }, []); + setIsMobileOpen(false) + }, []) return (
@@ -49,14 +57,25 @@ const AiWorkflowsSidebar: FC = props => {
    - {workflowRuns.map(workflowRun => ( -
  • - + {workflowRuns.map(run => ( +
  • + - {workflowRun.workflow.name} + {run.workflow.name} - +
  • ))}
@@ -68,13 +87,25 @@ const AiWorkflowsSidebar: FC = props => {
  • - } label='Passed' status="passed" /> + } + label='Passed' + status='passed' + />
  • - } label='Failed' status="failed" /> + } + label='Failed' + status='failed' + />
  • - } label='To be filled' status="pending" /> + } + label='To be filled' + status='pending' + />
diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx index 3e6647e95..dc95ef666 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx @@ -1,14 +1,13 @@ import { FC } from 'react' -import styles from './ScorecardHeader.module.scss' -import { useAiScorecardContext } from '../../AiScorecardContext' import { AiScorecardContextModel } from '~/apps/review/src/lib/models' -interface ScorecardHeaderProps { -} +import { useAiScorecardContext } from '../../AiScorecardContext' + +import styles from './ScorecardHeader.module.scss' -const ScorecardHeader: FC = props => { - const { workflow, scorecard }: AiScorecardContextModel = useAiScorecardContext() +const ScorecardHeader: FC = () => { + const { workflow }: AiScorecardContextModel = useAiScorecardContext() return (
diff --git a/src/apps/review/src/pages/past-review-assignments/past-review.routes.tsx b/src/apps/review/src/pages/past-review-assignments/past-review.routes.tsx index bb72edd4b..04402e78f 100644 --- a/src/apps/review/src/pages/past-review-assignments/past-review.routes.tsx +++ b/src/apps/review/src/pages/past-review-assignments/past-review.routes.tsx @@ -1,7 +1,7 @@ -import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core'; +import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core' -import { pastReviewAssignmentsRouteId } from '../../config/routes.config'; -import { challengeDetailsRoutes } from '../active-review-assignements'; +import { pastReviewAssignmentsRouteId } from '../../config/routes.config' +import { challengeDetailsRoutes } from '../active-review-assignements' const PastReviewsPage: LazyLoadedComponent = lazyLoad( () => import('./PastReviewsPage'), @@ -16,7 +16,7 @@ export const pastReviewChildRoutes = [ route: '', }, ...challengeDetailsRoutes, -]; +] export const pastReviewRoutes = [ { @@ -24,5 +24,5 @@ export const pastReviewRoutes = [ element: getRoutesContainer(pastReviewChildRoutes), id: pastReviewAssignmentsRouteId, route: pastReviewAssignmentsRouteId, - } + }, ] diff --git a/src/apps/review/src/pages/scorecards/scorecard.routes.tsx b/src/apps/review/src/pages/scorecards/scorecard.routes.tsx index a5ca54964..3d535d58c 100644 --- a/src/apps/review/src/pages/scorecards/scorecard.routes.tsx +++ b/src/apps/review/src/pages/scorecards/scorecard.routes.tsx @@ -1,4 +1,5 @@ import { getRoutesContainer, lazyLoad, LazyLoadedComponent, PlatformRoute, UserRole } from '~/libs/core' + import { scorecardRouteId } from '../../config/routes.config' const ScorecardsListPage: LazyLoadedComponent = lazyLoad( @@ -61,5 +62,5 @@ export const scorecardRoutes: ReadonlyArray = [ UserRole.administrator, ], route: scorecardRouteId, - } + }, ] diff --git a/src/apps/review/src/review-app.routes.tsx b/src/apps/review/src/review-app.routes.tsx index 0d4990756..cd95ac79e 100644 --- a/src/apps/review/src/review-app.routes.tsx +++ b/src/apps/review/src/review-app.routes.tsx @@ -14,7 +14,6 @@ import { rootRoute, } from './config/routes.config' import { scorecardRoutes } from './pages/scorecards' -import { aiScorecardRoutes } from './pages/ai-scorecards' import { activeReviewRoutes } from './pages/active-review-assignements' import { pastReviewRoutes } from './pages/past-review-assignments' diff --git a/src/libs/core/lib/router/get-routes-container.tsx b/src/libs/core/lib/router/get-routes-container.tsx index 23b60d17c..39240b54e 100644 --- a/src/libs/core/lib/router/get-routes-container.tsx +++ b/src/libs/core/lib/router/get-routes-container.tsx @@ -2,7 +2,7 @@ * The router outlet. */ -import { FC, Fragment, PropsWithChildren, useContext, useEffect, useMemo } from 'react' +import { FC, Fragment, useContext, useEffect, useMemo } from 'react' import { Outlet, Routes, useLocation } from 'react-router-dom' import { PlatformRoute } from './platform-route.model' @@ -10,7 +10,7 @@ import { routerContext, RouterContextData } from './router-context' export function getRoutesContainer(childRoutes: ReadonlyArray, contextContainer?: FC): JSX.Element { const ContextContainer = contextContainer ?? Fragment - const Container = () => { + const Container = (): JSX.Element => { const location = useLocation() const { getRouteElement }: RouterContextData = useContext(routerContext) const childRoutesWithContext = useMemo( From 9a77c85da7b7bab71ee371afb0f72af850c42451 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 5 Nov 2025 10:04:41 +0200 Subject: [PATCH 25/30] Hide title for review flow name; make first submission row toggled by default --- .../src/lib/components/AiReviewsTable/AiReviewsTable.tsx | 2 +- .../ChallengeDetailsContent/TabContentSubmissions.tsx | 3 ++- .../CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx | 3 ++- src/libs/ui/lib/components/table/table-row/TableRow.tsx | 1 + 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index ed9c21b3e..0a63b4614 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -174,7 +174,7 @@ const AiReviewsTable: FC = props => { - + {run.workflow.name} diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx index c2f2acf3f..eb569e5b4 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx @@ -323,10 +323,11 @@ export const TabContentSubmissions: FC = props => { label: 'Reviewer', mobileColSpan: 2, propertyName: 'virusScan', - renderer: (submission: BackendSubmission) => ( + renderer: (submission: BackendSubmission, allRows: BackendSubmission[]) => ( ), type: 'element', diff --git a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx index 5d00a31c0..7e786979d 100644 --- a/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx +++ b/src/apps/review/src/lib/components/CollapsibleAiReviewsRow/CollapsibleAiReviewsRow.tsx @@ -9,6 +9,7 @@ import { BackendSubmission } from '../../models' import styles from './CollapsibleAiReviewsRow.module.scss' interface CollapsibleAiReviewsRowProps { + defaultOpen?: boolean aiReviewers: { aiWorkflowId: string }[] submission: BackendSubmission } @@ -16,7 +17,7 @@ interface CollapsibleAiReviewsRowProps { const CollapsibleAiReviewsRow: FC = props => { const aiReviewersCount = props.aiReviewers.length + 1 - const [isOpen, setIsOpen] = useState(false) + const [isOpen, setIsOpen] = useState(props.defaultOpen ?? false) const toggleOpen = useCallback(() => { setIsOpen(wasOpen => !wasOpen) diff --git a/src/libs/ui/lib/components/table/table-row/TableRow.tsx b/src/libs/ui/lib/components/table/table-row/TableRow.tsx index a043b3318..f94748d61 100644 --- a/src/libs/ui/lib/components/table/table-row/TableRow.tsx +++ b/src/libs/ui/lib/components/table/table-row/TableRow.tsx @@ -66,6 +66,7 @@ export const TableRow: ( : undefined } style={colWidth ? { width: `${colWidth}px` } : {}} + allRows={props.allRows} /> ) }) From 0b46c0f9f69da0539a17d74b437250a4a9652e50 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 5 Nov 2025 10:31:36 +0200 Subject: [PATCH 26/30] fix lint --- .../ChallengeDetailsContent/TabContentSubmissions.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx index 6bde0404f..5bec3a5e1 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.tsx @@ -56,8 +56,6 @@ interface Props { const VIRUS_SCAN_FAILED_MESSAGE = 'Submission failed virus scan' export const TabContentSubmissions: FC = props => { - console.log('here', props.submissions); - const windowSize: WindowSize = useWindowSize() const isTablet = useMemo( () => (windowSize.width ?? 0) <= 984, From 386af7dc8826ec95fa6f348f366e6655b37fa4d5 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 5 Nov 2025 12:41:59 +0200 Subject: [PATCH 27/30] PM-2135 - AI Workflows - scorecard header --- .../review/src/lib/assets/icons/deepseek.svg | 3 + .../src/lib/assets/icons/icon-clock.svg | 3 + .../src/lib/assets/icons/icon-premium.svg | 3 + src/apps/review/src/lib/assets/icons/index.ts | 6 + .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 11 ++ .../AiModelModal/AiModelModal.module.scss | 49 ++++++++ .../components/AiModelModal/AiModelModal.tsx | 42 +++++++ .../components/AiModelModal/index.ts | 1 + .../ScorecardHeader.module.scss | 110 ++++++++++++++++++ .../ScorecardHeader/ScorecardHeader.tsx | 71 ++++++++++- 10 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 src/apps/review/src/lib/assets/icons/deepseek.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-clock.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-premium.svg create mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss create mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx create mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts diff --git a/src/apps/review/src/lib/assets/icons/deepseek.svg b/src/apps/review/src/lib/assets/icons/deepseek.svg new file mode 100644 index 000000000..e54d70391 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/deepseek.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-clock.svg b/src/apps/review/src/lib/assets/icons/icon-clock.svg new file mode 100644 index 000000000..bc8dd3a99 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-clock.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-premium.svg b/src/apps/review/src/lib/assets/icons/icon-premium.svg new file mode 100644 index 000000000..afa0cf4d4 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-premium.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/index.ts b/src/apps/review/src/lib/assets/icons/index.ts index 7ec1bf70b..80589e44d 100644 --- a/src/apps/review/src/lib/assets/icons/index.ts +++ b/src/apps/review/src/lib/assets/icons/index.ts @@ -9,6 +9,9 @@ import { ReactComponent as IconReview } from './icon-phase-review.svg' import { ReactComponent as IconAppeal } from './icon-phase-appeal.svg' import { ReactComponent as IconAppealResponse } from './icon-phase-appeal-response.svg' import { ReactComponent as IconPhaseWinners } from './icon-phase-winners.svg' +import { ReactComponent as IconDeepseekAi } from './deepseek.svg' +import { ReactComponent as IconClock } from './icon-clock.svg' +import { ReactComponent as IconPremium } from './icon-premium.svg' export * from './editor/bold' export * from './editor/code' @@ -37,6 +40,9 @@ export { IconAppeal, IconAppealResponse, IconPhaseWinners, + IconDeepseekAi, + IconClock, + IconPremium, } export const phasesIcons = { diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 1e3a69717..8787d55da 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -25,10 +25,21 @@ export interface AiWorkflow { name: string; description: string; scorecard?: Scorecard + defUrl: string + llm: { + name: string + description: string + icon: string + url: string + provider: { + name: string + } + } } export interface AiWorkflowRun { id: string; + startedAt: string; completedAt: string; status: AiWorkflowRunStatusEnum; score: number; diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss new file mode 100644 index 000000000..fbedc3b5b --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss @@ -0,0 +1,49 @@ +@import '@libs/ui/styles/includes'; + +.modelNameWrap { + display: flex; + align-items: center; + gap: $sp-4; +} + +.modelIcon { + width: 60px; + height: 60px; + border-radius: $sp-1; + border: 1px solid #A8A8A8; + padding: $sp-1; + + align-items: center; + display: flex; + justify-content: center; + + flex: 0 0 auto; +} + +.modelName { + display: flex; + align-items: center; + gap: $sp-3; + + h3 { + font-family: "Figtree", sans-serif; + font-size: 26px; + font-weight: 700; + line-height: 30px; + color: #0A0A0A; + } + + svg { + display: block; + width: 16px; + height: 16px; + } +} + +.modelDescription { + margin-top: $sp-6; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 20px; + color: #0A0A0A; +} diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx new file mode 100644 index 000000000..2389e0ad2 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react' + +import { BaseModal } from '~/libs/ui' +import { AiWorkflow } from '~/apps/review/src/lib/hooks' +import { IconExternalLink } from '~/apps/review/src/lib/assets/icons' + +import styles from './AiModelModal.module.scss' + +interface AiModelModalProps { + model: AiWorkflow['llm'] + onClose: () => void +} + +const AiModelModal: FC = props => ( + +
+
+
+ {props.model.name} +
+
+

{props.model.name}

+ + + +
+
+ +

+ {props.model.description} +

+
+
+) + +export default AiModelModal diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts new file mode 100644 index 000000000..948754b83 --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts @@ -0,0 +1 @@ +export { default as AiModelModal } from './AiModelModal' diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss index e69de29bb..77dd052ed 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss @@ -0,0 +1,110 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + width: 100%; +} + +.headerWrap { + display: flex; + align-items: flex-start; +} + +.workflowInfo { + display: flex; + align-items: flex-start; + gap: $sp-4; +} + +.workflowIcon { + width: 60px; + height: 60px; + border-radius: $sp-1; + border: 1px solid #A8A8A8; + padding: $sp-1; + + align-items: center; + display: flex; + justify-content: center; + + flex: 0 0 auto; +} + +.workflowName { + display: flex; + flex-direction: column; + gap: $sp-2; + + h3 { + font-family: "Figtree", sans-serif; + font-size: 26px; + font-weight: 700; + line-height: 30px; + color: #0A0A0A; + } + + span { + color: $link-blue-dark; + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 16px; + line-height: 22px; + } +} + +.workflowRunStats { + margin-left: auto; + display: flex; + flex-direction: column; + gap: $sp-1; + + flex: 0 0 auto; + + > span { + display: flex; + align-items: center; + gap: $sp-2; + + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 19px; + color: var(--GrayFontColor); + } + + strong { + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + font-weight: 700; + line-height: 19px; + color: var(--FontColor); + } +} + +.workflowDescription { + margin-top: $sp-6; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 20px; + color: #0A0A0A; +} + +.workflowFileLink { + margin-top: $sp-4; + a { + display: flex; + align-items: center; + gap: $sp-1; + color: $link-blue-dark; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 20px; + + svg { + width: 12px; + height: 12px; + path { + fill: $link-blue-dark; + } + } + } + +} diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx index dc95ef666..e8066582e 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx @@ -1,17 +1,82 @@ -import { FC } from 'react' +import { FC, useCallback, useMemo, useState } from 'react' +import moment, { Duration } from 'moment' import { AiScorecardContextModel } from '~/apps/review/src/lib/models' import { useAiScorecardContext } from '../../AiScorecardContext' +import { IconClock, IconDeepseekAi, IconPremium } from '../../../../lib/assets/icons' +import { AiModelModal } from '../AiModelModal' import styles from './ScorecardHeader.module.scss' +const formatDuration = (duration: Duration): string => [ + !!duration.hours() && `${duration.hours()}h`, + !!duration.minutes() && `${duration.minutes()}m`, + !!duration.seconds() && `${duration.seconds()}s`, +].filter(Boolean) + .join(' ') + const ScorecardHeader: FC = () => { - const { workflow }: AiScorecardContextModel = useAiScorecardContext() + const { workflow, workflowRun }: AiScorecardContextModel = useAiScorecardContext() + const runDuration = useMemo(() => ( + workflowRun && moment.duration( + +new Date(workflowRun.completedAt) - +new Date(workflowRun.startedAt), + 'milliseconds', + ) + ), [workflowRun]) + const [modelDetailsModalVisible, setModelDetailsModalVisible] = useState(false) + + const toggleModelDetails = useCallback(() => { + setModelDetailsModalVisible(wasVisible => !wasVisible) + }, []) + + if (!workflow || !workflowRun) { + return <> + } return (
- {workflow?.name} +
+
+ +
+

{workflow.name}

+ {workflow.llm.name} +
+
+
+ + + + Minimum passing score: + {' '} + {workflow.scorecard?.minimumPassingScore.toFixed(2)} + + + + + + Duration: + {' '} + {!!runDuration && formatDuration(runDuration)} + + +
+
+

+ {workflow.description} +

+ {/* */} + + {modelDetailsModalVisible && ( + + )}
) } From 6251eaefac9fbadabd77aec2e952129daa6087c4 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 5 Nov 2025 12:52:17 +0200 Subject: [PATCH 28/30] PM-2135 - mobile ui --- .../AiModelModal/AiModelModal.module.scss | 17 ++++++++++ .../ScorecardHeader.module.scss | 31 ++++++++++++++++--- .../ScorecardHeader/ScorecardHeader.tsx | 2 +- 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss index fbedc3b5b..3f0514beb 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss @@ -1,9 +1,19 @@ @import '@libs/ui/styles/includes'; +.wrap { + @include ltemd { + padding-top: $sp-15; + } +} + .modelNameWrap { display: flex; align-items: center; gap: $sp-4; + @include ltemd { + flex-direction: column; + gap: $sp-4; + } } .modelIcon { @@ -38,6 +48,13 @@ width: 16px; height: 16px; } + + @include ltemd { + h3 { + font-size: 22px; + line-height: 26px; + } + } } .modelDescription { diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss index 77dd052ed..b84af1272 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss @@ -2,11 +2,18 @@ .wrap { width: 100%; + color: #0A0A0A; } .headerWrap { display: flex; align-items: flex-start; + + @include ltemd { + flex-direction: column; + align-items: stretch; + gap: $sp-6; + } } .workflowInfo { @@ -27,19 +34,20 @@ justify-content: center; flex: 0 0 auto; + @include ltemd { + width: 56px; + height: 56px; + } } .workflowName { - display: flex; - flex-direction: column; - gap: $sp-2; - h3 { font-family: "Figtree", sans-serif; font-size: 26px; font-weight: 700; line-height: 30px; color: #0A0A0A; + margin-bottom: $sp-2; } span { @@ -49,6 +57,17 @@ font-size: 16px; line-height: 22px; } + + .modelName { + cursor: pointer; + } + + @include ltemd { + h3 { + font-size: 22px; + line-height: 26px; + } + } } .workflowRunStats { @@ -77,6 +96,10 @@ line-height: 19px; color: var(--FontColor); } + + @include ltemd { + margin-left: 0; + } } .workflowDescription { diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx index e8066582e..5977e71fa 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx @@ -41,7 +41,7 @@ const ScorecardHeader: FC = () => {

{workflow.name}

- {workflow.llm.name} + {workflow.llm.name}
From e1dc7fd5918954f810dca0ede029d43cf8ef92bd Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 5 Nov 2025 13:24:04 +0200 Subject: [PATCH 29/30] PM-2135 - fallback for ai model icon --- .../ai-scorecards/components/AiModelIcon.tsx | 27 +++++++++++++++++++ .../components/AiModelModal/AiModelModal.tsx | 6 +++-- .../ScorecardHeader/ScorecardHeader.tsx | 9 ++++--- 3 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx new file mode 100644 index 000000000..9d296780d --- /dev/null +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx @@ -0,0 +1,27 @@ +import { FC, useCallback, useRef } from 'react' + +import iconDeepseekAi from '~/apps/review/src/lib/assets/icons/deepseek.svg' + +import { AiWorkflow } from '../../../lib/hooks' + +interface AiModelIconProps { + model: AiWorkflow['llm'] +} + +const AiModelIcon: FC = props => { + const llmIconImgRef = useRef(null) + + const handleError = useCallback(() => { + if (!llmIconImgRef.current) { + return + } + + llmIconImgRef.current.src = iconDeepseekAi + }, []) + + return ( + {props.model.name} + ) +} + +export default AiModelIcon diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx index 2389e0ad2..8aa2e02ae 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx +++ b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx @@ -4,6 +4,8 @@ import { BaseModal } from '~/libs/ui' import { AiWorkflow } from '~/apps/review/src/lib/hooks' import { IconExternalLink } from '~/apps/review/src/lib/assets/icons' +import AiModelIcon from '../AiModelIcon' + import styles from './AiModelModal.module.scss' interface AiModelModalProps { @@ -22,11 +24,11 @@ const AiModelModal: FC = props => (
- {props.model.name} +

{props.model.name}

- +
diff --git a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx index 5977e71fa..442ee1232 100644 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx @@ -4,8 +4,9 @@ import moment, { Duration } from 'moment' import { AiScorecardContextModel } from '~/apps/review/src/lib/models' import { useAiScorecardContext } from '../../AiScorecardContext' -import { IconClock, IconDeepseekAi, IconPremium } from '../../../../lib/assets/icons' +import { IconClock, IconPremium } from '../../../../lib/assets/icons' import { AiModelModal } from '../AiModelModal' +import AiModelIcon from '../AiModelIcon' import styles from './ScorecardHeader.module.scss' @@ -19,7 +20,7 @@ const formatDuration = (duration: Duration): string => [ const ScorecardHeader: FC = () => { const { workflow, workflowRun }: AiScorecardContextModel = useAiScorecardContext() const runDuration = useMemo(() => ( - workflowRun && moment.duration( + workflowRun && workflowRun.completedAt && workflowRun.startedAt && moment.duration( +new Date(workflowRun.completedAt) - +new Date(workflowRun.startedAt), 'milliseconds', ) @@ -38,7 +39,9 @@ const ScorecardHeader: FC = () => {
- +
+ +

{workflow.name}

{workflow.llm.name} From 69161641afb2a53fc0f6b1179eac6f2dc66c8ff8 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 6 Nov 2025 09:13:51 +0200 Subject: [PATCH 30/30] add llm icons --- public/llm-icons/chatgpt-icon.svg | 1 + public/llm-icons/deepseek-icon.svg | 3 ++ public/llm-icons/google-gemini-icon.svg | 1 + public/llm-icons/meta-llama-3-icon.svg | 66 +++++++++++++++++++++++++ public/llm-icons/qwen-icon.svg | 15 ++++++ 5 files changed, 86 insertions(+) create mode 100644 public/llm-icons/chatgpt-icon.svg create mode 100644 public/llm-icons/deepseek-icon.svg create mode 100644 public/llm-icons/google-gemini-icon.svg create mode 100644 public/llm-icons/meta-llama-3-icon.svg create mode 100644 public/llm-icons/qwen-icon.svg diff --git a/public/llm-icons/chatgpt-icon.svg b/public/llm-icons/chatgpt-icon.svg new file mode 100644 index 000000000..f6f6925e7 --- /dev/null +++ b/public/llm-icons/chatgpt-icon.svg @@ -0,0 +1 @@ +ChatGPT \ No newline at end of file diff --git a/public/llm-icons/deepseek-icon.svg b/public/llm-icons/deepseek-icon.svg new file mode 100644 index 000000000..e54d70391 --- /dev/null +++ b/public/llm-icons/deepseek-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/llm-icons/google-gemini-icon.svg b/public/llm-icons/google-gemini-icon.svg new file mode 100644 index 000000000..ecc24b6c2 --- /dev/null +++ b/public/llm-icons/google-gemini-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/llm-icons/meta-llama-3-icon.svg b/public/llm-icons/meta-llama-3-icon.svg new file mode 100644 index 000000000..7b9223978 --- /dev/null +++ b/public/llm-icons/meta-llama-3-icon.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + diff --git a/public/llm-icons/qwen-icon.svg b/public/llm-icons/qwen-icon.svg new file mode 100644 index 000000000..78a16baf5 --- /dev/null +++ b/public/llm-icons/qwen-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file