From 419163ebcf7e17101c3a967c608b841d0aed79c3 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 16 Oct 2025 17:04:29 +0300 Subject: [PATCH 001/125] 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 002/125] 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 003/125] 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 004/125] 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 005/125] 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 006/125] 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 007/125] 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 008/125] 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 009/125] 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 010/125] 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 011/125] 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 012/125] 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 013/125] 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 014/125] 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 015/125] 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 016/125] 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 017/125] 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 018/125] 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 019/125] 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 020/125] 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 021/125] 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 022/125] 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 023/125] 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 024/125] 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 025/125] 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 026/125] 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 027/125] 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 028/125] 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 029/125] 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 030/125] 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 From aa97f024dbdeba8c3fe8d03604f25a25aedfd9a1 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 6 Nov 2025 14:16:54 +0200 Subject: [PATCH 031/125] PM-1906 - AI workflow - ai review scorecard UI --- .../ScorecardGroup/ScorecardGroup.module.scss | 59 ++++++++++++ .../ScorecardGroup/ScorecardGroup.tsx | 61 +++++++++++++ .../ScorecardViewer/ScorecardGroup/index.ts | 1 + .../AiFeedback/AiFeedback.module.scss | 7 ++ .../AiFeedback/AiFeedback.tsx | 49 ++++++++++ .../ScorecardQuestion/AiFeedback/index.ts | 1 + .../ScorecardQuestion.module.scss | 25 +++++ .../ScorecardQuestion/ScorecardQuestion.tsx | 51 +++++++++++ .../ScorecardQuestionRow.module.scss | 18 ++++ .../ScorecardQuestionRow.tsx | 22 +++++ .../ScorecardQuestionRow/index.ts | 1 + .../ScorecardQuestion/index.ts | 1 + .../ScorecardScore/ScorecardScore.module.scss | 11 +++ .../ScorecardScore/ScorecardScore.tsx | 33 +++++++ .../ScorecardViewer/ScorecardScore/index.ts | 1 + .../ScorecardSection.module.scss | 25 +++++ .../ScorecardSection/ScorecardSection.tsx | 55 +++++++++++ .../ScorecardViewer/ScorecardSection/index.ts | 1 + .../ScorecardTotal/ScorecardTotal.module.scss | 20 ++++ .../ScorecardTotal/ScorecardTotal.tsx | 27 ++++++ .../ScorecardViewer/ScorecardTotal/index.ts | 1 + .../ScorecardViewer.context.tsx | 52 +++++++++++ .../ScorecardViewer.module.scss | 8 ++ .../ScorecardViewer/ScorecardViewer.tsx | 32 +++++++ .../Scorecard/ScorecardViewer/index.ts | 1 + .../Scorecard/ScorecardViewer/utils.ts | 25 +++++ .../src/lib/components/Scorecard/index.ts | 1 + .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 38 +++++++- .../src/lib/models/AiFeedbackItem.model.ts | 9 ++ src/apps/review/src/lib/models/index.ts | 1 + src/apps/review/src/lib/services/index.ts | 1 - .../review/src/lib/services/tabs.service.ts | 11 --- src/apps/review/src/mock-datas/MockTabs.ts | 91 ------------------- src/apps/review/src/mock-datas/index.ts | 1 - .../AiScorecardViewer.module.scss | 5 +- .../AiScorecardViewer/AiScorecardViewer.tsx | 18 +++- 36 files changed, 655 insertions(+), 109 deletions(-) create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/index.ts create mode 100644 src/apps/review/src/lib/models/AiFeedbackItem.model.ts delete mode 100644 src/apps/review/src/lib/services/tabs.service.ts delete mode 100644 src/apps/review/src/mock-datas/MockTabs.ts delete mode 100644 src/apps/review/src/mock-datas/index.ts diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss new file mode 100644 index 000000000..320c5f8d8 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss @@ -0,0 +1,59 @@ +@import '@libs/ui/styles/includes'; + +.wrap { +} + +.headerBar { + display: flex; + align-items: center; + gap: $sp-4; + + background: #00797A; + padding: $sp-4; + + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 16px; + line-height: 22px; + color: #FFFFFF; + + cursor: pointer; + transition: 0.15s ease-in-out; + + &:hover { + background: darken(#00797A, 1.5%); + } + + &:active { + transition: none; + background: darken(#00797A, 3%); + } + + &.toggled { + .toggleBtn { + svg { + transform: rotate(180deg); + } + } + } +} + +.index { + width: 24px; +} + +.mx { + margin: 0 auto; +} + +.toggleBtn { + cursor: pointer; + width: 24px; + + svg { + display: block; + width: 16px; + height: 16px; + transition: 0.15s ease-in-out; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx new file mode 100644 index 000000000..f18d9451e --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx @@ -0,0 +1,61 @@ +import { FC, useCallback, useMemo } from 'react' +import classNames from 'classnames' + +import { IconOutline } from '~/libs/ui' + +import { ScorecardGroup as ScorecardGroupModel } from '../../../../models' +import { ScorecardSection } from '../ScorecardSection' +import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' + +import styles from './ScorecardGroup.module.scss' +import { ScorecardScore } from '../ScorecardScore' +import { calcGroupScore } from '../utils' + +interface ScorecardGroupProps { + index: number + group: ScorecardGroupModel +} + +const ScorecardGroup: FC = props => { + const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() + const allFeedbackItems = aiFeedbackItems || []; + const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext(); + + const isVissible = !toggledItems[props.group.id]; + const toggle = useCallback(() => toggleItem(props.group.id), [props.group, toggleItem]); + + const score = useMemo(() => ( + calcGroupScore(props.group, allFeedbackItems) + ), [props.group, allFeedbackItems]) + + return ( +
+
+ + {props.index}. + + + {props.group.name} + + + + + + + + +
+ + {isVissible && props.group.sections.map((section, index) => ( + + ))} +
+ ) +} + +export default ScorecardGroup diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/index.ts new file mode 100644 index 000000000..ceab26317 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/index.ts @@ -0,0 +1 @@ +export { default as ScorecardGroup } from './ScorecardGroup' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss new file mode 100644 index 000000000..541a15810 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss @@ -0,0 +1,7 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + p { + margin-bottom: $sp-4; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx new file mode 100644 index 000000000..001b0457a --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx @@ -0,0 +1,49 @@ +import { FC, useMemo } from 'react' + +import { ScorecardViewerContextValue, useScorecardContext } from '../../ScorecardViewer.context' +import { ScorecardQuestionRow } from '../ScorecardQuestionRow' + +import styles from './AiFeedback.module.scss' +import { IconAiReview } from '~/apps/review/src/lib/assets/icons' +import { ScorecardQuestion } from '~/apps/review/src/lib/models' +import { ScorecardScore } from '../../ScorecardScore' + +interface AiFeedbackProps { + question: ScorecardQuestion +} + +const AiFeedback: FC = props => { + const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() + const feedback = useMemo(() => aiFeedbackItems?.find(r => r.scorecardQuestionId === props.question.id), [props.question.id, aiFeedbackItems]) + + if (!aiFeedbackItems?.length || !feedback) { + return <> + } + + const isYesNo = props.question.type === 'YES_NO'; + + return ( + } + index="AI Feedback" + className={styles.wrap} + score={ + + } + > + {isYesNo && ( +

+ {feedback.questionScore ? 'Yes' : 'No'} +

+ )} + {feedback.content} +
+ ) +} + +export default AiFeedback diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/index.ts new file mode 100644 index 000000000..5b6150e88 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/index.ts @@ -0,0 +1 @@ +export { default as AiFeedback } from './AiFeedback' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss new file mode 100644 index 000000000..12c21f897 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss @@ -0,0 +1,25 @@ +@import '@libs/ui/styles/includes'; + +.toggleBtn { + cursor: pointer; + display: block; + width: 16px; + height: 16px; + color: #767676; + transition: 0.15s ease-in-out; + + &.toggled { + transform: rotate(180deg); + } +} + +.questionText { + font-weight: bold; + + * { + margin-top: $sp-2; + } +} + +.guidelines { + white-space: pre-wrap; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx new file mode 100644 index 000000000..e0a33ae5e --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx @@ -0,0 +1,51 @@ +import { FC, useCallback } from 'react' + +import { IconOutline } from '~/libs/ui' + +import { ScorecardQuestion as ScorecardQuestionModel } from '../../../../models' + +import styles from './ScorecardQuestion.module.scss' +import { AiFeedback } from './AiFeedback' +import { ScorecardQuestionRow } from './ScorecardQuestionRow' +import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' +import classNames from 'classnames' + +interface ScorecardQuestionProps { + index: string + question: ScorecardQuestionModel +} + +const ScorecardQuestion: FC = props => { + const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext(); + + const isToggled = toggledItems[props.question.id!]; + const toggle = useCallback(() => toggleItem(props.question.id!), [props.question, toggleItem]); + + return ( +
+ + } + index={`Question ${props.index}`} + className={styles.headerBar} + score='' + > + + {props.question.description} + + {isToggled && ( +
+ {props.question.guidelines} +
+ )} +
+ +
+ ) +} + +export default ScorecardQuestion diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.module.scss new file mode 100644 index 000000000..fb7faf360 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.module.scss @@ -0,0 +1,18 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + display: grid; + gap: $sp-4; + padding: $sp-4; + grid-template-columns: 24px 128px auto 144px 24px; + + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 14px; + line-height: 20px; + color: #0A0A0A; +} + +.content { + font-weight: normal; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx new file mode 100644 index 000000000..bce540b25 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx @@ -0,0 +1,22 @@ +import classNames from 'classnames' +import { FC, PropsWithChildren, ReactNode } from 'react' + +import styles from './ScorecardQuestionRow.module.scss' + +interface ScorecardQuestionRowProps extends PropsWithChildren { + className?: string + icon?: ReactNode + index?: string + score?: ReactNode +} + +const ScorecardQuestionRow: FC = props => ( +
+ {props.icon} + {props.index} + {props.children} + {props.score} +
+) + +export default ScorecardQuestionRow diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/index.ts new file mode 100644 index 000000000..2075f87d2 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/index.ts @@ -0,0 +1 @@ +export { default as ScorecardQuestionRow } from './ScorecardQuestionRow' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/index.ts new file mode 100644 index 000000000..450320793 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/index.ts @@ -0,0 +1 @@ +export { default as ScorecardQuestion } from './ScorecardQuestion' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss new file mode 100644 index 000000000..7ca015ccd --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss @@ -0,0 +1,11 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + font-weight: normal; + text-align: right; + + span { + margin-left: $sp-1; + opacity: 0.75; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx new file mode 100644 index 000000000..98f33b4a5 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx @@ -0,0 +1,33 @@ +import { FC } from 'react' + +import styles from './ScorecardScore.module.scss' +import { ScorecardQuestion } from '../../../../models' + +interface ScorecardScoreProps { + score: number + scaleMax: number + scaleType: ScorecardQuestion['type'] + weight: number +} + +export const calcScore = (score: number, scaleMax: number, weight: number) => ( + (score / (scaleMax || 1)) * weight +) + +const ScorecardScore: FC = props => { + const score = calcScore(props.score, props.scaleMax, props.weight) + + return ( +
+ + {score.toFixed(2)} + + / + + {props.weight.toFixed(2)} + +
+ ) +} + +export default ScorecardScore diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/index.ts new file mode 100644 index 000000000..693b38108 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/index.ts @@ -0,0 +1 @@ +export { default as ScorecardScore } from './ScorecardScore' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss new file mode 100644 index 000000000..3d73dc829 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss @@ -0,0 +1,25 @@ +@import '@libs/ui/styles/includes'; + +.wrap { +} + +.headerBar { + display: flex; + align-items: center; + gap: $sp-4; + + background: #E0E4E8; + padding: $sp-4; + + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 14px; + line-height: 20px; + color: #0A0A0A; + + padding-right: 56px; +} + +.mx { + margin: 0 auto; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx new file mode 100644 index 000000000..26dc61b8d --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx @@ -0,0 +1,55 @@ +import { FC, useMemo } from 'react' + +import { ScorecardSection as ScorecardSectionModel } from '../../../../models' +import { ScorecardQuestion } from '../ScorecardQuestion' +import { ScorecardScore } from '../ScorecardScore' +import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' +import { calcSectionScore } from '../utils' + +import styles from './ScorecardSection.module.scss' + +interface ScorecardSectionProps { + index: string + section: ScorecardSectionModel +} + +const ScorecardSection: FC = props => { + const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() + const allFeedbackItems = aiFeedbackItems || []; + + const score = useMemo(() => ( + calcSectionScore(props.section, allFeedbackItems) + ), [props.section, allFeedbackItems]) + + return ( +
+
+ + {props.index}. + + + {props.section.name} + + + + + +
+ + {props.section.questions.map((question, index) => ( + + ))} +
+ ) +} + +export default ScorecardSection diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/index.ts new file mode 100644 index 000000000..2b352782a --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/index.ts @@ -0,0 +1 @@ +export { default as ScorecardSection } from './ScorecardSection' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.module.scss new file mode 100644 index 000000000..4fe0e4845 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.module.scss @@ -0,0 +1,20 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + display: flex; + align-items: center; + gap: $sp-6; + + padding: $sp-4 56px; + background: #E9ECEF; + + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 16px; + line-height: 22px; + color: #0A0A0A; +} + +.mx { + margin: 0 auto; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx new file mode 100644 index 000000000..bc741215a --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx @@ -0,0 +1,27 @@ +import { FC } from 'react' + +import styles from './ScorecardTotal.module.scss' +import { ScorecardScore } from '../ScorecardScore' + +interface ScorecardTotalProps { + score?: number +} + +const ScorecardTotal: FC = props => { + + + return ( +
+ Total Score + + +
+ ) +} + +export default ScorecardTotal diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/index.ts new file mode 100644 index 000000000..60836c05c --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/index.ts @@ -0,0 +1 @@ +export { default as ScorecardTotal } from './ScorecardTotal' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx new file mode 100644 index 000000000..5ba4328a4 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx @@ -0,0 +1,52 @@ +import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react'; +import { AiFeedbackItem, Scorecard } from '../../../models'; +import { isEmpty } from 'lodash'; + +export interface ScorecardViewerContextProps { + children: ReactNode; + scorecard: Scorecard + aiFeedbackItems?: AiFeedbackItem[] +} + +export type ScorecardViewerContextValue = { + aiFeedbackItems?: AiFeedbackItem[] + toggledItems: {[key: string]: boolean} + toggleItem: (id: string) => void +}; + +const ScorecardViewerContext = createContext({} as ScorecardViewerContextValue); + + +export function ScorecardViewerContextProvider({ + children, + aiFeedbackItems, + scorecard, + ...props +}: ScorecardViewerContextProps) { + const [toggledItems, setToggledItems] = useState<{[key: string]: boolean}>({}); + + const toggleItem = useCallback((id: string, toggle?: boolean) => { + setToggledItems((prevItems) => ({ + ...prevItems, + [id]: typeof toggle === 'boolean' ? toggle : !prevItems[id], + })) + }, []); + + // reset toggle state on scorecard change + useEffect(() => setToggledItems({}), [scorecard]); + + return ( + + {children} + + ); +}; + +export const useScorecardContext = () => useContext(ScorecardViewerContext); diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.module.scss new file mode 100644 index 000000000..03f153733 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.module.scss @@ -0,0 +1,8 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + width: 100%; + strong { + font-weight: bold; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx new file mode 100644 index 000000000..77c989a59 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -0,0 +1,32 @@ +import { FC } from 'react' + +import styles from './ScorecardViewer.module.scss' +import { AiFeedbackItem, Scorecard } from '../../../models' +import { ScorecardGroup } from './ScorecardGroup' +import { ScorecardViewerContextProvider } from './ScorecardViewer.context' +import { ScorecardTotal } from './ScorecardTotal' + +interface ScorecardViewerProps { + scorecard: Scorecard + aiFeedback?: AiFeedbackItem[] + score?: number +} + +const ScorecardViewer: FC = props => { + + return ( +
+ + {props.scorecard.scorecardGroups.map((group, index) => ( + + ))} + + +
+ ) +} + +export default ScorecardViewer diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/index.ts new file mode 100644 index 000000000..0cf63a0e2 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/index.ts @@ -0,0 +1 @@ +export { default as ScorecardViewer } from './ScorecardViewer' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts new file mode 100644 index 000000000..20e60a381 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts @@ -0,0 +1,25 @@ +import { AiFeedbackItem, ScorecardGroup, ScorecardSection } from '../../../models' + +export const calcSectionScore = ( + section: ScorecardSection, + feedbackItems: Pick[], +): number => { + const feedbackItemsMap = Object.fromEntries(feedbackItems.map(r => [r.scorecardQuestionId, r])) + + return section.questions.reduce((sum, question) => ( + sum + ( + (feedbackItemsMap[question.id as string]?.questionScore ?? 0) / (question.scaleMax || 1) + ) * (question.weight / 100) + ), 0) +} + +export const calcGroupScore = ( + group: ScorecardGroup, + feedbackItems: Pick[], +): number => ( + group.sections.reduce((sum, section) => ( + sum + ( + calcSectionScore(section, feedbackItems) + ) * (section.weight / 100) + ), 0) +) diff --git a/src/apps/review/src/lib/components/Scorecard/index.ts b/src/apps/review/src/lib/components/Scorecard/index.ts new file mode 100644 index 000000000..2a3dc98a0 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/index.ts @@ -0,0 +1 @@ +export * from './ScorecardViewer' diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 8787d55da..3120c869a 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -5,7 +5,7 @@ import { EnvironmentConfig } from '~/config' import { xhrGetAsync } from '~/libs/core' import { handleError } from '~/libs/shared/lib/utils/handle-error' -import { Scorecard } from '../models' +import { AiFeedbackItem, Scorecard } from '../models' import { useRolePermissions, UseRolePermissionsResult } from './useRolePermissions' @@ -46,6 +46,8 @@ export interface AiWorkflowRun { workflow: AiWorkflow } +export type AiWorkflowRunItem = AiFeedbackItem + const TC_API_BASE_URL = EnvironmentConfig.API.V6 export interface AiWorkflowRunsResponse { @@ -53,6 +55,11 @@ export interface AiWorkflowRunsResponse { isLoading: boolean } +export interface AiWorkflowRunItemsResponse { + runItems: AiWorkflowRunItem[] + isLoading: boolean +} + export const aiRunInProgress = (aiRun: Pick): boolean => [ AiWorkflowRunStatusEnum.INIT, AiWorkflowRunStatusEnum.QUEUED, @@ -103,3 +110,32 @@ export function useFetchAiWorkflowsRuns( runs: runs.filter(r => isAdmin || !aiRunFailed(r)), } } + +export function useFetchAiWorkflowsRunItems( + workflowId: string, + runId: string | undefined, +): AiWorkflowRunItemsResponse { + // Use swr hooks for challenge info fetching + const { + data: runItems = [], + error: fetchError, + isValidating: isLoading, + }: SWRResponse = useSWR( + `${TC_API_BASE_URL}/workflows/${workflowId}/runs/${runId}/items`, + { + isPaused: () => !workflowId || !runId, + }, + ) + + // Show backend error when fetching challenge info + useEffect(() => { + if (fetchError) { + handleError(fetchError) + } + }, [fetchError]) + + return { + isLoading, + runItems, + } +} diff --git a/src/apps/review/src/lib/models/AiFeedbackItem.model.ts b/src/apps/review/src/lib/models/AiFeedbackItem.model.ts new file mode 100644 index 000000000..5ea7040f2 --- /dev/null +++ b/src/apps/review/src/lib/models/AiFeedbackItem.model.ts @@ -0,0 +1,9 @@ +export interface AiFeedbackItem { + id: string + content: string + upVotes: number + downVotes: number + questionScore: number + comments: string[] + scorecardQuestionId: string +} diff --git a/src/apps/review/src/lib/models/index.ts b/src/apps/review/src/lib/models/index.ts index ff5ead5a7..32aaa987e 100644 --- a/src/apps/review/src/lib/models/index.ts +++ b/src/apps/review/src/lib/models/index.ts @@ -1,4 +1,5 @@ export * from './AiScorecardContext.model' +export * from './AiFeedbackItem.model' export * from './ChallengeInfo.model' export * from './SubmissionInfo.model' export * from './ReviewInfo.model' diff --git a/src/apps/review/src/lib/services/index.ts b/src/apps/review/src/lib/services/index.ts index 79cbfbd69..097c180b6 100644 --- a/src/apps/review/src/lib/services/index.ts +++ b/src/apps/review/src/lib/services/index.ts @@ -1,7 +1,6 @@ export * from './reviews.service' export * from './challenges.service' export * from './scorecards.service' -export * from './tabs.service' export * from './file-upload.service' export * from './resources.service' export * from './payments.service' diff --git a/src/apps/review/src/lib/services/tabs.service.ts b/src/apps/review/src/lib/services/tabs.service.ts deleted file mode 100644 index 770bacfd7..000000000 --- a/src/apps/review/src/lib/services/tabs.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { find, isFunction } from 'lodash' - -import { MockTabs } from '../../mock-datas' -import { SelectOption } from '../models' - -export const fetchTabs = async (type: string, tabsLength: number = 1): Promise => { - const tabs = (find(MockTabs, t => t.name.includes(type)) ?? MockTabs[0]).tabs - return Promise.resolve( - isFunction(tabs) ? tabs(tabsLength) as SelectOption[] : tabs as SelectOption[], - ) -} diff --git a/src/apps/review/src/mock-datas/MockTabs.ts b/src/apps/review/src/mock-datas/MockTabs.ts deleted file mode 100644 index cf9031d45..000000000 --- a/src/apps/review/src/mock-datas/MockTabs.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { - BUG_HUNT, - CODE, - COPILOT_OPPORTUNITY, - DESIGN, - FIRST2FINISH, - MARATHON_MATCH, - OTHER, - TEST_SUITE, -} from '../config/index.config' -import type { SelectOption } from '../lib/models' - -type TabsFactory = (tabsLength?: number) => SelectOption[] - -type MockTabsConfig = { - name: string - tabs: SelectOption[] | TabsFactory -} - -/** - * Mock data for the tabs - */ -export const MockTabs: MockTabsConfig[] = [ - { - name: `${CODE} ${BUG_HUNT} ${TEST_SUITE} ${COPILOT_OPPORTUNITY} ${MARATHON_MATCH} ${OTHER}`, - tabs: [ - { - label: 'Registration', - value: 'Registration', - }, - { - label: 'Submission / Screening', - value: 'Submission / Screening', - }, - { - label: 'Review / Appeals', - value: 'Review / Appeals', - }, - { - label: 'Winners', - value: 'Winners', - }, - ], - }, - { - name: `${DESIGN}`, - tabs: [ - { - label: 'Registration', - value: 'Registration', - }, - { - label: 'Submission / Screening', - value: 'Submission / Screening', - }, - { - label: 'Review', - value: 'Review', - }, - { - label: 'Approval', - value: 'Approval', - }, - { - label: 'Winners', - value: 'Winners', - }, - ], - }, - { - name: `${FIRST2FINISH}`, - tabs: () => [ - { - label: 'Registration', - value: 'Registration', - }, - { - label: 'Submission / Screening', - value: 'Submission / Screening', - }, - { - label: 'Iterative Review', - value: 'Iterative Review', - }, - { - label: 'Winners', - value: 'Winners', - }, - ], - }, -] diff --git a/src/apps/review/src/mock-datas/index.ts b/src/apps/review/src/mock-datas/index.ts deleted file mode 100644 index 5aff070c9..000000000 --- a/src/apps/review/src/mock-datas/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './MockTabs' 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 8ce7e7e01..53a8cd26f 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 @@ -5,7 +5,7 @@ flex-direction: column; } -.contentWrap { +.pageContentWrap { display: flex; flex-direction: row; gap: $sp-10; @@ -26,3 +26,6 @@ } } } +.contentWrap { + width: 100%; +} 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 7f5ef87da..6f566fea9 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx @@ -10,10 +10,13 @@ import { AiScorecardContextModel } from '../../../lib/models' import { AiWorkflowsSidebar } from '../components/AiWorkflowsSidebar' import styles from './AiScorecardViewer.module.scss' +import { ScorecardViewer } from '../../../lib/components/Scorecard' +import { AiWorkflowRunItemsResponse, useFetchAiWorkflowsRunItems } from '../../../lib/hooks' const AiScorecardViewer: FC = () => { const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() - const { challengeInfo }: AiScorecardContextModel = useAiScorecardContext() + const { challengeInfo, scorecard, workflowId, workflowRun }: AiScorecardContextModel = useAiScorecardContext() + const { runItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(workflowId, workflowRun?.id) const breadCrumb = useMemo( () => [{ index: 1, label: 'My Active Challenges' }], @@ -36,9 +39,18 @@ const AiScorecardViewer: FC = () => { className={styles.container} breadCrumb={breadCrumb} > -
+
- +
+ + {!!scorecard && ( + + )} +
) From e35179f9ed6f4bff5ea0fe9fdce1447a91a8e07f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 6 Nov 2025 23:14:45 +0200 Subject: [PATCH 032/125] lint fixes --- .../ScorecardGroup/ScorecardGroup.tsx | 18 +++---- .../AiFeedback/AiFeedback.tsx | 20 ++++---- .../ScorecardQuestion/ScorecardQuestion.tsx | 16 +++--- .../ScorecardQuestionRow.tsx | 2 +- .../ScorecardScore/ScorecardScore.tsx | 4 +- .../ScorecardSection/ScorecardSection.tsx | 8 +-- .../ScorecardTotal/ScorecardTotal.tsx | 30 +++++------- .../ScorecardViewer.context.tsx | 49 +++++++++---------- .../ScorecardViewer/ScorecardViewer.tsx | 32 ++++++------ .../AiScorecardViewer/AiScorecardViewer.tsx | 4 +- 10 files changed, 88 insertions(+), 95 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx index f18d9451e..ccccda9ad 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx @@ -6,11 +6,11 @@ import { IconOutline } from '~/libs/ui' import { ScorecardGroup as ScorecardGroupModel } from '../../../../models' import { ScorecardSection } from '../ScorecardSection' import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' - -import styles from './ScorecardGroup.module.scss' import { ScorecardScore } from '../ScorecardScore' import { calcGroupScore } from '../utils' +import styles from './ScorecardGroup.module.scss' + interface ScorecardGroupProps { index: number group: ScorecardGroupModel @@ -18,11 +18,11 @@ interface ScorecardGroupProps { const ScorecardGroup: FC = props => { const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() - const allFeedbackItems = aiFeedbackItems || []; - const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext(); + const allFeedbackItems = aiFeedbackItems || [] + const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext() - const isVissible = !toggledItems[props.group.id]; - const toggle = useCallback(() => toggleItem(props.group.id), [props.group, toggleItem]); + const isVissible = !toggledItems[props.group.id] + const toggle = useCallback(() => toggleItem(props.group.id), [props.group, toggleItem]) const score = useMemo(() => ( calcGroupScore(props.group, allFeedbackItems) @@ -32,7 +32,8 @@ const ScorecardGroup: FC = props => {
- {props.index}. + {props.index} + . {props.group.name} @@ -42,7 +43,6 @@ const ScorecardGroup: FC = props => { @@ -52,7 +52,7 @@ const ScorecardGroup: FC = props => {
{isVissible && props.group.sections.map((section, index) => ( - + ))}
) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx index 001b0457a..6342883c9 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx @@ -1,12 +1,13 @@ import { FC, useMemo } from 'react' +import { IconAiReview } from '~/apps/review/src/lib/assets/icons' +import { ScorecardQuestion } from '~/apps/review/src/lib/models' + import { ScorecardViewerContextValue, useScorecardContext } from '../../ScorecardViewer.context' import { ScorecardQuestionRow } from '../ScorecardQuestionRow' +import { ScorecardScore } from '../../ScorecardScore' import styles from './AiFeedback.module.scss' -import { IconAiReview } from '~/apps/review/src/lib/assets/icons' -import { ScorecardQuestion } from '~/apps/review/src/lib/models' -import { ScorecardScore } from '../../ScorecardScore' interface AiFeedbackProps { question: ScorecardQuestion @@ -14,27 +15,28 @@ interface AiFeedbackProps { const AiFeedback: FC = props => { const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() - const feedback = useMemo(() => aiFeedbackItems?.find(r => r.scorecardQuestionId === props.question.id), [props.question.id, aiFeedbackItems]) + const feedback = useMemo(() => ( + aiFeedbackItems?.find(r => r.scorecardQuestionId === props.question.id) + ), [props.question.id, aiFeedbackItems]) if (!aiFeedbackItems?.length || !feedback) { return <> } - const isYesNo = props.question.type === 'YES_NO'; + const isYesNo = props.question.type === 'YES_NO' return ( } - index="AI Feedback" + index='AI Feedback' className={styles.wrap} - score={ + score={( - } + )} > {isYesNo && (

diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx index e0a33ae5e..fcfd40163 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx @@ -1,14 +1,14 @@ import { FC, useCallback } from 'react' +import classNames from 'classnames' import { IconOutline } from '~/libs/ui' import { ScorecardQuestion as ScorecardQuestionModel } from '../../../../models' +import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' -import styles from './ScorecardQuestion.module.scss' import { AiFeedback } from './AiFeedback' import { ScorecardQuestionRow } from './ScorecardQuestionRow' -import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' -import classNames from 'classnames' +import styles from './ScorecardQuestion.module.scss' interface ScorecardQuestionProps { index: string @@ -16,20 +16,20 @@ interface ScorecardQuestionProps { } const ScorecardQuestion: FC = props => { - const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext(); + const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext() - const isToggled = toggledItems[props.question.id!]; - const toggle = useCallback(() => toggleItem(props.question.id!), [props.question, toggleItem]); + const isToggled = toggledItems[props.question.id!] + const toggle = useCallback(() => toggleItem(props.question.id!), [props.question, toggleItem]) return (

- } + )} index={`Question ${props.index}`} className={styles.headerBar} score='' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx index bce540b25..3a6cc0d9a 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionRow/ScorecardQuestionRow.tsx @@ -1,5 +1,5 @@ -import classNames from 'classnames' import { FC, PropsWithChildren, ReactNode } from 'react' +import classNames from 'classnames' import styles from './ScorecardQuestionRow.module.scss' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx index 98f33b4a5..765954eed 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx @@ -1,16 +1,14 @@ import { FC } from 'react' import styles from './ScorecardScore.module.scss' -import { ScorecardQuestion } from '../../../../models' interface ScorecardScoreProps { score: number scaleMax: number - scaleType: ScorecardQuestion['type'] weight: number } -export const calcScore = (score: number, scaleMax: number, weight: number) => ( +export const calcScore = (score: number, scaleMax: number, weight: number): number => ( (score / (scaleMax || 1)) * weight ) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx index 26dc61b8d..0c8763103 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx @@ -15,7 +15,7 @@ interface ScorecardSectionProps { const ScorecardSection: FC = props => { const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() - const allFeedbackItems = aiFeedbackItems || []; + const allFeedbackItems = aiFeedbackItems || [] const score = useMemo(() => ( calcSectionScore(props.section, allFeedbackItems) @@ -25,7 +25,8 @@ const ScorecardSection: FC = props => {
- {props.index}. + {props.index} + . {props.section.name} @@ -35,7 +36,6 @@ const ScorecardSection: FC = props => { @@ -44,7 +44,7 @@ const ScorecardSection: FC = props => { {props.section.questions.map((question, index) => ( ))} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx index bc741215a..e5e6e6c47 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardTotal/ScorecardTotal.tsx @@ -1,27 +1,23 @@ import { FC } from 'react' -import styles from './ScorecardTotal.module.scss' import { ScorecardScore } from '../ScorecardScore' +import styles from './ScorecardTotal.module.scss' + interface ScorecardTotalProps { score?: number } -const ScorecardTotal: FC = props => { - - - return ( -
- Total Score - - -
- ) -} +const ScorecardTotal: FC = props => ( +
+ Total Score + + +
+) export default ScorecardTotal diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx index 5ba4328a4..b6cb305cf 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx @@ -1,6 +1,6 @@ -import { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react'; -import { AiFeedbackItem, Scorecard } from '../../../models'; -import { isEmpty } from 'lodash'; +import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' + +import { AiFeedbackItem, Scorecard } from '../../../models' export interface ScorecardViewerContextProps { children: ReactNode; @@ -14,39 +14,38 @@ export type ScorecardViewerContextValue = { toggleItem: (id: string) => void }; -const ScorecardViewerContext = createContext({} as ScorecardViewerContextValue); - +const ScorecardViewerContext = createContext({} as ScorecardViewerContextValue) -export function ScorecardViewerContextProvider({ - children, - aiFeedbackItems, - scorecard, - ...props -}: ScorecardViewerContextProps) { - const [toggledItems, setToggledItems] = useState<{[key: string]: boolean}>({}); +export const ScorecardViewerContextProvider: FC = props => { + const [toggledItems, setToggledItems] = useState<{[key: string]: boolean}>({}) const toggleItem = useCallback((id: string, toggle?: boolean) => { - setToggledItems((prevItems) => ({ + setToggledItems(prevItems => ({ ...prevItems, [id]: typeof toggle === 'boolean' ? toggle : !prevItems[id], })) - }, []); + }, []) // reset toggle state on scorecard change - useEffect(() => setToggledItems({}), [scorecard]); + useEffect(() => setToggledItems({}), [props.scorecard]) + + const ctxValue = useMemo(() => ({ + aiFeedbackItems: props.aiFeedbackItems, + toggledItems, + toggleItem, + }), [ + props.aiFeedbackItems, + toggledItems, + toggleItem, + ]) return ( - {children} + {props.children} - ); -}; + ) +} -export const useScorecardContext = () => useContext(ScorecardViewerContext); +export const useScorecardContext = (): ScorecardViewerContextValue => useContext(ScorecardViewerContext) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index 77c989a59..4c4352273 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -1,10 +1,11 @@ import { FC } from 'react' -import styles from './ScorecardViewer.module.scss' import { AiFeedbackItem, Scorecard } from '../../../models' + import { ScorecardGroup } from './ScorecardGroup' import { ScorecardViewerContextProvider } from './ScorecardViewer.context' import { ScorecardTotal } from './ScorecardTotal' +import styles from './ScorecardViewer.module.scss' interface ScorecardViewerProps { scorecard: Scorecard @@ -12,21 +13,18 @@ interface ScorecardViewerProps { score?: number } -const ScorecardViewer: FC = props => { - - return ( -
- - {props.scorecard.scorecardGroups.map((group, index) => ( - - ))} - - -
- ) -} +const ScorecardViewer: FC = props => ( +
+ + {props.scorecard.scorecardGroups.map((group, index) => ( + + ))} + + +
+) export default ScorecardViewer 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 6f566fea9..5e8379cf2 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx @@ -8,11 +8,11 @@ import { PageWrapper } from '../../../lib' import { useAiScorecardContext } from '../AiScorecardContext' import { AiScorecardContextModel } from '../../../lib/models' import { AiWorkflowsSidebar } from '../components/AiWorkflowsSidebar' - -import styles from './AiScorecardViewer.module.scss' import { ScorecardViewer } from '../../../lib/components/Scorecard' import { AiWorkflowRunItemsResponse, useFetchAiWorkflowsRunItems } from '../../../lib/hooks' +import styles from './AiScorecardViewer.module.scss' + const AiScorecardViewer: FC = () => { const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() const { challengeInfo, scorecard, workflowId, workflowRun }: AiScorecardContextModel = useAiScorecardContext() From be10e1956fa9c588af9526bb5d78d81b6b753251 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 7 Nov 2025 09:08:07 +0200 Subject: [PATCH 033/125] PM-1906 - mobile ui --- .../ScorecardGroup/ScorecardGroup.module.scss | 11 ++++++ .../ScorecardGroup/ScorecardGroup.tsx | 4 +-- .../AiFeedback/AiFeedback.module.scss | 4 +++ .../ScorecardQuestionRow.module.scss | 36 +++++++++++++++++++ .../ScorecardQuestionRow.tsx | 6 ++-- .../ScorecardScore/ScorecardScore.module.scss | 4 +++ .../ScorecardSection.module.scss | 10 ++++++ .../ScorecardSection/ScorecardSection.tsx | 6 ++-- .../ScorecardViewer.module.scss | 19 ++++++++++ .../ScorecardViewer/ScorecardViewer.tsx | 21 +++++++++++ 10 files changed, 113 insertions(+), 8 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss index 320c5f8d8..f642ce0ea 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.module.scss @@ -36,6 +36,17 @@ } } } + + @include ltemd { + flex-wrap: wrap; + row-gap: $sp-2; + + .score { + order: 7; + width: 100%; + margin-left: $sp-10; + } + } } .index { diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx index ccccda9ad..cac45c094 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx @@ -35,11 +35,11 @@ const ScorecardGroup: FC = props => { {props.index} . - + {props.group.name} - + = props => (
- {props.icon} - {props.index} + {props.icon} + {props.index} {props.children} - {props.score} + {props.score}
) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss index 7ca015ccd..5e0763c09 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.module.scss @@ -8,4 +8,8 @@ margin-left: $sp-1; opacity: 0.75; } + + @include ltemd { + text-align: left; + } } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss index 3d73dc829..dbb4d4482 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.module.scss @@ -18,6 +18,16 @@ color: #0A0A0A; padding-right: 56px; + @include ltemd { + flex-wrap: wrap; + row-gap: $sp-2; + + .score { + order: 7; + width: 100%; + margin-left: $sp-10; + } + } } .mx { diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx index 0c8763103..1f26a9af6 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx @@ -24,15 +24,15 @@ const ScorecardSection: FC = props => { return (
- + {props.index} . - + {props.section.name} - + strong { + font-size: 16px; + line-height: 22px; + + margin-bottom: $sp-2; + } + + p { + font-size: 14px; + line-height: 20px; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index 4c4352273..fa5757c50 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -19,6 +19,27 @@ const ScorecardViewer: FC = props => ( scorecard={props.scorecard} aiFeedbackItems={props.aiFeedback} > + {!!props.score && ( +
+ Conclusion +

+ Congratulations! You earned a score of + {' '} + + {props.score.toFixed(2)} + + {' '} + out of the maximum of + {' '} + + {props.scorecard.maxScore.toFixed(2)} + + . + You did a good job on passing the scorecard criteria. + Please check the below sections to see if there is any place for improvement. +

+
+ )} {props.scorecard.scorecardGroups.map((group, index) => ( ))} From 2b428d0b94743eefaa7ffcd217c131eaf11a2854 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 7 Nov 2025 09:28:21 +0200 Subject: [PATCH 034/125] PM-1906 - tabs --- .../AiReviewsTable/AiReviewsTable.tsx | 2 +- .../ScorecardAttachments.module.scss | 0 .../ScorecardAttachments.tsx | 17 ++++++++++++ .../Scorecard/ScorecardAttachments/index.ts | 1 + .../ScorecardTabsLayout.module.scss | 0 .../ScorecardTabsLayout.tsx | 16 ++++++++++++ .../Scorecard/ScorecardTabsLayout/index.ts | 1 + .../src/lib/components/Scorecard/index.ts | 1 + .../AiScorecardViewer.module.scss | 6 +++++ .../AiScorecardViewer/AiScorecardViewer.tsx | 26 ++++++++++++++++--- 10 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/index.ts diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index 06114c48e..6f688f723 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -144,7 +144,7 @@ const AiReviewsTable: FC = props => { {run.status === 'SUCCESS' ? ( run.workflow.scorecard ? ( - {run.score} + {run.score} ) : run.score ) : '-'} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx new file mode 100644 index 000000000..57126689a --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx @@ -0,0 +1,17 @@ +import { FC } from 'react' + +import styles from './ScorecardAttachments.module.scss' + +interface ScorecardAttachmentsProps { +} + +const ScorecardAttachments: FC = props => { + + return ( +
+ attachments +
+ ) +} + +export default ScorecardAttachments diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/index.ts new file mode 100644 index 000000000..a360be3f0 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/index.ts @@ -0,0 +1 @@ +export { default as ScorecardAttachments } from './ScorecardAttachments' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.module.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.tsx new file mode 100644 index 000000000..7f6aae302 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.tsx @@ -0,0 +1,16 @@ +import { FC } from 'react' + +import styles from './ScorecardTabsLayout.module.scss' + +interface ScorecardTabsLayoutProps { +} + +const ScorecardTabsLayout: FC = props => { + + return ( +
+
+ ) +} + +export default ScorecardTabsLayout diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/index.ts new file mode 100644 index 000000000..852d8b38b --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/index.ts @@ -0,0 +1 @@ +export { default as ScorecardTabsLayout } from './ScorecardTabsLayout' diff --git a/src/apps/review/src/lib/components/Scorecard/index.ts b/src/apps/review/src/lib/components/Scorecard/index.ts index 2a3dc98a0..ede5e8975 100644 --- a/src/apps/review/src/lib/components/Scorecard/index.ts +++ b/src/apps/review/src/lib/components/Scorecard/index.ts @@ -1 +1,2 @@ export * from './ScorecardViewer' +export * from './ScorecardTabsLayout' 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 53a8cd26f..1b3b36e66 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 @@ -26,6 +26,12 @@ } } } + .contentWrap { width: 100%; } + +.tabs { + justify-content: center; + margin: $sp-6 0; +} 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 5e8379cf2..36e010807 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx @@ -1,17 +1,23 @@ -import { FC, useEffect, useMemo } from 'react' +import { FC, useEffect, useMemo, useState } from 'react' import { NotificationContextType, useNotification } from '~/libs/shared' import { ScorecardHeader } from '../components/ScorecardHeader' import { IconAiReview } from '../../../lib/assets/icons' -import { PageWrapper } from '../../../lib' +import { PageWrapper, Tabs } from '../../../lib' import { useAiScorecardContext } from '../AiScorecardContext' -import { AiScorecardContextModel } from '../../../lib/models' +import { AiScorecardContextModel, SelectOption } from '../../../lib/models' import { AiWorkflowsSidebar } from '../components/AiWorkflowsSidebar' import { ScorecardViewer } from '../../../lib/components/Scorecard' import { AiWorkflowRunItemsResponse, useFetchAiWorkflowsRunItems } from '../../../lib/hooks' import styles from './AiScorecardViewer.module.scss' +import { ScorecardAttachments } from '../../../lib/components/Scorecard/ScorecardAttachments' + +const tabItems: SelectOption[] = [ + { label: 'Scorecard', value: 'scorecard'}, + { label: 'Attachments', value: 'attachments' }, +] const AiScorecardViewer: FC = () => { const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() @@ -23,6 +29,8 @@ const AiScorecardViewer: FC = () => { [], ) + const [selectedTab, setSelectedTab] = useState('scorecard'); + useEffect(() => { const notification = showBannerNotification({ icon: , @@ -43,13 +51,23 @@ const AiScorecardViewer: FC = () => {
- {!!scorecard && ( + + {!!scorecard && selectedTab === 'scorecard' && ( )} + + {selectedTab === 'attachments' && ( + + )}
From cf5bb7a239efc8475063ab3a3da7f1eedfda7c29 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Fri, 7 Nov 2025 09:42:02 +0200 Subject: [PATCH 035/125] Lint fixes --- .../components/AiReviewsTable/AiReviewsTable.tsx | 6 +++++- .../ScorecardAttachments.tsx | 16 +++++++--------- .../ScorecardTabsLayout.module.scss | 0 .../ScorecardTabsLayout/ScorecardTabsLayout.tsx | 16 ---------------- .../Scorecard/ScorecardTabsLayout/index.ts | 1 - .../review/src/lib/components/Scorecard/index.ts | 1 - .../AiScorecardViewer/AiScorecardViewer.tsx | 6 +++--- 7 files changed, 15 insertions(+), 31 deletions(-) delete mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.module.scss delete mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.tsx delete mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/index.ts diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index 6f688f723..e3fd0b774 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -144,7 +144,11 @@ const AiReviewsTable: FC = props => { {run.status === 'SUCCESS' ? ( run.workflow.scorecard ? ( - {run.score} + + {run.score} + ) : run.score ) : '-'} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx index 57126689a..a0091861c 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx @@ -1,17 +1,15 @@ import { FC } from 'react' -import styles from './ScorecardAttachments.module.scss' +// import styles from './ScorecardAttachments.module.scss' interface ScorecardAttachmentsProps { + className?: string } -const ScorecardAttachments: FC = props => { - - return ( -
- attachments -
- ) -} +const ScorecardAttachments: FC = props => ( +
+ attachments +
+) export default ScorecardAttachments diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.module.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.tsx deleted file mode 100644 index 7f6aae302..000000000 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/ScorecardTabsLayout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { FC } from 'react' - -import styles from './ScorecardTabsLayout.module.scss' - -interface ScorecardTabsLayoutProps { -} - -const ScorecardTabsLayout: FC = props => { - - return ( -
-
- ) -} - -export default ScorecardTabsLayout diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/index.ts deleted file mode 100644 index 852d8b38b..000000000 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardTabsLayout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ScorecardTabsLayout } from './ScorecardTabsLayout' diff --git a/src/apps/review/src/lib/components/Scorecard/index.ts b/src/apps/review/src/lib/components/Scorecard/index.ts index ede5e8975..2a3dc98a0 100644 --- a/src/apps/review/src/lib/components/Scorecard/index.ts +++ b/src/apps/review/src/lib/components/Scorecard/index.ts @@ -1,2 +1 @@ export * from './ScorecardViewer' -export * from './ScorecardTabsLayout' 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 36e010807..7b27c1fef 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx @@ -10,12 +10,12 @@ import { AiScorecardContextModel, SelectOption } from '../../../lib/models' import { AiWorkflowsSidebar } from '../components/AiWorkflowsSidebar' import { ScorecardViewer } from '../../../lib/components/Scorecard' import { AiWorkflowRunItemsResponse, useFetchAiWorkflowsRunItems } from '../../../lib/hooks' +import { ScorecardAttachments } from '../../../lib/components/Scorecard/ScorecardAttachments' import styles from './AiScorecardViewer.module.scss' -import { ScorecardAttachments } from '../../../lib/components/Scorecard/ScorecardAttachments' const tabItems: SelectOption[] = [ - { label: 'Scorecard', value: 'scorecard'}, + { label: 'Scorecard', value: 'scorecard' }, { label: 'Attachments', value: 'attachments' }, ] @@ -29,7 +29,7 @@ const AiScorecardViewer: FC = () => { [], ) - const [selectedTab, setSelectedTab] = useState('scorecard'); + const [selectedTab, setSelectedTab] = useState('scorecard') useEffect(() => { const notification = showBannerNotification({ From 65bcad6829b9c155dff183eafc975fced21adf4a Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 7 Nov 2025 21:02:06 +0100 Subject: [PATCH 036/125] feat: added ai workflow list component to create or update default reviewer form --- .../DefaultReviewersAddForm.tsx | 73 ++++++++-------- .../models/DefaultChallengeReviewer.model.ts | 2 + .../src/lib/services/ai-workflows.service.ts | 12 +++ .../admin/src/lib/utils/validation-schemas.ts | 2 + yarn.lock | 83 +++++++------------ 5 files changed, 84 insertions(+), 88 deletions(-) create mode 100644 src/apps/admin/src/lib/services/ai-workflows.service.ts diff --git a/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx b/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx index 80d7218bc..97f42e6b5 100644 --- a/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx +++ b/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx @@ -1,17 +1,7 @@ -import { - useCallback, - useEffect, - useMemo, - useState, -} from 'react' import type { FC } from 'react' -import { useNavigate, useParams } from 'react-router-dom' -import type { NavigateFunction } from 'react-router-dom' -import { Controller, useForm } from 'react-hook-form' -import type { - ControllerRenderProps, - UseFormReturn, -} from 'react-hook-form' +import { useCallback, useEffect, useMemo, useState } from 'react' +import { NavigateFunction, useNavigate, useParams } from 'react-router-dom' +import { Controller, ControllerRenderProps, useForm, UseFormReturn } from 'react-hook-form' import _ from 'lodash' import classNames from 'classnames' @@ -25,13 +15,14 @@ import { LinkButton, } from '~/libs/ui' -import { FormAddWrapper } from '../common/FormAddWrapper' -import { FormAddDefaultReviewer } from '../../models' -import { formAddDefaultReviewerSchema } from '../../utils' import { useManageAddDefaultReviewer, useManageAddDefaultReviewerProps, } from '../../hooks' +import { FormAddWrapper } from '../common/FormAddWrapper' +import { FormAddDefaultReviewer } from '../../models' +import { formAddDefaultReviewerSchema } from '../../utils' +import { getAiWorkflows } from '../../services/ai-workflows.service' import styles from './DefaultReviewersAddForm.module.scss' @@ -127,6 +118,7 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { formState: { errors, isDirty }, }: UseFormReturn = useForm({ defaultValues: { + aiWorkflowId: '', baseCoefficient: 0, fixedAmount: 0, incrementalCoefficient: 0, @@ -165,11 +157,22 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { [doAddDefaultReviewer, doUpdateDefaultReviewer, isEdit, navigate], ) + const [aiWorkflows, setAiWorkflows] = useState<{ label: string; value: string }[]>([]) + + useEffect(() => { + getAiWorkflows() + .then((workflows: { id: string; name: string }[]) => { + const options = workflows.map((wf: { id: string; name: string }) => ({ label: wf.name, value: wf.id })) + setAiWorkflows(options) + }) + }, []) + const isMemberReview = watch('isMemberReview') useEffect(() => { if (defaultReviewerInfo) { reset({ + aiWorkflowId: defaultReviewerInfo.aiWorkflowId ?? '', baseCoefficient: defaultReviewerInfo.baseCoefficient ?? 0, fixedAmount: defaultReviewerInfo.fixedAmount ?? 0, incrementalCoefficient: defaultReviewerInfo.incrementalCoefficient ?? 0, @@ -460,17 +463,18 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { ) }} /> +
- }) { + field: ControllerRenderProps + }) { return ( = (props: Props) => { }} />
-
+ {!isMemberReview && ( + field: ControllerRenderProps }) { return ( - ) }} /> -
+ )} { + const response = await xhrGetAsync(`${EnvironmentConfig.API.V6}/workflows`) + return response +} diff --git a/src/apps/admin/src/lib/utils/validation-schemas.ts b/src/apps/admin/src/lib/utils/validation-schemas.ts index d458e795e..9fc3cbb71 100644 --- a/src/apps/admin/src/lib/utils/validation-schemas.ts +++ b/src/apps/admin/src/lib/utils/validation-schemas.ts @@ -20,6 +20,8 @@ export const formSearchDefaultReviewersSchema: Yup.ObjectSchema = Yup.object({ + aiWorkflowId: Yup.string() + .optional(), baseCoefficient: Yup.number() .optional() .min(0, 'Must be non-negative'), diff --git a/yarn.lock b/yarn.lock index 534b04b9b..d7ceec7c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5741,17 +5741,18 @@ axios@*, axios@^1.12.0, axios@^1.7.4: form-data "^4.0.4" proxy-from-env "^1.1.0" -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== dependencies: - follow-redirects "^1.14.7" + follow-redirects "^1.14.9" + form-data "^4.0.0" -axobject-query@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== +axobject-query@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" @@ -8291,7 +8292,7 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: dependencies: es-errors "^1.3.0" -es-set-tostringtag@^2.1.0: +es-set-tostringtag@^2.0.3, es-set-tostringtag@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== @@ -8301,16 +8302,6 @@ es-set-tostringtag@^2.1.0: has-tostringtag "^1.0.2" hasown "^2.0.2" -es-shim-unscopables@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== - dependencies: - es-errors "^1.3.0" - get-intrinsic "^1.2.6" - has-tostringtag "^1.0.2" - hasown "^2.0.2" - es-shim-unscopables@^1.0.2, es-shim-unscopables@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz#438df35520dac5d105f3943d927549ea3b00f4b5" @@ -9464,15 +9455,27 @@ flux-standard-action@^2.0.3: lodash.isplainobject "^4.0.6" lodash.isstring "^4.0.1" -follow-redirects@^1.0.0, follow-redirects@^1.14.7, follow-redirects@^1.15.6: +follow-redirects@^1.0.0, follow-redirects@^1.15.6: version "1.15.9" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== +follow-redirects@^1.14.9, follow-redirects@^1.15.2: + version "1.15.11" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" + integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== dependencies: is-callable "^1.2.7" @@ -10009,13 +10012,6 @@ has-symbols@^1.0.1, has-symbols@^1.0.3, has-symbols@^1.1.0: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - has-tostringtag@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" @@ -10023,13 +10019,6 @@ has-tostringtag@^1.0.2: dependencies: has-symbols "^1.0.3" -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - has-symbols "^1.0.3" - hasha@^5.0.0: version "5.2.2" resolved "https://registry.yarnpkg.com/hasha/-/hasha-5.2.2.tgz#a48477989b3b327aea3c04f53096d816d97522a1" @@ -17584,7 +17573,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17598,13 +17587,6 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" @@ -17974,9 +17956,9 @@ tar@^6.2.1: mkdirp "^1.0.3" yallist "^4.0.0" -tc-auth-lib@topcoder-platform/tc-auth-lib#1.0.27: +tc-auth-lib@topcoder-platform/tc-auth-lib#master: version "1.0.2" - resolved "https://codeload.github.com/topcoder-platform/tc-auth-lib/tar.gz/dc5b3a29ac3b8e2a0f386fce411c6533c2f33f05" + resolved "https://codeload.github.com/topcoder-platform/tc-auth-lib/tar.gz/1c9be61eb32583beeb74f596fe58bb3ada97462d" dependencies: lodash "^4.17.19" @@ -19641,7 +19623,7 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19659,15 +19641,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 5eee5b17d0cc3d8e8604d32dbb0d40a1f13d982f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 10 Nov 2025 14:52:20 +0200 Subject: [PATCH 037/125] PM-2178_merge-review-ui-with-ai-review --- .../src/lib/assets/icons/icon-comment.svg | 3 + src/apps/review/src/lib/assets/icons/index.ts | 2 + .../ScorecardGroup/ScorecardGroup.tsx | 22 +- .../AiFeedback/AiFeedback.tsx | 3 +- .../ReviewAnswer/ReviewAnswer.module.scss | 7 + .../ReviewAnswer/ReviewAnswer.tsx | 114 ++++++ .../ReviewResponse/ReviewAnswer/index.ts | 1 + .../ReviewAppeal/ReviewAppeal.module.scss | 112 ++++++ .../ReviewAppeal/ReviewAppeal.tsx | 247 ++++++++++++ .../ReviewResponse/ReviewAppeal/index.ts | 1 + .../ReviewComment/ReviewComment.module.scss | 109 ++++++ .../ReviewComment/ReviewComment.tsx | 184 +++++++++ .../ReviewResponse/ReviewComment/index.ts | 1 + .../ReviewComments/ReviewComments.tsx | 57 +++ .../ReviewResponse/ReviewComments/index.ts | 1 + .../ReviewManagerComment.module.scss | 97 +++++ .../ReviewManagerComment.tsx | 227 +++++++++++ .../ReviewManagerComment/index.ts | 1 + .../ScorecardQuestion.module.scss | 4 - .../ScorecardQuestion/ScorecardQuestion.tsx | 97 +++-- .../ScorecardQuestionEdit.module.scss | 90 +++++ .../ScorecardQuestionEdit.tsx | 340 +++++++++++++++++ .../ScorecardQuestionEdit/index.ts | 3 + .../ScorecardQuestionRow.tsx | 2 +- .../ScorecardQuestionView.module.scss | 29 ++ .../ScorecardQuestionView.tsx | 83 ++++ .../ScorecardQuestionView/index.ts | 3 + .../ScorecardSection/ScorecardSection.tsx | 16 +- .../ScorecardViewer.context.tsx | 117 +++++- .../ScorecardViewer.module.scss | 43 +++ .../ScorecardViewer/ScorecardViewer.tsx | 358 ++++++++++++++++-- .../ScorecardViewer/hooks/useReviewForm.ts | 174 +++++++++ .../Scorecard/ScorecardViewer/utils.ts | 51 ++- src/apps/review/src/lib/models/index.ts | 1 + 34 files changed, 2521 insertions(+), 79 deletions(-) create mode 100644 src/apps/review/src/lib/assets/icons/icon-comment.svg create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts diff --git a/src/apps/review/src/lib/assets/icons/icon-comment.svg b/src/apps/review/src/lib/assets/icons/icon-comment.svg new file mode 100644 index 000000000..13268f759 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-comment.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 80589e44d..b5409968d 100644 --- a/src/apps/review/src/lib/assets/icons/index.ts +++ b/src/apps/review/src/lib/assets/icons/index.ts @@ -12,6 +12,7 @@ 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' +import { ReactComponent as IconComment } from './icon-comment.svg' export * from './editor/bold' export * from './editor/code' @@ -43,6 +44,7 @@ export { IconDeepseekAi, IconClock, IconPremium, + IconComment, } export const phasesIcons = { diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx index cac45c094..d9e3ae6c6 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx @@ -7,13 +7,20 @@ import { ScorecardGroup as ScorecardGroupModel } from '../../../../models' import { ScorecardSection } from '../ScorecardSection' import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' import { ScorecardScore } from '../ScorecardScore' -import { calcGroupScore } from '../utils' +import { calcGroupScore, createReviewItemMapping } from '../utils' import styles from './ScorecardGroup.module.scss' interface ScorecardGroupProps { index: number group: ScorecardGroupModel + reviewItemMapping?: ReturnType + formControl?: any + formErrors?: any + formIsTouched?: { [key: string]: boolean } + formSetIsTouched?: any + formTrigger?: any + recalculateReviewProgress?: () => void } const ScorecardGroup: FC = props => { @@ -52,7 +59,18 @@ const ScorecardGroup: FC = props => {
{isVissible && props.group.sections.map((section, index) => ( - + ))}
) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx index 6342883c9..ff7964ffd 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx @@ -8,6 +8,7 @@ import { ScorecardQuestionRow } from '../ScorecardQuestionRow' import { ScorecardScore } from '../../ScorecardScore' import styles from './AiFeedback.module.scss' +import { MarkdownReview } from '../../../../MarkdownReview' interface AiFeedbackProps { question: ScorecardQuestion @@ -43,7 +44,7 @@ const AiFeedback: FC = props => { {feedback.questionScore ? 'Yes' : 'No'}

)} - {feedback.content} + ) } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.module.scss new file mode 100644 index 000000000..8436a7aa2 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.module.scss @@ -0,0 +1,7 @@ +.wrap { +} + +.select { + min-width: 200px; +} + diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.tsx new file mode 100644 index 000000000..86abb54a2 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.tsx @@ -0,0 +1,114 @@ +import { FC, useCallback, useMemo, useState } from 'react' +import Select, { SingleValue } from 'react-select' + +import { ReviewItemInfo, ScorecardQuestion, SelectOption } from '../../../../../../models' +import { QUESTION_YES_NO_OPTIONS } from '../../../../../../../config/index.config' +import { ScorecardViewerContextValue, useScorecardContext } from '../../../ScorecardViewer.context' +import { ScorecardQuestionRow } from '../../ScorecardQuestionRow' +import { ScorecardScore } from '../../../ScorecardScore' + +import styles from './ReviewAnswer.module.scss' + +interface ReviewAnswerProps { + question: ScorecardQuestion + reviewItem: ReviewItemInfo +} + +const ReviewAnswer: FC = props => { + const { + isManagerEdit, + isSavingManagerComment, + addManagerComment, + }: ScorecardViewerContextValue = useScorecardContext() + + const answer = useMemo(() => ( + props.reviewItem.finalAnswer || props.reviewItem.initialAnswer || '' + ), [props.reviewItem.finalAnswer, props.reviewItem.initialAnswer]) + + const [selectedScore, setSelectedScore] = useState(answer) + const [showManagerCommentForm, setShowManagerCommentForm] = useState(false) + + const responseOptions = useMemo(() => { + if (props.question.type === 'SCALE') { + const length = props.question.scaleMax - props.question.scaleMin + 1 + return Array.from( + new Array(length), + (x, i) => `${i + props.question.scaleMin}`, + ).map(item => ({ + label: item, + value: item, + })) + } + + if (props.question.type === 'YES_NO') { + return QUESTION_YES_NO_OPTIONS + } + + return [] + }, [props.question]) + + const handleScoreChange = useCallback((option: SingleValue) => { + const nextValue = (option as SelectOption | null)?.value ?? '' + setSelectedScore(nextValue) + + if (nextValue && nextValue !== answer && addManagerComment) { + setShowManagerCommentForm(true) + } else { + setShowManagerCommentForm(false) + } + }, [answer, addManagerComment]) + + const score = useMemo(() => { + const currentAnswer = selectedScore || answer + if (props.question.type === 'YES_NO') { + return currentAnswer === 'Yes' ? 1 : 0 + } + if (props.question.type === 'SCALE' && currentAnswer) { + const answerNum = parseInt(currentAnswer, 10) + const totalPoint = props.question.scaleMax - props.question.scaleMin + if (totalPoint > 0 && !Number.isNaN(answerNum)) { + return (answerNum - props.question.scaleMin) / totalPoint + } + } + return 0 + }, [selectedScore, answer, props.question]) + + const selectedOption = useMemo(() => ( + responseOptions.find(opt => opt.value === (selectedScore || answer)) + ), [responseOptions, selectedScore, answer]) + + if (!answer && !isManagerEdit) { + return <> + } + + return ( + + )} + > + {isManagerEdit && responseOptions.length > 0 ? ( + ) => { + setUpdatedResponse(option) + }} + isDisabled={isSavingAppealResponse} + /> + )} +
+ + }) { + return ( + + ) + }} + /> +
+ + +
+ + )} +
+ ) +} + +export default ReviewAppeal + diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/index.ts new file mode 100644 index 000000000..27195d6aa --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/index.ts @@ -0,0 +1 @@ +export { default as ReviewAppeal } from './ReviewAppeal' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.module.scss new file mode 100644 index 000000000..bbc3f31ab --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.module.scss @@ -0,0 +1,109 @@ +.wrap { + display: flex; + flex-direction: column; + gap: 4px; +} + +.commentRow { +} + +.content { +} + +.appealActions { + display: flex; + gap: 8px; + margin-top: 8px; + margin-left: 16px; +} + +.addAppealButton, +.editAppealButton, +.deleteAppealButton { + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.addAppealButton, +.editAppealButton { + background-color: #ffc107; + color: #000; + + &:hover:not(:disabled) { + background-color: #e0a800; + } +} + +.deleteAppealButton { + background-color: #dc3545; + color: white; + + &:hover:not(:disabled) { + background-color: #c82333; + } +} + +.appealForm { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 8px; + margin-left: 16px; + padding: 12px; + border: 1px solid #e0e0e0; + border-radius: 4px; + background-color: #fff3cd; +} + +.appealFormLabel { + font-weight: bold; + font-size: 14px; +} + +.markdownEditor { +} + +.appealFormActions { + display: flex; + gap: 8px; +} + +.submitButton, +.cancelButton { + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.submitButton { + background-color: #28a745; + color: white; + + &:hover:not(:disabled) { + background-color: #218838; + } +} + +.cancelButton { + background-color: #6c757d; + color: white; + + &:hover:not(:disabled) { + background-color: #5a6268; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx new file mode 100644 index 000000000..46d5ef014 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx @@ -0,0 +1,184 @@ +import { FC, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { + Controller, + ControllerRenderProps, + useForm, + UseFormReturn, +} from 'react-hook-form' +import { get } from 'lodash' + +import { yupResolver } from '@hookform/resolvers/yup' + +import { AppealInfo, ChallengeDetailContextModel, FormAppealResponse } from '../../../../../../models' +import { ReviewItemComment } from '../../../../../../models/ReviewItemComment.model' +import { formAppealResponseSchema, isAppealsPhase } from '../../../../../../utils' +import { ChallengeDetailContext } from '../../../../../../contexts' +import { FieldMarkdownEditor } from '../../../../../../components/FieldMarkdownEditor' +import { ScorecardViewerContextValue, useScorecardContext } from '../../../ScorecardViewer.context' +import { ScorecardQuestionRow } from '../../ScorecardQuestionRow' + +import styles from './ReviewComment.module.scss' + +interface ReviewCommentProps { + comment: ReviewItemComment + appeal?: AppealInfo + index: number +} + +const ReviewComment: FC = props => { + const { + actionChallengeRole, + addAppeal, + doDeleteAppeal, + isSavingAppeal, + }: ScorecardViewerContextValue = useScorecardContext() + + const { challengeInfo }: ChallengeDetailContextModel = useContext( + ChallengeDetailContext, + ) + const canAddAppeal = useMemo(() => isAppealsPhase(challengeInfo), [challengeInfo]) + const isSubmitter = actionChallengeRole === 'Submitter' + + const [appealContent, setAppealContent] = useState(props.appeal?.content || '') + const [showAppealForm, setShowAppealForm] = useState(false) + + const { + handleSubmit, + control, + formState: { errors }, + }: UseFormReturn = useForm({ + defaultValues: { + response: '', + }, + mode: 'all', + resolver: yupResolver(formAppealResponseSchema), + }) + + const onSubmit = useCallback((data: FormAppealResponse) => { + if (addAppeal) { + addAppeal(data.response, props.comment, () => { + setAppealContent(data.response) + setShowAppealForm(false) + }) + } + }, [addAppeal, props.comment]) + + useEffect(() => { + if (props.appeal) { + setAppealContent(props.appeal.content) + } + }, [props.appeal]) + + const typeDisplay = props.comment.typeDisplay || props.comment.type + + return ( +
+ +
+ {props.comment.content} +
+
+ + {isSubmitter && canAddAppeal && ( + <> + {!props.appeal && !showAppealForm && ( +
+ +
+ )} + + {props.appeal && ( +
+ + +
+ )} + + {showAppealForm && ( +
+ + + }) { + return ( + + ) + }} + /> +
+ + +
+ + )} + + )} +
+ ) +} + +export default ReviewComment + diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/index.ts new file mode 100644 index 000000000..4fe1d3daf --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/index.ts @@ -0,0 +1 @@ +export { default as ReviewComment } from './ReviewComment' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx new file mode 100644 index 000000000..b2355a9d8 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx @@ -0,0 +1,57 @@ +import { FC, useMemo } from 'react' + +import { MappingAppeal, ReviewItemInfo, ScorecardQuestion } from '../../../../../../models' + +import ReviewAppeal from '../ReviewAppeal/ReviewAppeal' +import ReviewComment from '../ReviewComment/ReviewComment' +import ReviewManagerComment from '../ReviewManagerComment/ReviewManagerComment' + +// import styles from './ReviewComments.module.scss' + +interface ReviewCommentsProps { + question: ScorecardQuestion + reviewItem: ReviewItemInfo + mappingAppeals?: MappingAppeal +} + +const ReviewComments: FC = props => { + const comments = useMemo(() => ( + (props.reviewItem.reviewItemComments || []).filter(c => c.content || c.appeal || props.mappingAppeals?.[c.id]) + ).sort((a, b) => a.sortOrder - b.sortOrder), [props.reviewItem.reviewItemComments, props.mappingAppeals]) + + if (!comments.length && !props.reviewItem.managerComment) { + return <> + } + + return ( +
+ {comments.map((comment, index) => { + const appeal = props.mappingAppeals?.[comment.id] ?? comment.appeal + return ( +
+ + {appeal && ( + + )} +
+ ) + })} + +
+ ) +} + +export default ReviewComments + diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/index.ts new file mode 100644 index 000000000..c32957809 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/index.ts @@ -0,0 +1 @@ +export { default as ReviewComments } from './ReviewComments' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.module.scss new file mode 100644 index 000000000..689274a54 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.module.scss @@ -0,0 +1,97 @@ +.wrap { +} + +.content { +} + +.displayContainer { + display: flex; + flex-direction: column; + gap: 8px; +} + +.editButton, +.addButton { + align-self: flex-start; + padding: 6px 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + margin-top: 8px; + + &:hover:not(:disabled) { + background-color: #0056b3; + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.commentForm { + display: flex; + flex-direction: column; + gap: 8px; +} + +.commentFormHeader { + display: flex; + flex-direction: column; + gap: 8px; +} + +.commentFormLabel { + font-weight: bold; + font-size: 14px; +} + +.scoreSelect { + min-width: 200px; +} + +.select { +} + +.markdownEditor { +} + +.commentFormActions { + display: flex; + gap: 8px; +} + +.submitButton, +.cancelButton { + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.submitButton { + background-color: #28a745; + color: white; + + &:hover:not(:disabled) { + background-color: #218838; + } +} + +.cancelButton { + background-color: #6c757d; + color: white; + + &:hover:not(:disabled) { + background-color: #5a6268; + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.tsx new file mode 100644 index 000000000..2dfe745f3 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.tsx @@ -0,0 +1,227 @@ +import { FC, useCallback, useEffect, useMemo, useState } from 'react' +import { + Controller, + ControllerRenderProps, + useForm, + UseFormReturn, +} from 'react-hook-form' +import Select, { SingleValue } from 'react-select' +import classNames from 'classnames' + +import { yupResolver } from '@hookform/resolvers/yup' + +import { FormManagerComment, ReviewItemInfo, ScorecardQuestion, SelectOption } from '../../../../../../models' +import { formManagerCommentSchema } from '../../../../../../utils' +import { QUESTION_YES_NO_OPTIONS } from '../../../../../../../config/index.config' +import { MarkdownReview } from '../../../../../../components/MarkdownReview' +import { FieldMarkdownEditor } from '../../../../../../components/FieldMarkdownEditor' +import { ScorecardViewerContextValue, useScorecardContext } from '../../../ScorecardViewer.context' +import { ScorecardQuestionRow } from '../../ScorecardQuestionRow' + +import styles from './ReviewManagerComment.module.scss' + +interface ReviewManagerCommentProps { + managerComment?: string + reviewItem?: ReviewItemInfo + scorecardQuestion?: ScorecardQuestion +} + +const ReviewManagerComment: FC = props => { + const { + isManagerEdit, + isSavingManagerComment, + addManagerComment, + }: ScorecardViewerContextValue = useScorecardContext() + + const [comment, setComment] = useState(props.managerComment || '') + const [showCommentForm, setShowCommentForm] = useState(false) + + const responseOptions = useMemo(() => { + if (!props.scorecardQuestion) { + return [] + } + + if (props.scorecardQuestion.type === 'SCALE') { + const length = props.scorecardQuestion.scaleMax - props.scorecardQuestion.scaleMin + 1 + return Array.from( + new Array(length), + (x, i) => `${i + props.scorecardQuestion!.scaleMin}`, + ).map(item => ({ + label: item, + value: item, + })) + } + + if (props.scorecardQuestion.type === 'YES_NO') { + return QUESTION_YES_NO_OPTIONS + } + + return [] + }, [props.scorecardQuestion]) + + const { + handleSubmit, + control, + formState: { errors }, + }: UseFormReturn = useForm({ + defaultValues: { + finalScore: '', + response: '', + }, + mode: 'all', + resolver: yupResolver(formManagerCommentSchema), + }) + + const onSubmit = useCallback((data: FormManagerComment) => { + if (addManagerComment && props.reviewItem) { + addManagerComment( + data.response, + data.finalScore, + props.reviewItem, + () => { + setComment(data.response) + setShowCommentForm(false) + }, + ) + } + }, [addManagerComment, props.reviewItem]) + + useEffect(() => { + if (props.managerComment) { + setComment(props.managerComment) + } + }, [props.managerComment]) + + if (!props.managerComment && !isManagerEdit) { + return <> + } + + return ( + + {!showCommentForm && comment && ( +
+
+ +
+ {isManagerEdit && ( + + )} +
+ )} + + {!showCommentForm && !comment && isManagerEdit && ( + + )} + + {showCommentForm && isManagerEdit && ( +
+
+ + {props.scorecardQuestion && responseOptions.length > 0 && ( +
+ + }) { + return ( + ) { + controlProps.field.onChange( + (option as SelectOption).value, + ) + props.recalculateReviewProgress() + }} + onBlur={function onBlur() { + controlProps.field.onBlur() + props.setIsTouched(old => ({ + ...old, + [`reviews.${props.fieldIndex}.initialAnswer.message`]: true, + })) + props.trigger( + `reviews.${props.fieldIndex}.initialAnswer`, + ) + }} + isDisabled={props.disabled} + /> +
+ + ) + }} + /> + + + {fields.map((commentItem, idx) => ( + + + }) { + return ( +
+ = props => { } : undefined } - onChange={function onChange(option: SingleValue<{ label: string; value: string }>) { + onChange={function onChange( + option: SingleValue<{ label: string; value: string }>, + ) { controlProps.field.onChange( (option as SelectOption).value, ) - props.recalculateReviewProgress() + recalculateReviewProgress() }} onBlur={function onBlur() { controlProps.field.onBlur() - props.setIsTouched(old => ({ + setIsTouched(old => ({ ...old, [`reviews.${props.fieldIndex}.initialAnswer.message`]: true, })) - props.trigger( + trigger( `reviews.${props.fieldIndex}.initialAnswer`, ) }} @@ -223,15 +243,14 @@ export const ScorecardQuestionEdit: FC = props => { }} /> - {fields.map((commentItem, idx) => ( = props => { controlProps.field.onChange( (option as SelectOption).value, ) - props.trigger( + trigger( `reviews.${props.fieldIndex}.comments.${idx}.content`, ) }} @@ -275,7 +294,7 @@ export const ScorecardQuestionEdit: FC = props => { = props => { onChange={controlProps.field.onChange} onBlur={function onBlur() { controlProps.field.onBlur() - props.setIsTouched( + setIsTouched( old => ({ ...old, [`reviews.${props.fieldIndex}.comments.${idx}.content`]: true, @@ -311,24 +330,17 @@ export const ScorecardQuestionEdit: FC = props => { ))} Add Response - } + )} />
@@ -336,5 +348,3 @@ export const ScorecardQuestionEdit: FC = props => { } export default ScorecardQuestionEdit - - diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/index.ts index d4b0b9dd1..4d7c9bec7 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/index.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/index.ts @@ -1,3 +1 @@ -export { default as ScorecardQuestionEdit } from './ScorecardQuestionEdit' - - +export { default as ScorecardQuestionEdit } from './ScorecardQuestionEdit' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx index bd783401d..cb1df6f0d 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx @@ -79,5 +79,3 @@ export const ScorecardQuestionView: FC = props => { } export default ScorecardQuestionView - - diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts index 99832d11d..b9403438d 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts @@ -1,3 +1 @@ -export { default as ScorecardQuestionView } from './ScorecardQuestionView' - - +export { default as ScorecardQuestionView } from './ScorecardQuestionView' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx index c6a1ac527..148a62d09 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx @@ -12,12 +12,6 @@ interface ScorecardSectionProps { index: string section: ScorecardSectionModel reviewItemMapping?: ReturnType - formControl?: any - formErrors?: any - formIsTouched?: { [key: string]: boolean } - formSetIsTouched?: any - formTrigger?: any - recalculateReviewProgress?: () => void } const ScorecardSection: FC = props => { @@ -54,12 +48,6 @@ const ScorecardSection: FC = props => { index={[props.index, index + 1].join('.')} question={question} reviewItemMapping={props.reviewItemMapping} - formControl={props.formControl} - formErrors={props.formErrors} - formIsTouched={props.formIsTouched} - formSetIsTouched={props.formSetIsTouched} - formTrigger={props.formTrigger} - recalculateReviewProgress={props.recalculateReviewProgress} /> ))}
diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx index b28ec25ce..fa8567b24 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx @@ -1,4 +1,5 @@ import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { FieldErrors, UseFormReturn, UseFormTrigger } from 'react-hook-form' import { AiFeedbackItem, @@ -8,12 +9,15 @@ import { ReviewInfo, ReviewItemInfo, Scorecard, + ScorecardInfo, } from '../../../models' import { ReviewItemComment } from '../../../models/ReviewItemComment.model' +import { useReviewForm } from './hooks/useReviewForm' + export interface ScorecardViewerContextProps { children: ReactNode; - scorecard: Scorecard + scorecard: Scorecard | ScorecardInfo aiFeedbackItems?: AiFeedbackItem[] reviewInfo?: ReviewInfo isEdit?: boolean @@ -53,6 +57,7 @@ export interface ScorecardViewerContextProps { reviewItem: ReviewItemInfo, success: () => void, ) => void + onFormChange?: (isDirty: boolean) => void } export type ScorecardViewerContextValue = { @@ -97,6 +102,16 @@ export type ScorecardViewerContextValue = { reviewItem: ReviewItemInfo, success: () => void, ) => void + // Form control related + form?: UseFormReturn + reviewProgress: number + totalScore: number + isTouched: { [key: string]: boolean } + setIsTouched: React.Dispatch> + recalculateReviewProgress: () => void + touchedAllFields: () => void + formErrors?: FieldErrors + formTrigger?: UseFormTrigger }; const ScorecardViewerContext = createContext({} as ScorecardViewerContextValue) @@ -114,24 +129,35 @@ export const ScorecardViewerContextProvider: FC = p // reset toggle state on scorecard change useEffect(() => setToggledItems({}), [props.scorecard]) + const reviewFormCtx = useReviewForm({ + onFormChange: props.onFormChange, + reviewInfo: props.reviewInfo, + scorecardInfo: props.scorecard, + }) + const ctxValue = useMemo(() => ({ + actionChallengeRole: props.actionChallengeRole, + addAppeal: props.addAppeal, + addAppealResponse: props.addAppealResponse, + addManagerComment: props.addManagerComment, aiFeedbackItems: props.aiFeedbackItems, - toggledItems, - toggleItem, - reviewInfo: props.reviewInfo, + doDeleteAppeal: props.doDeleteAppeal, isEdit: props.isEdit, isManagerEdit: props.isManagerEdit, - actionChallengeRole: props.actionChallengeRole, - mappingAppeals: props.mappingAppeals, - isSavingReview: props.isSavingReview, isSavingAppeal: props.isSavingAppeal, isSavingAppealResponse: props.isSavingAppealResponse, isSavingManagerComment: props.isSavingManagerComment, + isSavingReview: props.isSavingReview, + mappingAppeals: props.mappingAppeals, + reviewInfo: props.reviewInfo, saveReviewInfo: props.saveReviewInfo, - addAppeal: props.addAppeal, - doDeleteAppeal: props.doDeleteAppeal, - addAppealResponse: props.addAppealResponse, - addManagerComment: props.addManagerComment, + toggledItems, + toggleItem, + // Form control related + ...reviewFormCtx, + form: props.isEdit ? reviewFormCtx.form : undefined, + formErrors: props.isEdit ? reviewFormCtx.form.formState.errors : undefined, + formTrigger: props.isEdit ? reviewFormCtx.form.trigger : undefined, }), [ props.aiFeedbackItems, props.reviewInfo, @@ -150,6 +176,7 @@ export const ScorecardViewerContextProvider: FC = p props.addManagerComment, toggledItems, toggleItem, + reviewFormCtx, ]) return ( diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index f4fd8ecbf..a5ec01d17 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -1,4 +1,6 @@ -import { FC, useCallback, useMemo, useState, Fragment } from 'react' +/* eslint-disable complexity */ + +import { FC, useCallback, useMemo, useState } from 'react' import { isEmpty } from 'lodash' import classNames from 'classnames' @@ -15,14 +17,17 @@ import { Scorecard, ScorecardInfo, } from '../../../models' -import { useReviewForm } from './hooks/useReviewForm' -import { createReviewItemMapping } from './utils' -import { ScorecardGroup } from './ScorecardGroup' -import { ScorecardViewerContextProvider } from './ScorecardViewer.context' -import { ScorecardTotal } from './ScorecardTotal' -import { ConfirmModal } from '../../ConfirmModal' import { IconError } from '../../../assets/icons' +import { ConfirmModal } from '../../ConfirmModal' +import { + ScorecardViewerContextProvider, + ScorecardViewerContextValue, + useScorecardContext, +} from './ScorecardViewer.context' +import { ScorecardGroup } from './ScorecardGroup' +import { ScorecardTotal } from './ScorecardTotal' +import { createReviewItemMapping } from './utils' import styles from './ScorecardViewer.module.scss' interface ScorecardViewerProps { @@ -72,46 +77,28 @@ interface ScorecardViewerProps { setIsChanged?: (changed: boolean) => void } -const ScorecardViewer: FC = props => { +const ScorecardViewerContent: FC = props => { const [isShowSaveAsDraftModal, setIsShowSaveAsDraftModal] = useState(false) const [shouldRedirectAfterDraft, setShouldRedirectAfterDraft] = useState(false) - // const [isChanged, setIsChanged] = useState(false) const reviewItemMapping = useMemo(() => { if (!props.reviewInfo?.reviewItems) { return undefined } + return createReviewItemMapping(props.reviewInfo.reviewItems) }, [props.reviewInfo]) const { form, - reviewProgress, totalScore, isTouched, - setIsTouched, - recalculateReviewProgress, touchedAllFields, - } = useReviewForm({ - reviewInfo: props.reviewInfo, - scorecardInfo: props.scorecard as ScorecardInfo, - onFormChange: props.setIsChanged, - }) - - const { handleSubmit, getValues, formState: { errors, isDirty } } = form - - const displayedTotalScore = useMemo(() => { - const maybeFinalScore = props.reviewInfo?.finalScore - if ( - !props.isEdit - && typeof maybeFinalScore === 'number' - && Number.isFinite(maybeFinalScore) - ) { - return maybeFinalScore.toFixed(2) - } + formErrors, + }: ScorecardViewerContextValue = useScorecardContext() - return totalScore.toFixed(2) - }, [props.isEdit, props.reviewInfo?.finalScore, totalScore]) + const isDirty = form?.formState?.isDirty || false + const errors = formErrors || {} const errorMessageTop = isEmpty(errors) || isEmpty(isTouched) ? '' @@ -121,20 +108,20 @@ const ScorecardViewer: FC = props => { ? '' : 'There were validation errors. Check above.' - const onSubmit = useCallback((data: FormReviews) => { + const onSubmit = useCallback(() => { if (props.saveReviewInfo) { props.saveReviewInfo( - isDirty ? getValues() : undefined, - getValues(), + isDirty ? form?.getValues() : undefined, + form?.getValues(), true, totalScore, - () => { + (): void => { // Success callback - could navigate or show success message }, ) } }, [ - getValues, + form, isDirty, props.saveReviewInfo, totalScore, @@ -143,8 +130,8 @@ const ScorecardViewer: FC = props => { const handleSaveAsDraft = useCallback(() => { if (props.saveReviewInfo) { props.saveReviewInfo( - isDirty ? getValues() : undefined, - getValues(), + isDirty ? form?.getValues() : undefined, + form?.getValues(), false, totalScore, () => { @@ -153,7 +140,7 @@ const ScorecardViewer: FC = props => { }, ) } - }, [getValues, isDirty, props.saveReviewInfo, totalScore]) + }, [form, isDirty, props.saveReviewInfo, totalScore]) const handleCloseDraftModal = useCallback(() => { setIsShowSaveAsDraftModal(false) @@ -162,161 +149,115 @@ const ScorecardViewer: FC = props => { } }, [shouldRedirectAfterDraft]) - const expandAll = useCallback(() => { - // Expand all questions - this would need to be implemented in context - }, []) - - const collapseAll = useCallback(() => { - // Collapse all questions - this would need to be implemented in context - }, []) - const ContainerTag = props.isEdit ? 'form' : 'div' + if (props.isLoading) { + return + } + return (
- - {props.isLoading ? ( - - ) : ( - <> - {/* {(props.isEdit || props.reviewInfo) && ( - - )} */} + {errorMessageTop && ( +
+ + + + {errorMessageTop} +
+ )} + + {!!props.score && !props.reviewInfo && ( +
+ Conclusion +

+ Congratulations! You earned a score of + {' '} + + {props.score.toFixed(2)} + + {' '} + out of the maximum of + {' '} + + {(props.scorecard as Scorecard).maxScore.toFixed(2)} + + . + You did a good job on passing the scorecard criteria. + Please check the below sections to see if there is any place for improvement. +

+
+ )} + + {props.reviewInfo && ( + + {props.scorecard.scorecardGroups.map((group, index) => ( + + ))} + + )} - {errorMessageTop && ( -
+ {!props.reviewInfo && props.scorecard.scorecardGroups.map((group, index) => ( + + ))} + + + + {props.isEdit && ( +
+
+ {errorMessageBottom && ( +
- {errorMessageTop} + {errorMessageBottom}
)} +
- {!!props.score && !props.reviewInfo && ( -
- Conclusion -

- Congratulations! You earned a score of - {' '} - - {props.score.toFixed(2)} - - {' '} - out of the maximum of - {' '} - - {(props.scorecard as Scorecard).maxScore.toFixed(2)} - - . - You did a good job on passing the scorecard criteria. - Please check the below sections to see if there is any place for improvement. -

-
- )} - - {props.reviewInfo && ( - + {props.onCancelEdit && ( + - )} - - -
-
+ Cancel + )} - - )} -
+ + +
+
+ )} = props => { ) } +const ScorecardViewer: FC = props => ( + + + +) + export default ScorecardViewer diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts index 363d405bf..5070f080f 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts @@ -1,174 +1,173 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useForm, UseFormReturn } from 'react-hook-form' -import { filter, forEach, isEmpty, reduce } from 'lodash' -import { yupResolver } from '@hookform/resolvers/yup' - -import { FormReviews, ReviewInfo, ScorecardInfo } from '../../../../models' -import { formReviewsSchema, roundWith2DecimalPlaces } from '../../../../utils' -import { normalizeScorecardQuestionId } from '../utils' - -interface UseReviewFormProps { - reviewInfo?: ReviewInfo - scorecardInfo?: ScorecardInfo - onFormChange?: (isDirty: boolean) => void -} - -interface UseReviewFormReturn { - form: UseFormReturn - reviewProgress: number - totalScore: number - isTouched: { [key: string]: boolean } - setIsTouched: React.Dispatch> - recalculateReviewProgress: () => void - touchedAllFields: () => void -} - -export const useReviewForm = ({ - reviewInfo, - scorecardInfo, - onFormChange, -}: UseReviewFormProps): UseReviewFormReturn => { - const [reviewProgress, setReviewProgress] = useState(0) - const [totalScore, setTotalScore] = useState(0) - const [isTouched, setIsTouched] = useState<{ [key: string]: boolean }>({}) - - const form = useForm({ - defaultValues: { - reviews: [], - }, - mode: 'onChange', - resolver: yupResolver(formReviewsSchema), - }) - - const { formState: { isDirty }, getValues, reset } = form - - useEffect(() => { - onFormChange?.(isDirty) - }, [isDirty, onFormChange]) - - const recalculateReviewProgress = useCallback(() => { - const reviewFormDatas = getValues().reviews - const mappingResult: { - [scorecardQuestionId: string]: string - } = {} - - const newReviewProgress = reviewFormDatas.length > 0 - ? Math.round( - (filter(reviewFormDatas, review => { - const normalizedId = normalizeScorecardQuestionId( - review.scorecardQuestionId, - ) - if (normalizedId) { - mappingResult[normalizedId] = review.initialAnswer - } - - return !!review.initialAnswer - }).length - * 100) - / reviewFormDatas.length, - ) - : 0 - setReviewProgress(newReviewProgress) - - const groupsScore = reduce( - scorecardInfo?.scorecardGroups ?? [], - (groupResult, group) => { - const groupPoint = (reduce( - group.sections ?? [], - (sectionResult, section) => { - const sectionPoint = (reduce( - section.questions ?? [], - (questionResult, question) => { - let questionPoint = 0 - const normalizedQuestionId = normalizeScorecardQuestionId( - question.id as string, - ) - const initialAnswer = normalizedQuestionId - ? mappingResult[normalizedQuestionId] - : undefined - - if ( - question.type === 'YES_NO' - && initialAnswer === 'Yes' - ) { - questionPoint = 100 - } else if ( - question.type === 'SCALE' - && !!initialAnswer - ) { - const totalPoint = question.scaleMax - question.scaleMin - const initialAnswerNumber = parseInt(initialAnswer, 10) - question.scaleMin - questionPoint = totalPoint > 0 - ? (initialAnswerNumber * 100) / totalPoint - : 0 - } - - return ( - questionResult - + (questionPoint * question.weight) / 100 - ) - }, - 0, - ) * section.weight) / 100 - return sectionResult + sectionPoint - }, - 0, - ) * group.weight) / 100 - return groupResult + groupPoint - }, - 0, - ) - setTotalScore(roundWith2DecimalPlaces(groupsScore)) - }, [getValues, scorecardInfo]) - - useEffect(() => { - if (reviewInfo) { - const newFormData = { - reviews: reviewInfo.reviewItems.map( - (reviewItem, reviewItemIndex) => ({ - comments: reviewItem.reviewItemComments.map( - (commentItem, commentIndex) => ({ - content: commentItem.content ?? '', - id: commentItem.id, - index: commentIndex, - type: commentItem.type ?? '', - }), - ), - id: reviewItem.id, - index: reviewItemIndex, - initialAnswer: reviewItem.finalAnswer || reviewItem.initialAnswer, - scorecardQuestionId: reviewItem.scorecardQuestionId, - }), - ), - } - reset(newFormData) - recalculateReviewProgress() - } - }, [reviewInfo, recalculateReviewProgress, reset]) - - const touchedAllFields = useCallback(() => { - const formData = getValues() - const isTouchedAll: { [key: string]: boolean } = {} - forEach(formData.reviews, review => { - isTouchedAll[`reviews.${review.index}.initialAnswer.message`] = true - forEach(review.comments, comment => { - isTouchedAll[ - `reviews.${review.index}.comments.${comment.index}.content` - ] = true - }) - }) - setIsTouched(isTouchedAll) - }, [getValues]) - - return { - form, - reviewProgress, - totalScore, - isTouched, - setIsTouched, - recalculateReviewProgress, - touchedAllFields, - } -} - - +import { useCallback, useEffect, useState } from 'react' +import { useForm, UseFormReturn } from 'react-hook-form' +import { filter, forEach, reduce } from 'lodash' + +import { yupResolver } from '@hookform/resolvers/yup' + +import { FormReviews, ReviewInfo, Scorecard, ScorecardInfo } from '../../../../models' +import { formReviewsSchema, roundWith2DecimalPlaces } from '../../../../utils' +import { normalizeScorecardQuestionId } from '../utils' + +interface UseReviewFormProps { + reviewInfo?: ReviewInfo + scorecardInfo?: Scorecard | ScorecardInfo + onFormChange?: (isDirty: boolean) => void +} + +export interface UseReviewForm { + form: UseFormReturn + reviewProgress: number + totalScore: number + isTouched: { [key: string]: boolean } + setIsTouched: React.Dispatch> + recalculateReviewProgress: () => void + touchedAllFields: () => void +} + +export const useReviewForm = ({ + reviewInfo, + scorecardInfo, + onFormChange, +}: UseReviewFormProps): UseReviewForm => { + const [reviewProgress, setReviewProgress] = useState(0) + const [totalScore, setTotalScore] = useState(0) + const [isTouched, setIsTouched] = useState<{ [key: string]: boolean }>({}) + + const form = useForm({ + defaultValues: { + reviews: [], + }, + mode: 'onChange', + resolver: yupResolver(formReviewsSchema), + }) + + const { formState: { isDirty }, getValues, reset }: UseFormReturn = form + + useEffect(() => { + onFormChange?.(isDirty) + }, [isDirty, onFormChange]) + + const recalculateReviewProgress = useCallback(() => { + const reviewFormDatas = getValues().reviews + const mappingResult: { + [scorecardQuestionId: string]: string + } = {} + + const newReviewProgress = reviewFormDatas.length > 0 + ? Math.round( + (filter(reviewFormDatas, review => { + const normalizedId = normalizeScorecardQuestionId( + review.scorecardQuestionId, + ) + if (normalizedId) { + mappingResult[normalizedId] = review.initialAnswer + } + + return !!review.initialAnswer + }).length + * 100) + / reviewFormDatas.length, + ) + : 0 + setReviewProgress(newReviewProgress) + + const groupsScore = reduce( + scorecardInfo?.scorecardGroups ?? [], + (groupResult, group) => { + const groupPoint = (reduce( + group.sections ?? [], + (sectionResult, section) => { + const sectionPoint = (reduce( + section.questions ?? [], + (questionResult, question) => { + let questionPoint = 0 + const normalizedQuestionId = normalizeScorecardQuestionId( + question.id as string, + ) + const initialAnswer = normalizedQuestionId + ? mappingResult[normalizedQuestionId] + : undefined + + if ( + question.type === 'YES_NO' + && initialAnswer === 'Yes' + ) { + questionPoint = 100 + } else if ( + question.type === 'SCALE' + && !!initialAnswer + ) { + const totalPoint = question.scaleMax - question.scaleMin + const initialAnswerNumber = parseInt(initialAnswer, 10) - question.scaleMin + questionPoint = totalPoint > 0 + ? (initialAnswerNumber * 100) / totalPoint + : 0 + } + + return ( + questionResult + + (questionPoint * question.weight) / 100 + ) + }, + 0, + ) * section.weight) / 100 + return sectionResult + sectionPoint + }, + 0, + ) * group.weight) / 100 + return groupResult + groupPoint + }, + 0, + ) + setTotalScore(roundWith2DecimalPlaces(groupsScore)) + }, [getValues, scorecardInfo]) + + useEffect(() => { + if (reviewInfo) { + const newFormData = { + reviews: reviewInfo.reviewItems.map( + (reviewItem, reviewItemIndex) => ({ + comments: reviewItem.reviewItemComments.map( + (commentItem, commentIndex) => ({ + content: commentItem.content ?? '', + id: commentItem.id, + index: commentIndex, + type: commentItem.type ?? '', + }), + ), + id: reviewItem.id, + index: reviewItemIndex, + initialAnswer: reviewItem.finalAnswer || reviewItem.initialAnswer, + scorecardQuestionId: reviewItem.scorecardQuestionId, + }), + ), + } + reset(newFormData) + recalculateReviewProgress() + } + }, [reviewInfo, recalculateReviewProgress, reset]) + + const touchedAllFields = useCallback(() => { + const formData = getValues() + const isTouchedAll: { [key: string]: boolean } = {} + forEach(formData.reviews, review => { + isTouchedAll[`reviews.${review.index}.initialAnswer.message`] = true + forEach(review.comments, comment => { + isTouchedAll[ + `reviews.${review.index}.comments.${comment.index}.content` + ] = true + }) + }) + setIsTouched(isTouchedAll) + }, [getValues]) + + return { + form, + isTouched, + recalculateReviewProgress, + reviewProgress, + setIsTouched, + totalScore, + touchedAllFields, + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts index b45b397e2..8e95359a4 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts @@ -57,7 +57,7 @@ export const createReviewItemMapping = ( index: number } } = {} - + reviewItems.forEach((item, index) => { const normalizedId = normalizeScorecardQuestionId( item.scorecardQuestionId, @@ -69,6 +69,6 @@ export const createReviewItemMapping = ( } } }) - + return result } diff --git a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx index 0938a2855..c6cdb25b8 100644 --- a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx @@ -28,7 +28,7 @@ import { PageWrapper, ScorecardDetails, } from '../../../lib' -import { BreadCrumbData, ChallengeDetailContextModel } from '../../../lib/models' +import { BreadCrumbData, ChallengeDetailContextModel, ScorecardInfo } from '../../../lib/models' import { SubmissionBarInfo } from '../../../lib/components/SubmissionBarInfo' import { ChallengeLinksForAdmin } from '../../../lib/components/ChallengeLinksForAdmin' import { ADMIN, COPILOT, MANAGER } from '../../../config/index.config' @@ -36,6 +36,7 @@ import { useIsEditReview, useIsEditReviewProps } from '../../../lib/hooks/useIsE import { activeReviewAssignmentsRouteId, rootRoute } from '../../../config/routes.config' import styles from './ScorecardDetailsPage.module.scss' +import { ScorecardViewer } from '../../../lib/components/Scorecard' type ReviewPhaseType = | 'screening' @@ -553,6 +554,7 @@ export const ScorecardDetailsPage: FC = (props: Props) => {
) : ( + <> = (props: Props) => { doDeleteAppeal={doDeleteAppeal} addManagerComment={addManagerComment} /> + + )} {isEdit && ( From ffb901ba2cfdc686c35c961dfa8c4a46ffeeb4dd Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Mon, 10 Nov 2025 17:19:09 +0200 Subject: [PATCH 039/125] more refactoring --- .../ScorecardQuestion.module.scss | 25 ++++- .../ScorecardQuestion/ScorecardQuestion.tsx | 88 +++++++++++---- .../ScorecardQuestionEdit.tsx | 2 - .../ScorecardQuestionView.module.scss | 29 ----- .../ScorecardQuestionView.tsx | 81 -------------- .../ScorecardQuestionView/index.ts | 1 - .../ScorecardViewer.context.tsx | 41 ++++--- .../hooks/useProgressCalculation.ts | 59 ++++++++++ .../ScorecardViewer/hooks/useReviewForm.ts | 92 +--------------- .../ScorecardViewer/hooks/useToggleItems.ts | 33 ++++++ .../Scorecard/ScorecardViewer/utils.ts | 103 +++++++++++++++++- 11 files changed, 303 insertions(+), 251 deletions(-) delete mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.module.scss delete mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx delete mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useProgressCalculation.ts create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useToggleItems.ts diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss index 7eb3399cc..9029680c0 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.module.scss @@ -1,21 +1,36 @@ @import '@libs/ui/styles/includes'; +.wrap { +} + +.header { + margin-bottom: 12px; +} + .toggleBtn { cursor: pointer; + transition: transform 0.2s; + width: 20px; + height: 20px; display: block; - width: 16px; - height: 16px; color: #767676; - transition: 0.15s ease-in-out; - &.toggled { + &.expanded { transform: rotate(180deg); } } .questionText { - font-weight: bold; + font-weight: 600; + flex: 1; + * { margin-top: $sp-2; } } + +.guidelines { + white-space: pre-wrap; +} + +.guidelinesContent { +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx index 79300dbdc..33f488e65 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx @@ -1,12 +1,18 @@ -import { FC, useMemo } from 'react' +import { FC, useCallback, useMemo } from 'react' +import classNames from 'classnames' + +import { IconOutline } from '~/libs/ui' import { ScorecardQuestion as ScorecardQuestionModel } from '../../../../models' import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' import { createReviewItemMapping, normalizeScorecardQuestionId } from '../utils' +import { MarkdownReview } from '../../../MarkdownReview' +import { ReviewComments } from './ReviewResponse/ReviewComments' import { AiFeedback } from './AiFeedback' import { ScorecardQuestionEdit } from './ScorecardQuestionEdit' -import { ScorecardQuestionView } from './ScorecardQuestionView' +import { ScorecardQuestionRow } from './ScorecardQuestionRow' +import { ReviewAnswer } from './ReviewResponse/ReviewAnswer' import styles from './ScorecardQuestion.module.scss' interface ScorecardQuestionProps { @@ -18,6 +24,9 @@ interface ScorecardQuestionProps { const ScorecardQuestion: FC = props => { const { isEdit, + toggleItem, + toggledItems, + mappingAppeals, }: ScorecardViewerContextValue = useScorecardContext() const normalizedQuestionId = useMemo( @@ -33,6 +42,21 @@ const ScorecardQuestion: FC = props => { return props.reviewItemMapping[normalizedQuestionId] }, [normalizedQuestionId, props.reviewItemMapping]) + const isExpanded = toggledItems[props.question.id!] ?? false + const toggle = useCallback(() => toggleItem(props.question.id!), [props.question.id, toggleItem]) + + const hasReviewData = useMemo(() => { + if (!reviewItemInfo?.item) { + return false + } + + const reviewItem = reviewItemInfo.item + const hasAnswer = !!(reviewItem.finalAnswer || reviewItem.initialAnswer) + const hasComments = reviewItem.reviewItemComments?.length > 0 + const hasManagerComment = !!reviewItem.managerComment + return hasAnswer || hasComments || hasManagerComment + }, [reviewItemInfo]) + // If in edit mode and we have review item, show edit component if (isEdit && reviewItemInfo) { return ( @@ -47,28 +71,48 @@ const ScorecardQuestion: FC = props => { ) } - // If in view mode and we have review item, show view component - if (!isEdit && reviewItemInfo) { - return ( -
- -
- ) - } - - // Default: show read-only question (for AI scorecards or when no review data) + // View mode: render question with review data if available return (
- - + + )} + index={`Question ${props.index}`} + className={styles.header} + > + + {props.question.description} + + {isExpanded && props.question.guidelines && ( +
+ +
+ )} +
+ + {!reviewItemInfo && } + + {hasReviewData && reviewItemInfo && ( + <> + + + + + )}
) } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx index 5b855b0b3..7dc545bf0 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx @@ -47,7 +47,6 @@ export const ScorecardQuestionEdit: FC = props => { isTouched, setIsTouched, formTrigger, - recalculateReviewProgress, }: ScorecardViewerContextValue = useScorecardContext() const isExpanded = toggledItems[props.question.id!] ?? false @@ -223,7 +222,6 @@ export const ScorecardQuestionEdit: FC = props => { controlProps.field.onChange( (option as SelectOption).value, ) - recalculateReviewProgress() }} onBlur={function onBlur() { controlProps.field.onBlur() diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.module.scss deleted file mode 100644 index 7d1016376..000000000 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.module.scss +++ /dev/null @@ -1,29 +0,0 @@ -.wrap { -} - -.header { - margin-bottom: 12px; -} - -.toggleBtn { - cursor: pointer; - transition: transform 0.2s; - width: 20px; - height: 20px; - - &.expanded { - transform: rotate(180deg); - } -} - -.questionText { - font-weight: 600; - flex: 1; -} - -.guidelines { - white-space: pre-wrap; -} - - - diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx deleted file mode 100644 index cb1df6f0d..000000000 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/ScorecardQuestionView.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { FC, useCallback, useMemo } from 'react' -import classNames from 'classnames' - -import { IconOutline } from '~/libs/ui' - -import { ReviewItemInfo, ScorecardQuestion } from '../../../../../models' -import { ScorecardViewerContextValue, useScorecardContext } from '../../ScorecardViewer.context' -import { ScorecardQuestionRow } from '../ScorecardQuestionRow' -import { ReviewAnswer } from '../ReviewResponse/ReviewAnswer' -import { ReviewComments } from '../ReviewResponse/ReviewComments' - -import styles from './ScorecardQuestionView.module.scss' - -interface ScorecardQuestionViewProps { - question: ScorecardQuestion - reviewItem?: ReviewItemInfo - index: string -} - -export const ScorecardQuestionView: FC = props => { - const { - toggleItem, - toggledItems, - mappingAppeals, - }: ScorecardViewerContextValue = useScorecardContext() - - const isExpanded = toggledItems[props.question.id!] ?? false - const toggle = useCallback(() => toggleItem(props.question.id!), [props.question.id, toggleItem]) - - const hasReviewData = useMemo(() => { - if (!props.reviewItem) { - return false - } - - const hasAnswer = !!(props.reviewItem.finalAnswer || props.reviewItem.initialAnswer) - const hasComments = props.reviewItem.reviewItemComments?.length > 0 - const hasManagerComment = !!props.reviewItem.managerComment - return hasAnswer || hasComments || hasManagerComment - }, [props.reviewItem]) - - return ( -
- - )} - index={`Question ${props.index}`} - className={styles.header} - > - - {props.question.description} - - {isExpanded && (props.question.guidelines && ( -
- {props.question.guidelines} -
- ))} -
- - {hasReviewData && props.reviewItem && ( - <> - - - - - )} -
- ) -} - -export default ScorecardQuestionView diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts deleted file mode 100644 index b9403438d..000000000 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as ScorecardQuestionView } from './ScorecardQuestionView' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx index fa8567b24..3f8f3a8c1 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx @@ -1,4 +1,4 @@ -import { createContext, FC, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { createContext, FC, ReactNode, useContext, useMemo } from 'react' import { FieldErrors, UseFormReturn, UseFormTrigger } from 'react-hook-form' import { @@ -13,7 +13,9 @@ import { } from '../../../models' import { ReviewItemComment } from '../../../models/ReviewItemComment.model' +import { useProgressCalculation } from './hooks/useProgressCalculation' import { useReviewForm } from './hooks/useReviewForm' +import { useToggleItems } from './hooks/useToggleItems' export interface ScorecardViewerContextProps { children: ReactNode; @@ -117,17 +119,7 @@ export type ScorecardViewerContextValue = { const ScorecardViewerContext = createContext({} as ScorecardViewerContextValue) export const ScorecardViewerContextProvider: FC = props => { - const [toggledItems, setToggledItems] = useState<{[key: string]: boolean}>({}) - - const toggleItem = useCallback((id: string, toggle?: boolean) => { - setToggledItems(prevItems => ({ - ...prevItems, - [id]: typeof toggle === 'boolean' ? toggle : !prevItems[id], - })) - }, []) - - // reset toggle state on scorecard change - useEffect(() => setToggledItems({}), [props.scorecard]) + const collapsiblesCtx = useToggleItems({ scorecard: props.scorecard }) const reviewFormCtx = useReviewForm({ onFormChange: props.onFormChange, @@ -135,29 +127,36 @@ export const ScorecardViewerContextProvider: FC = p scorecardInfo: props.scorecard, }) + const progressCtx = useProgressCalculation({ + form: reviewFormCtx.form, + scorecard: props.scorecard, + }) + const ctxValue = useMemo(() => ({ + ...collapsiblesCtx, + ...reviewFormCtx, + ...progressCtx, actionChallengeRole: props.actionChallengeRole, addAppeal: props.addAppeal, addAppealResponse: props.addAppealResponse, addManagerComment: props.addManagerComment, aiFeedbackItems: props.aiFeedbackItems, doDeleteAppeal: props.doDeleteAppeal, + form: props.isEdit ? reviewFormCtx.form : undefined, + formErrors: props.isEdit ? reviewFormCtx.form.formState.errors : undefined, + formTrigger: props.isEdit ? reviewFormCtx.form.trigger : undefined, isEdit: props.isEdit, isManagerEdit: props.isManagerEdit, isSavingAppeal: props.isSavingAppeal, isSavingAppealResponse: props.isSavingAppealResponse, isSavingManagerComment: props.isSavingManagerComment, isSavingReview: props.isSavingReview, + isTouched: reviewFormCtx.isTouched, mappingAppeals: props.mappingAppeals, reviewInfo: props.reviewInfo, saveReviewInfo: props.saveReviewInfo, - toggledItems, - toggleItem, - // Form control related - ...reviewFormCtx, - form: props.isEdit ? reviewFormCtx.form : undefined, - formErrors: props.isEdit ? reviewFormCtx.form.formState.errors : undefined, - formTrigger: props.isEdit ? reviewFormCtx.form.trigger : undefined, + setIsTouched: reviewFormCtx.setIsTouched, + touchedAllFields: reviewFormCtx.touchedAllFields, }), [ props.aiFeedbackItems, props.reviewInfo, @@ -174,9 +173,9 @@ export const ScorecardViewerContextProvider: FC = p props.doDeleteAppeal, props.addAppealResponse, props.addManagerComment, - toggledItems, - toggleItem, + collapsiblesCtx.toggledItems, reviewFormCtx, + progressCtx, ]) return ( diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useProgressCalculation.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useProgressCalculation.ts new file mode 100644 index 000000000..e0753e333 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useProgressCalculation.ts @@ -0,0 +1,59 @@ +import { useCallback, useEffect, useState } from 'react' +import { UseFormReturn, useWatch } from 'react-hook-form' + +import { FormReviews, Scorecard, ScorecardInfo } from '../../../../models' +import { calculateProgressAndScore, ProgressAndScore } from '../utils' + +interface UseProgressCalculationProps { + form?: UseFormReturn + scorecard: Scorecard | ScorecardInfo +} + +export interface UseProgressCalculationValue { + totalScore: number + reviewProgress: number + recalculateReviewProgress: () => void +} + +export const useProgressCalculation = ({ + form, + scorecard, +}: UseProgressCalculationProps): UseProgressCalculationValue => { + const [reviewProgress, setReviewProgress] = useState(0) + const [totalScore, setTotalScore] = useState(0) + + // Watch form values to automatically recalculate progress + const watchedReviews = useWatch({ + control: form?.control, + name: 'reviews', + }) + + // Recalculate progress when form values change + useEffect(() => { + if (!form || !scorecard) { + return + } + + const reviewFormDatas = watchedReviews ?? form.getValues().reviews + const { progress, score }: ProgressAndScore = calculateProgressAndScore(reviewFormDatas, scorecard) + setReviewProgress(progress) + setTotalScore(score) + }, [watchedReviews, form, scorecard]) + + const recalculateReviewProgress = useCallback(() => { + if (!form || !scorecard) { + return + } + + const reviewFormDatas = form.getValues().reviews + const { progress, score }: ProgressAndScore = calculateProgressAndScore(reviewFormDatas, scorecard) + setReviewProgress(progress) + setTotalScore(score) + }, [form, scorecard]) + + return { + recalculateReviewProgress, + reviewProgress, + totalScore, + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts index 5070f080f..3c49bafcd 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts @@ -1,12 +1,11 @@ import { useCallback, useEffect, useState } from 'react' import { useForm, UseFormReturn } from 'react-hook-form' -import { filter, forEach, reduce } from 'lodash' +import { forEach } from 'lodash' import { yupResolver } from '@hookform/resolvers/yup' import { FormReviews, ReviewInfo, Scorecard, ScorecardInfo } from '../../../../models' -import { formReviewsSchema, roundWith2DecimalPlaces } from '../../../../utils' -import { normalizeScorecardQuestionId } from '../utils' +import { formReviewsSchema } from '../../../../utils' interface UseReviewFormProps { reviewInfo?: ReviewInfo @@ -16,21 +15,15 @@ interface UseReviewFormProps { export interface UseReviewForm { form: UseFormReturn - reviewProgress: number - totalScore: number isTouched: { [key: string]: boolean } setIsTouched: React.Dispatch> - recalculateReviewProgress: () => void touchedAllFields: () => void } export const useReviewForm = ({ reviewInfo, - scorecardInfo, onFormChange, }: UseReviewFormProps): UseReviewForm => { - const [reviewProgress, setReviewProgress] = useState(0) - const [totalScore, setTotalScore] = useState(0) const [isTouched, setIsTouched] = useState<{ [key: string]: boolean }>({}) const form = useForm({ @@ -47,81 +40,6 @@ export const useReviewForm = ({ onFormChange?.(isDirty) }, [isDirty, onFormChange]) - const recalculateReviewProgress = useCallback(() => { - const reviewFormDatas = getValues().reviews - const mappingResult: { - [scorecardQuestionId: string]: string - } = {} - - const newReviewProgress = reviewFormDatas.length > 0 - ? Math.round( - (filter(reviewFormDatas, review => { - const normalizedId = normalizeScorecardQuestionId( - review.scorecardQuestionId, - ) - if (normalizedId) { - mappingResult[normalizedId] = review.initialAnswer - } - - return !!review.initialAnswer - }).length - * 100) - / reviewFormDatas.length, - ) - : 0 - setReviewProgress(newReviewProgress) - - const groupsScore = reduce( - scorecardInfo?.scorecardGroups ?? [], - (groupResult, group) => { - const groupPoint = (reduce( - group.sections ?? [], - (sectionResult, section) => { - const sectionPoint = (reduce( - section.questions ?? [], - (questionResult, question) => { - let questionPoint = 0 - const normalizedQuestionId = normalizeScorecardQuestionId( - question.id as string, - ) - const initialAnswer = normalizedQuestionId - ? mappingResult[normalizedQuestionId] - : undefined - - if ( - question.type === 'YES_NO' - && initialAnswer === 'Yes' - ) { - questionPoint = 100 - } else if ( - question.type === 'SCALE' - && !!initialAnswer - ) { - const totalPoint = question.scaleMax - question.scaleMin - const initialAnswerNumber = parseInt(initialAnswer, 10) - question.scaleMin - questionPoint = totalPoint > 0 - ? (initialAnswerNumber * 100) / totalPoint - : 0 - } - - return ( - questionResult - + (questionPoint * question.weight) / 100 - ) - }, - 0, - ) * section.weight) / 100 - return sectionResult + sectionPoint - }, - 0, - ) * group.weight) / 100 - return groupResult + groupPoint - }, - 0, - ) - setTotalScore(roundWith2DecimalPlaces(groupsScore)) - }, [getValues, scorecardInfo]) - useEffect(() => { if (reviewInfo) { const newFormData = { @@ -143,9 +61,8 @@ export const useReviewForm = ({ ), } reset(newFormData) - recalculateReviewProgress() } - }, [reviewInfo, recalculateReviewProgress, reset]) + }, [reviewInfo, reset]) const touchedAllFields = useCallback(() => { const formData = getValues() @@ -164,10 +81,7 @@ export const useReviewForm = ({ return { form, isTouched, - recalculateReviewProgress, - reviewProgress, setIsTouched, - totalScore, touchedAllFields, } } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useToggleItems.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useToggleItems.ts new file mode 100644 index 000000000..968a7b968 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useToggleItems.ts @@ -0,0 +1,33 @@ +import { useCallback, useEffect, useState } from 'react' + +import { Scorecard, ScorecardInfo } from '../../../../models' + +interface UseToggleItemsProps { + scorecard: Scorecard | ScorecardInfo +} + +export interface UseToggleItemsValue { + toggledItems: {[key: string]: boolean} + toggleItem: (id: string, toggle?: boolean) => void +} + +export const useToggleItems = (props: UseToggleItemsProps): UseToggleItemsValue => { + const [toggledItems, setToggledItems] = useState<{[key: string]: boolean}>({}) + + const toggleItem = useCallback((id: string, toggle?: boolean) => { + setToggledItems(prevItems => ({ + ...prevItems, + [id]: typeof toggle === 'boolean' ? toggle : !prevItems[id], + })) + }, []) + + // Reset toggle state on scorecard change + useEffect(() => { + setToggledItems({}) + }, [props.scorecard]) + + return { + toggledItems, + toggleItem, + } +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts index 8e95359a4..47a1cd66c 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts @@ -1,4 +1,15 @@ -import { AiFeedbackItem, ReviewItemInfo, ScorecardGroup, ScorecardSection } from '../../../models' +import { filter, reduce } from 'lodash' + +import { + AiFeedbackItem, + FormReviews, + ReviewItemInfo, + Scorecard, + ScorecardGroup, + ScorecardInfo, + ScorecardSection, +} from '../../../models' +import { roundWith2DecimalPlaces } from '../../../utils' export const calcSectionScore = ( section: ScorecardSection, @@ -72,3 +83,93 @@ export const createReviewItemMapping = ( return result } + +export interface ProgressAndScore { + progress: number; + score: number; +} + +/** + * Calculate progress and score from review form data + */ +export const calculateProgressAndScore = ( + reviewFormDatas: FormReviews['reviews'], + scorecard: Scorecard | ScorecardInfo, +): ProgressAndScore => { + if (!scorecard || reviewFormDatas.length === 0) { + return { progress: 0, score: 0 } + } + + const mappingResult: { + [scorecardQuestionId: string]: string + } = {} + + const newReviewProgress = Math.round( + (filter(reviewFormDatas, review => { + const normalizedId = normalizeScorecardQuestionId( + review.scorecardQuestionId, + ) + if (normalizedId) { + mappingResult[normalizedId] = review.initialAnswer + } + + return !!review.initialAnswer + }).length + * 100) + / reviewFormDatas.length, + ) + + const groupsScore = reduce( + scorecard.scorecardGroups ?? [], + (groupResult, group) => { + const groupPoint = (reduce( + group.sections ?? [], + (sectionResult, section) => { + const sectionPoint = (reduce( + section.questions ?? [], + (questionResult, question) => { + let questionPoint = 0 + const normalizedQuestionId = normalizeScorecardQuestionId( + question.id as string, + ) + const initialAnswer = normalizedQuestionId + ? mappingResult[normalizedQuestionId] + : undefined + + if ( + question.type === 'YES_NO' + && initialAnswer === 'Yes' + ) { + questionPoint = 100 + } else if ( + question.type === 'SCALE' + && !!initialAnswer + ) { + const totalPoint = question.scaleMax - question.scaleMin + const initialAnswerNumber = parseInt(initialAnswer, 10) - question.scaleMin + questionPoint = totalPoint > 0 + ? (initialAnswerNumber * 100) / totalPoint + : 0 + } + + return ( + questionResult + + (questionPoint * question.weight) / 100 + ) + }, + 0, + ) * section.weight) / 100 + return sectionResult + sectionPoint + }, + 0, + ) * group.weight) / 100 + return groupResult + groupPoint + }, + 0, + ) + + return { + progress: newReviewProgress, + score: roundWith2DecimalPlaces(groupsScore), + } +} From d638eb6e9077062bbcb1c481b36677e9338c384e Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 11 Nov 2025 00:35:26 +0100 Subject: [PATCH 040/125] fix: removed usage of isAIReviewer --- .../DefaultReviewersAddForm/DefaultReviewersAddForm.tsx | 2 -- src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts | 2 -- src/apps/admin/src/lib/utils/validation-schemas.ts | 2 -- 3 files changed, 6 deletions(-) diff --git a/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx b/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx index 97f42e6b5..d8ae345b3 100644 --- a/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx +++ b/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx @@ -122,7 +122,6 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { baseCoefficient: 0, fixedAmount: 0, incrementalCoefficient: 0, - isAIReviewer: false, isMemberReview: false, memberReviewerCount: 0, opportunityType: '', @@ -176,7 +175,6 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { baseCoefficient: defaultReviewerInfo.baseCoefficient ?? 0, fixedAmount: defaultReviewerInfo.fixedAmount ?? 0, incrementalCoefficient: defaultReviewerInfo.incrementalCoefficient ?? 0, - isAIReviewer: defaultReviewerInfo.isAIReviewer, isMemberReview: defaultReviewerInfo.isMemberReview, memberReviewerCount: defaultReviewerInfo.memberReviewerCount ?? 0, opportunityType: defaultReviewerInfo.opportunityType ?? '', diff --git a/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts b/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts index 33da0ac0a..b48db10d6 100644 --- a/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts +++ b/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts @@ -12,7 +12,6 @@ export interface DefaultChallengeReviewer { baseCoefficient?: number; incrementalCoefficient?: number; opportunityType?: string; - isAIReviewer: boolean; aiWorkflowId?: string; shouldOpenOpportunity: boolean; createdAt: string; @@ -41,7 +40,6 @@ export interface FormAddDefaultReviewer { baseCoefficient?: number; incrementalCoefficient?: number; opportunityType?: string; - isAIReviewer: boolean; shouldOpenOpportunity: boolean; aiWorkflowId?: string; } diff --git a/src/apps/admin/src/lib/utils/validation-schemas.ts b/src/apps/admin/src/lib/utils/validation-schemas.ts index 9fc3cbb71..3fe7e3f1b 100644 --- a/src/apps/admin/src/lib/utils/validation-schemas.ts +++ b/src/apps/admin/src/lib/utils/validation-schemas.ts @@ -31,8 +31,6 @@ export const formAddDefaultReviewerSchema: Yup.ObjectSchema Date: Tue, 11 Nov 2025 01:17:08 +0100 Subject: [PATCH 041/125] fix: lint and save ai workflow id --- .../DefaultReviewersAddForm.tsx | 37 ++++++++++++++++--- .../models/DefaultChallengeReviewer.model.ts | 2 +- .../admin/src/lib/utils/validation-schemas.ts | 12 +++++- 3 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx b/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx index d8ae345b3..a9c79cbac 100644 --- a/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx +++ b/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx @@ -139,10 +139,19 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { const onSubmit = useCallback( (data: FormAddDefaultReviewer) => { + const isMemberReview = data.isMemberReview const requestBody = _.omitBy( data, value => value === undefined || value === null || value === '', ) + + if (!isMemberReview) { + requestBody.memberReviewerCount = undefined + } else { + // eslint-disable-next-line unicorn/no-null + requestBody.aiWorkflowId = null + } + if (isEdit) { doUpdateDefaultReviewer(requestBody, () => { navigate('./../..') @@ -407,7 +416,7 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { classNameWrapper={styles.inputField} /> = (props: Props) => { onChange={_.noop} error={_.get(errors, 'baseCoefficient.message')} inputControl={register('baseCoefficient', { - valueAsNumber: true, + setValueAs: v => { + if (typeof v === 'string') { + const normalized = v.replace(',', '.') + const parsed = parseFloat(normalized) + return Number.isNaN(parsed) ? undefined : parsed + } + + return v + }, + valueAsNumber: false, })} dirty disabled={isLoading} classNameWrapper={styles.inputField} /> = (props: Props) => { onChange={_.noop} error={_.get(errors, 'incrementalCoefficient.message')} inputControl={register('incrementalCoefficient', { - valueAsNumber: true, + setValueAs: v => { + if (typeof v === 'string') { + const normalized = v.replace(',', '.') + const parsed = parseFloat(normalized) + return Number.isNaN(parsed) ? undefined : parsed + } + + return v + }, + valueAsNumber: false, })} dirty disabled={isLoading} @@ -497,7 +524,7 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { label='AI Workflow' placeholder='Select AI Workflow' options={aiWorkflows} - value={controlProps.field.value} + value={controlProps.field.value || ''} onChange={controlProps.field.onChange} onBlur={controlProps.field.onBlur} classNameWrapper={styles.inputField} diff --git a/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts b/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts index b48db10d6..2841abc1d 100644 --- a/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts +++ b/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts @@ -41,5 +41,5 @@ export interface FormAddDefaultReviewer { incrementalCoefficient?: number; opportunityType?: string; shouldOpenOpportunity: boolean; - aiWorkflowId?: string; + aiWorkflowId?: string | null; } diff --git a/src/apps/admin/src/lib/utils/validation-schemas.ts b/src/apps/admin/src/lib/utils/validation-schemas.ts index 3fe7e3f1b..7f0f60905 100644 --- a/src/apps/admin/src/lib/utils/validation-schemas.ts +++ b/src/apps/admin/src/lib/utils/validation-schemas.ts @@ -24,7 +24,17 @@ export const formAddDefaultReviewerSchema: Yup.ObjectSchema { + if (typeof originalValue === 'string') { + // Replace comma with dot for decimal separator + const normalized = originalValue.replace(',', '.') + return parseFloat(normalized) + } + + return value + }) + .typeError('Please enter a valid number'), fixedAmount: Yup.number() .optional() .min(0, 'Must be non-negative'), From e33c0430d47b44ae546c2ed98bec3b8fd6e048ec Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 11 Nov 2025 15:21:25 +0200 Subject: [PATCH 042/125] PM-2178 - more work for merge review ui --- src/apps/review/src/config/routes.config.ts | 2 +- .../ScorecardQuestionEdit.tsx | 14 +- .../ScorecardScore/ScorecardScore.tsx | 7 +- .../ScorecardSection/ScorecardSection.tsx | 9 +- .../ScorecardViewer/ScorecardViewer.tsx | 2 +- .../Scorecard/ScorecardViewer/utils.ts | 26 +- .../lib/models/AiScorecardContext.model.ts | 1 + .../Reviews/Reviews.module.scss | 56 +++ .../Reviews/Reviews.tsx | 349 ++++++++++++++++++ .../Reviews/ReviewsSidebar.module.scss | 206 +++++++++++ .../Reviews/ReviewsSidebar.tsx | 139 +++++++ .../Reviews/ReviewsWrapper.tsx | 18 + .../Reviews/index.ts | 2 + .../ScorecardDetailsPage.tsx | 80 ++-- .../challenge-details.routes.tsx | 4 + .../AiScorecardContextProvider.tsx | 14 +- .../ai-scorecards/ai-scorecard.routes.tsx | 2 +- 17 files changed, 871 insertions(+), 60 deletions(-) create mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.module.scss create mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.tsx create mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.module.scss create mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.tsx create mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsWrapper.tsx create mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/index.ts diff --git a/src/apps/review/src/config/routes.config.ts b/src/apps/review/src/config/routes.config.ts index a5e81f61b..a5bbdf1ca 100644 --- a/src/apps/review/src/config/routes.config.ts +++ b/src/apps/review/src/config/routes.config.ts @@ -13,5 +13,5 @@ export const openOpportunitiesRouteId = 'open-opportunities' export const pastReviewAssignmentsRouteId = 'past-challenges' export const challengeDetailRouteId = ':challengeId' export const scorecardRouteId = 'scorecard' -export const aiScorecardRouteId = 'ai-scorecard' +export const aiScorecardRouteId = 'scorecard' export const viewScorecardRouteId = ':scorecardId' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx index 7dc545bf0..51c6babb6 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx @@ -30,6 +30,18 @@ import { ScorecardScore } from '../../ScorecardScore' import styles from './ScorecardQuestionEdit.module.scss' +const normalizeAnswerValue = (questionType: ScorecardQuestion['type'], value: string | number): number => { + if (['undefined', 'null'].includes(typeof value)) { + return Number(value); + } + + if (questionType === 'YES_NO') { + return Number(`${value}`.toLowerCase() === 'yes') + } + + return Number(`${value}`) +} + interface ScorecardQuestionEditProps { question: ScorecardQuestion reviewItem: ReviewItemInfo @@ -192,7 +204,7 @@ export const ScorecardQuestionEdit: FC = props => { index='Answer' score={( diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx index 765954eed..083063fb2 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx @@ -13,12 +13,15 @@ export const calcScore = (score: number, scaleMax: number, weight: number): numb ) const ScorecardScore: FC = props => { - const score = calcScore(props.score, props.scaleMax, props.weight) + let score = calcScore(props.score, props.scaleMax, props.weight)?.toFixed(2) + if (props.score.toString() === 'NaN') { + score = '-'; + } return (
- {score.toFixed(2)} + {score} / diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx index 148a62d09..abbf19c70 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx @@ -15,12 +15,15 @@ interface ScorecardSectionProps { } const ScorecardSection: FC = props => { - const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() - const allFeedbackItems = aiFeedbackItems || [] + const { aiFeedbackItems, reviewInfo, form }: ScorecardViewerContextValue = useScorecardContext() + const formValues = form?.getValues(); + const allFeedbackItems = useMemo(() => ( + formValues?.reviews || aiFeedbackItems || reviewInfo?.reviewItems || [] + ), [formValues, aiFeedbackItems, reviewInfo]) const score = useMemo(() => ( calcSectionScore(props.section, allFeedbackItems) - ), [props.section, allFeedbackItems]) + ), [props.section, allFeedbackItems, formValues]) return (
diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index a5ec01d17..d7ac46554 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -179,7 +179,7 @@ const ScorecardViewerContent: FC = props => { out of the maximum of {' '} - {(props.scorecard as Scorecard).maxScore.toFixed(2)} + {(props.scorecard as Scorecard).maxScore?.toFixed(2)} . You did a good job on passing the scorecard criteria. diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts index 47a1cd66c..a5aeff93d 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts @@ -1,4 +1,4 @@ -import { filter, reduce } from 'lodash' +import { filter, map, reduce } from 'lodash' import { AiFeedbackItem, @@ -13,15 +13,29 @@ import { roundWith2DecimalPlaces } from '../../../utils' export const calcSectionScore = ( section: ScorecardSection, - feedbackItems: Pick[], + feedbackItems: ( + Pick[] | + Pick[] + ), ): number => { const feedbackItemsMap = Object.fromEntries(feedbackItems.map(r => [r.scorecardQuestionId, r])) - return section.questions.reduce((sum, question) => ( - sum + ( - (feedbackItemsMap[question.id as string]?.questionScore ?? 0) / (question.scaleMax || 1) + return section.questions.reduce((sum, question) => { + const item = feedbackItemsMap[question.id as string] + let score = 0 + + if (item && ('initialAnswer' in item || 'finalAnswer' in item)) { + score += Number(item.finalAnswer || item.initialAnswer) || 0 + } + + if (item && 'questionScore' in item) { + score += Number(item.questionScore) || 0 + } + + return sum + ( + score / (question.scaleMax || 1) ) * (question.weight / 100) - ), 0) + }, 0) } export const calcGroupScore = ( diff --git a/src/apps/review/src/lib/models/AiScorecardContext.model.ts b/src/apps/review/src/lib/models/AiScorecardContext.model.ts index 46ba4ccb7..d7e2f9aa1 100644 --- a/src/apps/review/src/lib/models/AiScorecardContext.model.ts +++ b/src/apps/review/src/lib/models/AiScorecardContext.model.ts @@ -11,4 +11,5 @@ export interface AiScorecardContextModel extends ChallengeDetailContextModel { workflowRun?: AiWorkflowRun scorecard?: Scorecard workflowRuns: AiWorkflowRun[] + setSubmissionId: (id: string) => void } diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.module.scss b/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.module.scss new file mode 100644 index 000000000..a588ee379 --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.module.scss @@ -0,0 +1,56 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; +} + +.pageContentWrap { + display: flex; + flex-direction: row; + gap: $sp-10; + + > .sidebar { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 256px; + } + + @include ltelg { + flex-direction: column; + gap: $sp-6; + + > .sidebar { + flex-basis: auto; + width: 100%; + } + } +} + +.contentWrap { + width: 100%; +} + +.summary { + margin-bottom: $sp-6; +} + +.lockedNotice { + padding: $sp-6; + background-color: #f9fafa; + border: 1px solid #D1DAE4; + border-radius: 4px; + margin: $sp-6 0; + + strong { + display: block; + margin-bottom: $sp-2; + font-weight: bold; + } + + span { + display: block; + color: #666; + } +} + diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.tsx b/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.tsx new file mode 100644 index 000000000..f4092fcc1 --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.tsx @@ -0,0 +1,349 @@ +/** + * Reviews Page - combines AiScorecardViewer layout with ScorecardDetailsPage functionality + */ +import { + FC, + useCallback, + useContext, + useEffect, + useMemo, + useState, +} from 'react' +import { useLocation, useParams } from 'react-router-dom' +import classNames from 'classnames' +import useSWR from 'swr' + +import { TableLoading } from '~/apps/admin/src/lib' + +import { + AiWorkflowRunItemsResponse, + useAppNavigate, + useFetchAiWorkflowsRunItems, + useFetchSubmissionReviews, + useFetchSubmissionReviewsProps, + useRole, + useRoleProps, +} from '../../../lib/hooks' +import { + ChallengeDetailContext, + ConfirmModal, + PageWrapper, + ScorecardDetails, +} from '../../../lib' +import { BreadCrumbData, ChallengeDetailContextModel } from '../../../lib/models' +import { SubmissionBarInfo } from '../../../lib/components/SubmissionBarInfo' +import { ChallengeLinksForAdmin } from '../../../lib/components/ChallengeLinksForAdmin' +import { ChallengeLinks } from '../../../lib/components/ChallengeLinks' +import { ADMIN, COPILOT, MANAGER } from '../../../config/index.config' +import { activeReviewAssignmentsRouteId, rootRoute } from '../../../config/routes.config' +import { ScorecardViewer } from '../../../lib/components/Scorecard' +import { useIsEditReview, useIsEditReviewProps } from '../../../lib/hooks/useIsEditReview' +import { fetchReviews } from '../../../lib/services' +import { BackendReview } from '../../../lib/models' +import { AiScorecardContextProvider, useAiScorecardContext } from '../../ai-scorecards/AiScorecardContext' +import ReviewsSidebar from './ReviewsSidebar' + +import styles from './Reviews.module.scss' + +interface Props { + className?: string +} + +export const Reviews: FC = props => { + const navigate = useAppNavigate() + const location = useLocation() + const aiScorecardCtx = useAiScorecardContext() + const { runItems: aiWorkflowRunItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(aiScorecardCtx.workflowId, aiScorecardCtx.workflowRun?.id) + + const { + actionChallengeRole, + myChallengeResources, + myChallengeRoles, + }: useRoleProps = useRole() + const [showCloseConfirmation, setShowCloseConfirmation] = useState(false) + const [isChanged, setIsChanged] = useState(false) + const [isManagerEdit, setIsManagerEdit] = useState(false) + + const { + challengeInfo, + isLoadingChallengeInfo, + }: ChallengeDetailContextModel = useContext(ChallengeDetailContext) + const { isEdit: isEditPhase }: useIsEditReviewProps = useIsEditReview() + + const { + addAppeal, + addAppealResponse, + addManagerComment, + doDeleteAppeal, + mappingAppeals, + isLoading: isLoadingReviewsData, + isSavingReview, + isSavingAppeal, + isSavingAppealResponse, + isSavingManagerComment, + isSubmitterPhaseLocked, + submitterLockedPhaseName, + reviewInfo, + scorecardInfo, + submissionInfo, + saveReviewInfo, + }: useFetchSubmissionReviewsProps = useFetchSubmissionReviews() + + const isLoading = isLoadingReviewsData || !scorecardInfo + + useEffect(() => { + if (submissionInfo?.id) { + aiScorecardCtx.setSubmissionId(submissionInfo.id) + } + }, [submissionInfo]); + + const isReviewCompleted = useMemo( + () => { + const statusUpper = (reviewInfo?.status || '') + .toString() + .toUpperCase() + + if (statusUpper === 'COMPLETED') { + return true + } + + return Boolean(reviewInfo?.committed) + }, + [reviewInfo?.committed, reviewInfo?.status], + ) + + const submitterLockedPhaseDisplay = useMemo( + () => { + if (!submitterLockedPhaseName) { + return 'This review phase' + } + + const trimmed = submitterLockedPhaseName.trim() + if (!trimmed) { + return 'This review phase' + } + + return trimmed.toLowerCase() + .endsWith('phase') + ? trimmed + : `${trimmed} phase` + }, + [submitterLockedPhaseName], + ) + + const isEdit = useMemo( + () => (isEditPhase) && !isReviewCompleted, + [isEditPhase, isReviewCompleted], + ) + + const reviewBreadcrumbLabel = useMemo( + () => submissionInfo?.id + ?? reviewInfo?.submissionId + ?? '', + [reviewInfo?.submissionId, submissionInfo?.id], + ) + const containsPastChallenges = location.pathname.indexOf('/past-challenges/') + + const breadCrumb = useMemo(() => [ + { + index: 1, + label: 'Active Challenges', + path: `${rootRoute}/${activeReviewAssignmentsRouteId}/`, + }, + { + fallback: './../../../../challenge-details', + index: 2, + label: challengeInfo?.name, + path: containsPastChallenges > -1 + ? `${rootRoute}/past-challenges/${challengeInfo?.id}/challenge-details` + : `${rootRoute}/active-challenges/${challengeInfo?.id}/challenge-details`, + }, + { + index: 3, + label: `Review Scorecard - ${reviewBreadcrumbLabel}`, + }, + ], [challengeInfo?.name, challengeInfo?.id, reviewBreadcrumbLabel, containsPastChallenges]) + + /** + * Cancel edit + */ + const onCancelEdit = useCallback(() => { + if (isChanged && isEdit) { + setShowCloseConfirmation(true) + } else { + navigate(-1, { + fallback: './../../../../challenge-details', + }) + } + }, [isChanged, isEdit, navigate]) + + const hasChallengeAdminRole = useMemo( + () => myChallengeResources.some( + resource => resource.roleName?.toLowerCase() === ADMIN.toLowerCase(), + ), + [myChallengeResources], + ) + + const hasTopcoderAdminRole = useMemo( + () => myChallengeRoles.some( + role => role?.toLowerCase() + .includes('admin'), + ), + [myChallengeRoles], + ) + + const hasChallengeManagerRole = useMemo( + () => myChallengeResources.some( + resource => resource.roleName?.toLowerCase() === MANAGER.toLowerCase(), + ), + [myChallengeResources], + ) + + const hasChallengeCopilotRole = useMemo( + () => myChallengeResources.some( + resource => resource.roleName?.toLowerCase() === COPILOT.toLowerCase(), + ), + [myChallengeResources], + ) + + const canEditScorecard = useMemo(() => { + const challengeStatus = (challengeInfo?.status ?? '') + .toString() + .trim() + .toUpperCase() + const isChallengeClosed = challengeStatus.includes('COMPLETED') + || challengeStatus.startsWith('CANCELLED') + + if (isChallengeClosed) { + return false + } + + return Boolean( + reviewInfo?.committed + && (hasChallengeAdminRole + || hasTopcoderAdminRole + || hasChallengeManagerRole + || hasChallengeCopilotRole), + ) + }, [ + challengeInfo?.status, + hasChallengeAdminRole, + hasChallengeCopilotRole, + hasChallengeManagerRole, + hasTopcoderAdminRole, + reviewInfo?.committed, + ]) + + useEffect(() => { + if (!canEditScorecard && isManagerEdit) { + setIsManagerEdit(false) + } + }, [canEditScorecard, isManagerEdit]) + + const toggleManagerEdit = useCallback(() => { + setIsManagerEdit(prev => !prev) + }, []) + + return ( + + {isLoadingChallengeInfo ? ( + + ) : ( + <> +
+ +
+
+ + {actionChallengeRole === ADMIN + || actionChallengeRole === COPILOT + || actionChallengeRole === MANAGER + ? ( + + ) : ( + + )} +
+ + {isSubmitterPhaseLocked && ( +
+ + {submitterLockedPhaseDisplay} + {' '} + is still in progress. + + + Feedback becomes available once the phase closes. Please check back later. + +
+ )} + {!isSubmitterPhaseLocked && ( + aiScorecardCtx.workflowRun ? ( + + ) : ( + + ) + )} +
+
+ + {isEdit && ( + +
Are you sure you want to discard the changes?
+
+ )} + + )} +
+ ) +} + +export default Reviews + diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.module.scss b/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.module.scss new file mode 100644 index 000000000..a4970d041 --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.module.scss @@ -0,0 +1,206 @@ +@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; + gap: $sp-4; + padding: $sp-4; + background-color: #f9fafa; + cursor: pointer; + position: relative; + + 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; + + &Wrap { + flex: 1 1 auto; + } + @include ltelg { + max-width: calc(90vw - 140px); + } +} + +.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; + } + } + } +} + +.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; + } + + @include gtexl { + display: none; + } +} + diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.tsx b/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.tsx new file mode 100644 index 000000000..dc145cf63 --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.tsx @@ -0,0 +1,139 @@ +import { FC, useCallback, useState } from 'react' +import { Link, useParams } from 'react-router-dom' +import classNames from 'classnames' + +import { AiScorecardContextModel } from '~/apps/review/src/lib/models' +import { AiWorkflowRunStatus } from '~/apps/review/src/lib/components/AiReviewsTable' +import { IconAiReview } from '~/apps/review/src/lib/assets/icons' +import { IconOutline, IconSolid } from '~/libs/ui' +import StatusLabel from '~/apps/review/src/lib/components/AiReviewsTable/StatusLabel' + +import { useAiScorecardContext } from '../../ai-scorecards/AiScorecardContext' + +import styles from './ReviewsSidebar.module.scss' + +interface ReviewsSidebarProps { + className?: string +} + +const ReviewsSidebar: FC = props => { + const [isMobileOpen, setIsMobileOpen] = useState(false) + const { reviewId, submissionId } = useParams<{ reviewId: string, submissionId: string }>() + const { + workflow, + workflowRun, + workflowRuns, + workflowId, + submissionId: contextSubmissionId, + }: AiScorecardContextModel = useAiScorecardContext() + + const isReviewActive = !workflowRun; + + const toggleOpen = useCallback(() => { + setIsMobileOpen(wasOpen => !wasOpen) + }, []) + + const close = useCallback(() => { + setIsMobileOpen(false) + }, []) + + const currentSubmissionId = submissionId || contextSubmissionId + + return ( +
+ {workflow && workflowRun && ( +
+
+ + + {workflow.name} + + +
+ +
+
+
+ )} + +
+
+ +
+
+
    + {workflowRuns.map(run => ( +
  • + + + + {run.workflow.name} + + +
  • + ))} + {currentSubmissionId && ( +
  • + + + Review + +
  • + )} +
+
+ +
+
+ Legend +
+
    +
  • + } + label='Passed' + status='passed' + /> +
  • +
  • + } + label='Failed' + status='failed' + /> +
  • +
  • + } + label='To be filled' + status='pending' + /> +
  • +
+
+
+
+ ) +} + +export default ReviewsSidebar + diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsWrapper.tsx b/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsWrapper.tsx new file mode 100644 index 000000000..b718798da --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsWrapper.tsx @@ -0,0 +1,18 @@ +/** + * Wrapper component that provides AiScorecardContext for Reviews + */ +import { FC } from 'react' + +import { AiScorecardContextProvider } from '../../ai-scorecards/AiScorecardContext' +import Reviews from './Reviews' + +const ReviewsWrapper: FC = () => { + return ( + + + + ) +} + +export default ReviewsWrapper + diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/index.ts b/src/apps/review/src/pages/active-review-assignements/Reviews/index.ts new file mode 100644 index 000000000..b4aa6c37e --- /dev/null +++ b/src/apps/review/src/pages/active-review-assignements/Reviews/index.ts @@ -0,0 +1,2 @@ +export { default } from './Reviews' + diff --git a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx index c6cdb25b8..4dd8442ab 100644 --- a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx @@ -32,11 +32,11 @@ import { BreadCrumbData, ChallengeDetailContextModel, ScorecardInfo } from '../. import { SubmissionBarInfo } from '../../../lib/components/SubmissionBarInfo' import { ChallengeLinksForAdmin } from '../../../lib/components/ChallengeLinksForAdmin' import { ADMIN, COPILOT, MANAGER } from '../../../config/index.config' -import { useIsEditReview, useIsEditReviewProps } from '../../../lib/hooks/useIsEditReview' import { activeReviewAssignmentsRouteId, rootRoute } from '../../../config/routes.config' +import { ScorecardViewer } from '../../../lib/components/Scorecard' +import { useIsEditReview, useIsEditReviewProps } from '../../../lib/hooks/useIsEditReview' import styles from './ScorecardDetailsPage.module.scss' -import { ScorecardViewer } from '../../../lib/components/Scorecard' type ReviewPhaseType = | 'screening' @@ -555,44 +555,44 @@ export const ScorecardDetailsPage: FC = (props: Props) => {
) : ( <> - - + + )} 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 e6a61eb14..1c97bc108 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 @@ -17,6 +17,10 @@ const ChallengeDetailsPage: LazyLoadedComponent = lazyLoad( 'ChallengeDetailsPage', ) +const ReviewsWrapper: LazyLoadedComponent = lazyLoad( + () => import('./Reviews/ReviewsWrapper'), +) + export const challengeDetailsChildRoutes = [ { element: , 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 5f6e4169f..b1d01519a 100644 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx +++ b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx @@ -1,8 +1,8 @@ /** * Context provider for challenge detail page */ -import { Context, createContext, FC, PropsWithChildren, useContext, useMemo } from 'react' -import { useParams } from 'react-router-dom' +import { Context, createContext, FC, PropsWithChildren, useContext, useMemo, useState } from 'react' +import { useParams, useSearchParams } from 'react-router-dom' import { ChallengeDetailContext } from '../../../lib' import { AiScorecardContextModel, ChallengeDetailContextModel } from '../../../lib/models' @@ -12,13 +12,16 @@ export const AiScorecardContext: Context = createContext({} as AiScorecardContextModel) export const AiScorecardContextProvider: FC = props => { - const { workflowId = '', submissionId = '' }: { + const { submissionId: submissionIdParam = '' }: { submissionId?: string, - workflowId?: string, } = useParams<{ submissionId: string, - workflowId: string, }>() + const [params] = useSearchParams() + const workflowId = params.get('workflowId') ?? '' + const reviewId = params.get('reviewId') ?? '' + + const [submissionId, setSubmissionId] = useState(submissionIdParam); const challengeDetailsCtx = useContext(ChallengeDetailContext) const { challengeInfo }: ChallengeDetailContextModel = challengeDetailsCtx @@ -53,6 +56,7 @@ export const AiScorecardContextProvider: FC = props => { workflowId, workflowRun, workflowRuns, + setSubmissionId, }), [ challengeDetailsCtx, 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 a911150f2..fe872896b 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 @@ -29,6 +29,6 @@ export const aiScorecardRoutes: ReadonlyArray = [ rolesRequired: [ // UserRole.administrator, ], - route: `${aiScorecardRouteId}/:submissionId/:workflowId`, + route: `${aiScorecardRouteId}/:submissionId`, }, ] From 621d909eff871d6edf2fb26d3d000c758df27b13 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 11 Nov 2025 16:42:00 +0200 Subject: [PATCH 043/125] PM-2178 - refactor routes to /reviews --- src/apps/review/src/config/routes.config.ts | 1 + .../ScorecardViewer/ScorecardViewer.tsx | 19 +- .../ChallengeDetailContextProvider.tsx | 4 +- .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 3 +- .../lib/hooks/useFetchSubmissionReviews.ts | 7 +- .../src/lib/models/ReviewsContext.model.ts | 15 + src/apps/review/src/lib/models/index.ts | 1 + .../Reviews/Reviews.tsx | 349 --------------- .../Reviews/ReviewsWrapper.tsx | 18 - .../Reviews/index.ts | 2 - .../ScorecardDetailsPage.tsx | 59 +-- .../challenge-details.routes.tsx | 8 +- .../ReviewsContext/ReviewsContextProvider.tsx | 79 ++++ .../src/pages/reviews/ReviewsContext/index.ts | 1 + .../ReviewsViewer/ReviewsViewer.module.scss | 37 ++ .../reviews/ReviewsViewer/ReviewsViewer.tsx | 70 +++ .../src/pages/reviews/ReviewsViewer/index.ts | 1 + .../pages/reviews/components/AiModelIcon.tsx | 27 ++ .../AiModelModal/AiModelModal.module.scss | 66 +++ .../components/AiModelModal/AiModelModal.tsx | 44 ++ .../reviews/components/AiModelModal/index.ts | 1 + .../AiReviewViewer/AiReviewViewer.module.scss | 10 + .../AiReviewViewer/AiReviewViewer.tsx | 50 +++ .../components/AiReviewViewer/index.ts | 1 + .../ReviewViewer/ReviewViewer.module.scss} | 112 ++--- .../components/ReviewViewer/ReviewViewer.tsx | 254 +++++++++++ .../reviews/components/ReviewViewer/index.ts | 1 + .../ReviewsSidebar.module.scss | 411 +++++++++--------- .../ReviewsSidebar}/ReviewsSidebar.tsx | 23 +- .../components/ReviewsSidebar/index.ts | 1 + .../ScorecardHeader.module.scss | 133 ++++++ .../ScorecardHeader/ScorecardHeader.tsx | 87 ++++ .../components/ScorecardHeader/index.ts | 1 + .../src/pages/reviews/components/index.ts | 1 + src/apps/review/src/pages/reviews/index.ts | 1 + .../src/pages/reviews/reviews.routes.tsx | 31 ++ 36 files changed, 1222 insertions(+), 707 deletions(-) create mode 100644 src/apps/review/src/lib/models/ReviewsContext.model.ts delete mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.tsx delete mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsWrapper.tsx delete mode 100644 src/apps/review/src/pages/active-review-assignements/Reviews/index.ts create mode 100644 src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx create mode 100644 src/apps/review/src/pages/reviews/ReviewsContext/index.ts create mode 100644 src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.module.scss create mode 100644 src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx create mode 100644 src/apps/review/src/pages/reviews/ReviewsViewer/index.ts create mode 100644 src/apps/review/src/pages/reviews/components/AiModelIcon.tsx create mode 100644 src/apps/review/src/pages/reviews/components/AiModelModal/AiModelModal.module.scss create mode 100644 src/apps/review/src/pages/reviews/components/AiModelModal/AiModelModal.tsx create mode 100644 src/apps/review/src/pages/reviews/components/AiModelModal/index.ts create mode 100644 src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.module.scss create mode 100644 src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx create mode 100644 src/apps/review/src/pages/reviews/components/AiReviewViewer/index.ts rename src/apps/review/src/pages/{active-review-assignements/Reviews/Reviews.module.scss => reviews/components/ReviewViewer/ReviewViewer.module.scss} (93%) create mode 100644 src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx create mode 100644 src/apps/review/src/pages/reviews/components/ReviewViewer/index.ts rename src/apps/review/src/pages/{active-review-assignements/Reviews => reviews/components/ReviewsSidebar}/ReviewsSidebar.module.scss (94%) rename src/apps/review/src/pages/{active-review-assignements/Reviews => reviews/components/ReviewsSidebar}/ReviewsSidebar.tsx (88%) create mode 100644 src/apps/review/src/pages/reviews/components/ReviewsSidebar/index.ts create mode 100644 src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss create mode 100644 src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx create mode 100644 src/apps/review/src/pages/reviews/components/ScorecardHeader/index.ts create mode 100644 src/apps/review/src/pages/reviews/components/index.ts create mode 100644 src/apps/review/src/pages/reviews/index.ts create mode 100644 src/apps/review/src/pages/reviews/reviews.routes.tsx diff --git a/src/apps/review/src/config/routes.config.ts b/src/apps/review/src/config/routes.config.ts index a5bbdf1ca..8fd655e70 100644 --- a/src/apps/review/src/config/routes.config.ts +++ b/src/apps/review/src/config/routes.config.ts @@ -14,4 +14,5 @@ export const pastReviewAssignmentsRouteId = 'past-challenges' export const challengeDetailRouteId = ':challengeId' export const scorecardRouteId = 'scorecard' export const aiScorecardRouteId = 'scorecard' +export const reviewsRouteId = 'reviews' export const viewScorecardRouteId = ':scorecardId' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index d7ac46554..edc8013c2 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -109,6 +109,7 @@ const ScorecardViewerContent: FC = props => { : 'There were validation errors. Check above.' const onSubmit = useCallback(() => { + debugger if (props.saveReviewInfo) { props.saveReviewInfo( isDirty ? form?.getValues() : undefined, @@ -188,21 +189,20 @@ const ScorecardViewerContent: FC = props => {
)} - {props.reviewInfo && ( - - {props.scorecard.scorecardGroups.map((group, index) => ( + + {props.reviewInfo && ( + props.scorecard.scorecardGroups.map((group, index) => ( - ))} - - )} + )) + )} {!props.reviewInfo && props.scorecard.scorecardGroups.map((group, index) => ( = props => {
)} + = props => { ) } + +export const useChallengeDetailsContext = (): ChallengeDetailContextModel => useContext(ChallengeDetailContext) diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 3120c869a..5bb434738 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -77,7 +77,6 @@ export function useFetchAiWorkflowsRuns( workflowIds: string[], ): AiWorkflowRunsResponse { const { isAdmin }: UseRolePermissionsResult = useRolePermissions() - // Use swr hooks for challenge info fetching const { data: runs = [], @@ -112,7 +111,7 @@ export function useFetchAiWorkflowsRuns( } export function useFetchAiWorkflowsRunItems( - workflowId: string, + workflowId: string | undefined, runId: string | undefined, ): AiWorkflowRunItemsResponse { // Use swr hooks for challenge info fetching diff --git a/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts b/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts index 2abb6dc78..5e0715778 100644 --- a/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts +++ b/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts @@ -340,15 +340,12 @@ export interface useFetchSubmissionReviewsProps { * Fetch reviews of submission * @returns reviews info */ -export function useFetchSubmissionReviews(): useFetchSubmissionReviewsProps { +export function useFetchSubmissionReviews(reviewId: string = ''): useFetchSubmissionReviewsProps { const [isSavingReview, setIsSavingReview] = useState(false) const [isSavingAppeal, setIsSavingAppeal] = useState(false) const [isSavingAppealResponse, setIsSavingAppealResponse] = useState(false) const [isSavingManagerComment, setIsSavingManagerComment] = useState(false) const { actionChallengeRole }: useRoleProps = useRole() - const { reviewId = '' }: { reviewId?: string } = useParams<{ - reviewId: string - }>() const { challengeId: contextChallengeId, @@ -531,7 +528,7 @@ export function useFetchSubmissionReviews(): useFetchSubmissionReviewsProps { error: fetchSubmissionError, isValidating: isLoadingSubmission, }: SWRResponse = useSWR( - `EnvironmentConfig.API.V6/submissions/${submissionId}`, + `/submissions/${submissionId}`, { fetcher: () => fetchSubmission(submissionId), isPaused: () => !submissionId, diff --git a/src/apps/review/src/lib/models/ReviewsContext.model.ts b/src/apps/review/src/lib/models/ReviewsContext.model.ts new file mode 100644 index 000000000..426bdf189 --- /dev/null +++ b/src/apps/review/src/lib/models/ReviewsContext.model.ts @@ -0,0 +1,15 @@ +import { AiWorkflow, AiWorkflowRun } from '../hooks' + +import { Scorecard } from './Scorecard.model' +import { ChallengeDetailContextModel } from './ChallengeDetailContextModel.model' + +export interface ReviewsContextModel extends ChallengeDetailContextModel { + isLoading: boolean + reviewId?: string; + submissionId: string + workflowId?: string + workflow?: AiWorkflow + workflowRun?: AiWorkflowRun + 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 79462b78d..38e77c79a 100644 --- a/src/apps/review/src/lib/models/index.ts +++ b/src/apps/review/src/lib/models/index.ts @@ -62,3 +62,4 @@ export * from './BackendReviewItem.model' export * from './BackendRequestReviewItem.model' export * from './BackendAppealResponse.model' export * from './ReviewItemComment.model' +export * from './ReviewsContext.model' diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.tsx b/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.tsx deleted file mode 100644 index f4092fcc1..000000000 --- a/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.tsx +++ /dev/null @@ -1,349 +0,0 @@ -/** - * Reviews Page - combines AiScorecardViewer layout with ScorecardDetailsPage functionality - */ -import { - FC, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react' -import { useLocation, useParams } from 'react-router-dom' -import classNames from 'classnames' -import useSWR from 'swr' - -import { TableLoading } from '~/apps/admin/src/lib' - -import { - AiWorkflowRunItemsResponse, - useAppNavigate, - useFetchAiWorkflowsRunItems, - useFetchSubmissionReviews, - useFetchSubmissionReviewsProps, - useRole, - useRoleProps, -} from '../../../lib/hooks' -import { - ChallengeDetailContext, - ConfirmModal, - PageWrapper, - ScorecardDetails, -} from '../../../lib' -import { BreadCrumbData, ChallengeDetailContextModel } from '../../../lib/models' -import { SubmissionBarInfo } from '../../../lib/components/SubmissionBarInfo' -import { ChallengeLinksForAdmin } from '../../../lib/components/ChallengeLinksForAdmin' -import { ChallengeLinks } from '../../../lib/components/ChallengeLinks' -import { ADMIN, COPILOT, MANAGER } from '../../../config/index.config' -import { activeReviewAssignmentsRouteId, rootRoute } from '../../../config/routes.config' -import { ScorecardViewer } from '../../../lib/components/Scorecard' -import { useIsEditReview, useIsEditReviewProps } from '../../../lib/hooks/useIsEditReview' -import { fetchReviews } from '../../../lib/services' -import { BackendReview } from '../../../lib/models' -import { AiScorecardContextProvider, useAiScorecardContext } from '../../ai-scorecards/AiScorecardContext' -import ReviewsSidebar from './ReviewsSidebar' - -import styles from './Reviews.module.scss' - -interface Props { - className?: string -} - -export const Reviews: FC = props => { - const navigate = useAppNavigate() - const location = useLocation() - const aiScorecardCtx = useAiScorecardContext() - const { runItems: aiWorkflowRunItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(aiScorecardCtx.workflowId, aiScorecardCtx.workflowRun?.id) - - const { - actionChallengeRole, - myChallengeResources, - myChallengeRoles, - }: useRoleProps = useRole() - const [showCloseConfirmation, setShowCloseConfirmation] = useState(false) - const [isChanged, setIsChanged] = useState(false) - const [isManagerEdit, setIsManagerEdit] = useState(false) - - const { - challengeInfo, - isLoadingChallengeInfo, - }: ChallengeDetailContextModel = useContext(ChallengeDetailContext) - const { isEdit: isEditPhase }: useIsEditReviewProps = useIsEditReview() - - const { - addAppeal, - addAppealResponse, - addManagerComment, - doDeleteAppeal, - mappingAppeals, - isLoading: isLoadingReviewsData, - isSavingReview, - isSavingAppeal, - isSavingAppealResponse, - isSavingManagerComment, - isSubmitterPhaseLocked, - submitterLockedPhaseName, - reviewInfo, - scorecardInfo, - submissionInfo, - saveReviewInfo, - }: useFetchSubmissionReviewsProps = useFetchSubmissionReviews() - - const isLoading = isLoadingReviewsData || !scorecardInfo - - useEffect(() => { - if (submissionInfo?.id) { - aiScorecardCtx.setSubmissionId(submissionInfo.id) - } - }, [submissionInfo]); - - const isReviewCompleted = useMemo( - () => { - const statusUpper = (reviewInfo?.status || '') - .toString() - .toUpperCase() - - if (statusUpper === 'COMPLETED') { - return true - } - - return Boolean(reviewInfo?.committed) - }, - [reviewInfo?.committed, reviewInfo?.status], - ) - - const submitterLockedPhaseDisplay = useMemo( - () => { - if (!submitterLockedPhaseName) { - return 'This review phase' - } - - const trimmed = submitterLockedPhaseName.trim() - if (!trimmed) { - return 'This review phase' - } - - return trimmed.toLowerCase() - .endsWith('phase') - ? trimmed - : `${trimmed} phase` - }, - [submitterLockedPhaseName], - ) - - const isEdit = useMemo( - () => (isEditPhase) && !isReviewCompleted, - [isEditPhase, isReviewCompleted], - ) - - const reviewBreadcrumbLabel = useMemo( - () => submissionInfo?.id - ?? reviewInfo?.submissionId - ?? '', - [reviewInfo?.submissionId, submissionInfo?.id], - ) - const containsPastChallenges = location.pathname.indexOf('/past-challenges/') - - const breadCrumb = useMemo(() => [ - { - index: 1, - label: 'Active Challenges', - path: `${rootRoute}/${activeReviewAssignmentsRouteId}/`, - }, - { - fallback: './../../../../challenge-details', - index: 2, - label: challengeInfo?.name, - path: containsPastChallenges > -1 - ? `${rootRoute}/past-challenges/${challengeInfo?.id}/challenge-details` - : `${rootRoute}/active-challenges/${challengeInfo?.id}/challenge-details`, - }, - { - index: 3, - label: `Review Scorecard - ${reviewBreadcrumbLabel}`, - }, - ], [challengeInfo?.name, challengeInfo?.id, reviewBreadcrumbLabel, containsPastChallenges]) - - /** - * Cancel edit - */ - const onCancelEdit = useCallback(() => { - if (isChanged && isEdit) { - setShowCloseConfirmation(true) - } else { - navigate(-1, { - fallback: './../../../../challenge-details', - }) - } - }, [isChanged, isEdit, navigate]) - - const hasChallengeAdminRole = useMemo( - () => myChallengeResources.some( - resource => resource.roleName?.toLowerCase() === ADMIN.toLowerCase(), - ), - [myChallengeResources], - ) - - const hasTopcoderAdminRole = useMemo( - () => myChallengeRoles.some( - role => role?.toLowerCase() - .includes('admin'), - ), - [myChallengeRoles], - ) - - const hasChallengeManagerRole = useMemo( - () => myChallengeResources.some( - resource => resource.roleName?.toLowerCase() === MANAGER.toLowerCase(), - ), - [myChallengeResources], - ) - - const hasChallengeCopilotRole = useMemo( - () => myChallengeResources.some( - resource => resource.roleName?.toLowerCase() === COPILOT.toLowerCase(), - ), - [myChallengeResources], - ) - - const canEditScorecard = useMemo(() => { - const challengeStatus = (challengeInfo?.status ?? '') - .toString() - .trim() - .toUpperCase() - const isChallengeClosed = challengeStatus.includes('COMPLETED') - || challengeStatus.startsWith('CANCELLED') - - if (isChallengeClosed) { - return false - } - - return Boolean( - reviewInfo?.committed - && (hasChallengeAdminRole - || hasTopcoderAdminRole - || hasChallengeManagerRole - || hasChallengeCopilotRole), - ) - }, [ - challengeInfo?.status, - hasChallengeAdminRole, - hasChallengeCopilotRole, - hasChallengeManagerRole, - hasTopcoderAdminRole, - reviewInfo?.committed, - ]) - - useEffect(() => { - if (!canEditScorecard && isManagerEdit) { - setIsManagerEdit(false) - } - }, [canEditScorecard, isManagerEdit]) - - const toggleManagerEdit = useCallback(() => { - setIsManagerEdit(prev => !prev) - }, []) - - return ( - - {isLoadingChallengeInfo ? ( - - ) : ( - <> -
- -
-
- - {actionChallengeRole === ADMIN - || actionChallengeRole === COPILOT - || actionChallengeRole === MANAGER - ? ( - - ) : ( - - )} -
- - {isSubmitterPhaseLocked && ( -
- - {submitterLockedPhaseDisplay} - {' '} - is still in progress. - - - Feedback becomes available once the phase closes. Please check back later. - -
- )} - {!isSubmitterPhaseLocked && ( - aiScorecardCtx.workflowRun ? ( - - ) : ( - - ) - )} -
-
- - {isEdit && ( - -
Are you sure you want to discard the changes?
-
- )} - - )} -
- ) -} - -export default Reviews - diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsWrapper.tsx b/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsWrapper.tsx deleted file mode 100644 index b718798da..000000000 --- a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsWrapper.tsx +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Wrapper component that provides AiScorecardContext for Reviews - */ -import { FC } from 'react' - -import { AiScorecardContextProvider } from '../../ai-scorecards/AiScorecardContext' -import Reviews from './Reviews' - -const ReviewsWrapper: FC = () => { - return ( - - - - ) -} - -export default ReviewsWrapper - diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/index.ts b/src/apps/review/src/pages/active-review-assignements/Reviews/index.ts deleted file mode 100644 index b4aa6c37e..000000000 --- a/src/apps/review/src/pages/active-review-assignements/Reviews/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './Reviews' - diff --git a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx index 4dd8442ab..07fcb69f1 100644 --- a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx @@ -554,46 +554,25 @@ export const ScorecardDetailsPage: FC = (props: Props) => {
) : ( - <> - - - + )} {isEdit && ( 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 1c97bc108..134a179a5 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,7 +1,7 @@ import { getRoutesContainer, lazyLoad, LazyLoadedComponent } from '~/libs/core' import { challengeDetailRouteId } from '../../config/routes.config' -import { aiScorecardRoutes } from '../ai-scorecards' +import { reviewsRoutes } from '../reviews' const ChallengeDetailContextProvider: LazyLoadedComponent = lazyLoad( () => import('../../lib/contexts/ChallengeDetailContextProvider'), @@ -17,10 +17,6 @@ const ChallengeDetailsPage: LazyLoadedComponent = lazyLoad( 'ChallengeDetailsPage', ) -const ReviewsWrapper: LazyLoadedComponent = lazyLoad( - () => import('./Reviews/ReviewsWrapper'), -) - export const challengeDetailsChildRoutes = [ { element: , @@ -32,7 +28,7 @@ export const challengeDetailsChildRoutes = [ id: 'scorecard-details-page', route: 'review/:reviewId', }, - ...aiScorecardRoutes, + ...reviewsRoutes, ] export const challengeDetailsRoutes = [ diff --git a/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx b/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx new file mode 100644 index 000000000..e482d650d --- /dev/null +++ b/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx @@ -0,0 +1,79 @@ +/** + * Context provider for challenge detail page + */ +import { Context, createContext, FC, PropsWithChildren, useContext, useMemo, useState } from 'react' +import { useParams, useSearchParams } from 'react-router-dom' + +import { ChallengeDetailContext } from '../../../lib' +import { ReviewsContextModel, ChallengeDetailContextModel } from '../../../lib/models' +import { AiWorkflowRunsResponse, useFetchAiWorkflowsRuns } from '../../../lib/hooks' + +export const ReviewsContext: Context + = createContext({} as ReviewsContextModel) + +export const ReviewsContextProvider: FC = props => { + const { submissionId = '' }: { + submissionId?: string, + } = useParams<{ + submissionId: string, + }>() + const [searchParams] = useSearchParams() + const workflowId = searchParams.get('workflowId') ?? '' + const reviewId = searchParams.get('reviewId') ?? '' + + 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 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, + reviewId, + scorecard, + submissionId, + workflow, + workflowId, + workflowRun, + workflowRuns, + }), + [ + challengeDetailsCtx, + isLoadingCtxData, + reviewId,, + scorecard, + submissionId, + workflow, + workflowId, + workflowRun, + workflowRuns, + ], + ) + + return ( + + {props.children} + + ) +} + +export const useReviewsContext = (): ReviewsContextModel => useContext(ReviewsContext) diff --git a/src/apps/review/src/pages/reviews/ReviewsContext/index.ts b/src/apps/review/src/pages/reviews/ReviewsContext/index.ts new file mode 100644 index 000000000..b3a7bc5c2 --- /dev/null +++ b/src/apps/review/src/pages/reviews/ReviewsContext/index.ts @@ -0,0 +1 @@ +export * from './ReviewsContextProvider' diff --git a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.module.scss b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.module.scss new file mode 100644 index 000000000..1b3b36e66 --- /dev/null +++ b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.module.scss @@ -0,0 +1,37 @@ +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; +} + +.pageContentWrap { + display: flex; + flex-direction: row; + gap: $sp-10; + + > .sidebar { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 256px; + } + + @include ltelg { + flex-direction: column; + gap: $sp-6; + + > .sidebar { + flex-basis: auto; + width: 100%; + } + } +} + +.contentWrap { + width: 100%; +} + +.tabs { + justify-content: center; + margin: $sp-6 0; +} diff --git a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx new file mode 100644 index 000000000..9f458fdc2 --- /dev/null +++ b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx @@ -0,0 +1,70 @@ +import { FC, useEffect, useMemo } from 'react' + +import { NotificationContextType, useNotification } from '~/libs/shared' + +import { IconAiReview } from '../../../lib/assets/icons' +import { PageWrapper } from '../../../lib' +import { BreadCrumbData, ReviewsContextModel } from '../../../lib/models' +import { ReviewsSidebar } from '../components/ReviewsSidebar' +import { useReviewsContext } from '../ReviewsContext' + +import styles from './ReviewsViewer.module.scss' +import { AiReviewViewer } from '../components/AiReviewViewer' +import { activeReviewAssignmentsRouteId, rootRoute } from '../../../config/routes.config' +import { ReviewViewer } from '../components/ReviewViewer' + + +const ReviewsViewer: FC = () => { + const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() + const { challengeInfo, submissionId, workflowRun }: ReviewsContextModel = useReviewsContext() + + const containsPastChallenges = location.pathname.indexOf('/past-challenges/') + + const breadCrumb = useMemo(() => [ + { + index: 1, + label: 'Active Challenges', + path: `${rootRoute}/${activeReviewAssignmentsRouteId}/`, + }, + { + fallback: './../../../../challenge-details', + index: 2, + label: challengeInfo?.name, + path: containsPastChallenges > -1 + ? `${rootRoute}/past-challenges/${challengeInfo?.id}/challenge-details` + : `${rootRoute}/active-challenges/${challengeInfo?.id}/challenge-details`, + }, + { + index: 3, + label: `Review Scorecard - ${submissionId}`, + }, + ], [challengeInfo?.name, challengeInfo?.id, submissionId, containsPastChallenges]) + + 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 ( + +
+ +
+ {!!workflowRun && } + {!workflowRun && } +
+
+
+ ) +} + +export default ReviewsViewer diff --git a/src/apps/review/src/pages/reviews/ReviewsViewer/index.ts b/src/apps/review/src/pages/reviews/ReviewsViewer/index.ts new file mode 100644 index 000000000..02d5af262 --- /dev/null +++ b/src/apps/review/src/pages/reviews/ReviewsViewer/index.ts @@ -0,0 +1 @@ +export { default as ReviewsViewer } from './ReviewsViewer' diff --git a/src/apps/review/src/pages/reviews/components/AiModelIcon.tsx b/src/apps/review/src/pages/reviews/components/AiModelIcon.tsx new file mode 100644 index 000000000..9d296780d --- /dev/null +++ b/src/apps/review/src/pages/reviews/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/reviews/components/AiModelModal/AiModelModal.module.scss b/src/apps/review/src/pages/reviews/components/AiModelModal/AiModelModal.module.scss new file mode 100644 index 000000000..3f0514beb --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/AiModelModal/AiModelModal.module.scss @@ -0,0 +1,66 @@ +@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 { + 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; + } + + @include ltemd { + h3 { + font-size: 22px; + line-height: 26px; + } + } +} + +.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/reviews/components/AiModelModal/AiModelModal.tsx b/src/apps/review/src/pages/reviews/components/AiModelModal/AiModelModal.tsx new file mode 100644 index 000000000..8aa2e02ae --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/AiModelModal/AiModelModal.tsx @@ -0,0 +1,44 @@ +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 AiModelIcon from '../AiModelIcon' + +import styles from './AiModelModal.module.scss' + +interface AiModelModalProps { + model: AiWorkflow['llm'] + onClose: () => void +} + +const AiModelModal: FC = props => ( + +
+
+
+ +
+
+

{props.model.name}

+ + + +
+
+ +

+ {props.model.description} +

+
+
+) + +export default AiModelModal diff --git a/src/apps/review/src/pages/reviews/components/AiModelModal/index.ts b/src/apps/review/src/pages/reviews/components/AiModelModal/index.ts new file mode 100644 index 000000000..948754b83 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/AiModelModal/index.ts @@ -0,0 +1 @@ +export { default as AiModelModal } from './AiModelModal' diff --git a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.module.scss b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.module.scss new file mode 100644 index 000000000..1530f73d8 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.module.scss @@ -0,0 +1,10 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + width: 100%; +} + +.tabs { + justify-content: center; + margin: $sp-6 0; +} diff --git a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx new file mode 100644 index 000000000..4e1b91213 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx @@ -0,0 +1,50 @@ +import { FC, useState } from 'react' + +import styles from './AiReviewViewer.module.scss' +import { ScorecardHeader } from '../ScorecardHeader' +import { Tabs } from '~/apps/review/src/lib' +import { ScorecardViewer } from '~/apps/review/src/lib/components/Scorecard' +import { ScorecardAttachments } from '~/apps/review/src/lib/components/Scorecard/ScorecardAttachments' +import { AiWorkflowRunItemsResponse, useFetchAiWorkflowsRunItems } from '~/apps/review/src/lib/hooks' +import { ReviewsContextModel, SelectOption } from '~/apps/review/src/lib/models' +import { useReviewsContext } from '../../ReviewsContext' + +interface AiReviewViewerProps { +} + +const tabItems: SelectOption[] = [ + { label: 'Scorecard', value: 'scorecard' }, + { label: 'Attachments', value: 'attachments' }, +] + +const AiReviewViewer: FC = props => { + const { scorecard, workflowId, workflowRun }: ReviewsContextModel = useReviewsContext() + const [selectedTab, setSelectedTab] = useState('scorecard') + const { runItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(workflowId, workflowRun?.id) + + return ( +
+ + + + {!!scorecard && selectedTab === 'scorecard' && ( + + )} + + {selectedTab === 'attachments' && ( + + )} +
+ ) +} + +export default AiReviewViewer diff --git a/src/apps/review/src/pages/reviews/components/AiReviewViewer/index.ts b/src/apps/review/src/pages/reviews/components/AiReviewViewer/index.ts new file mode 100644 index 000000000..ca168e139 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/AiReviewViewer/index.ts @@ -0,0 +1 @@ +export { default as AiReviewViewer } from './AiReviewViewer' diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.module.scss b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.module.scss similarity index 93% rename from src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.module.scss rename to src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.module.scss index a588ee379..ed9f89e8f 100644 --- a/src/apps/review/src/pages/active-review-assignements/Reviews/Reviews.module.scss +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.module.scss @@ -1,56 +1,56 @@ -@import '@libs/ui/styles/includes'; - -.container { - display: flex; - flex-direction: column; -} - -.pageContentWrap { - display: flex; - flex-direction: row; - gap: $sp-10; - - > .sidebar { - flex-grow: 0; - flex-shrink: 0; - flex-basis: 256px; - } - - @include ltelg { - flex-direction: column; - gap: $sp-6; - - > .sidebar { - flex-basis: auto; - width: 100%; - } - } -} - -.contentWrap { - width: 100%; -} - -.summary { - margin-bottom: $sp-6; -} - -.lockedNotice { - padding: $sp-6; - background-color: #f9fafa; - border: 1px solid #D1DAE4; - border-radius: 4px; - margin: $sp-6 0; - - strong { - display: block; - margin-bottom: $sp-2; - font-weight: bold; - } - - span { - display: block; - color: #666; - } -} - +@import '@libs/ui/styles/includes'; + +.container { + display: flex; + flex-direction: column; +} + +.pageContentWrap { + display: flex; + flex-direction: row; + gap: $sp-10; + + > .sidebar { + flex-grow: 0; + flex-shrink: 0; + flex-basis: 256px; + } + + @include ltelg { + flex-direction: column; + gap: $sp-6; + + > .sidebar { + flex-basis: auto; + width: 100%; + } + } +} + +.contentWrap { + width: 100%; +} + +.summary { + margin-bottom: $sp-6; +} + +.lockedNotice { + padding: $sp-6; + background-color: #f9fafa; + border: 1px solid #D1DAE4; + border-radius: 4px; + margin: $sp-6 0; + + strong { + display: block; + margin-bottom: $sp-2; + font-weight: bold; + } + + span { + display: block; + color: #666; + } +} + diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx new file mode 100644 index 000000000..c17d82815 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx @@ -0,0 +1,254 @@ +import { FC, useCallback, useEffect, useMemo, useState } from 'react' + +import styles from './ReviewViewer.module.scss' +import { SubmissionBarInfo } from '~/apps/review/src/lib/components/SubmissionBarInfo' +import { ChallengeLinksForAdmin } from '~/apps/review/src/lib/components/ChallengeLinksForAdmin' +import { ScorecardViewer } from '~/apps/review/src/lib/components/Scorecard' +import { useAppNavigate, useFetchSubmissionReviews, useFetchSubmissionReviewsProps, useRole, useRoleProps } from '~/apps/review/src/lib/hooks' +import { ChallengeDetailContextModel, ReviewsContextModel } from '~/apps/review/src/lib/models' +import { ChallengeLinks, ConfirmModal, useChallengeDetailsContext } from '~/apps/review/src/lib' +import { useIsEditReview, useIsEditReviewProps } from '~/apps/review/src/lib/hooks/useIsEditReview' +import { ADMIN, COPILOT, MANAGER } from '../../../../config/index.config' +import { useReviewsContext } from '../../ReviewsContext' + +interface ReviewViewerProps { +} + +const ReviewViewer: FC = props => { + const navigate = useAppNavigate() + const { reviewId }: ReviewsContextModel = useReviewsContext() + + const { + actionChallengeRole, + myChallengeResources, + myChallengeRoles, + }: useRoleProps = useRole() + const [showCloseConfirmation, setShowCloseConfirmation] = useState(false) + const [isChanged, setIsChanged] = useState(false) + const [isManagerEdit, setIsManagerEdit] = useState(false) + + const { + challengeInfo, + isLoadingChallengeInfo, + }: ChallengeDetailContextModel = useChallengeDetailsContext() + const { isEdit: isEditPhase }: useIsEditReviewProps = useIsEditReview() + + const { + addAppeal, + addAppealResponse, + addManagerComment, + doDeleteAppeal, + mappingAppeals, + isLoading: isLoadingReviewsData, + isSavingReview, + isSavingAppeal, + isSavingAppealResponse, + isSavingManagerComment, + isSubmitterPhaseLocked, + submitterLockedPhaseName, + reviewInfo, + scorecardInfo, + submissionInfo, + saveReviewInfo, + }: useFetchSubmissionReviewsProps = useFetchSubmissionReviews(reviewId) + + const isLoading = isLoadingReviewsData || !scorecardInfo + + const isReviewCompleted = useMemo( + () => { + const statusUpper = (reviewInfo?.status || '') + .toString() + .toUpperCase() + + if (statusUpper === 'COMPLETED') { + return true + } + + return Boolean(reviewInfo?.committed) + }, + [reviewInfo?.committed, reviewInfo?.status], + ) + + const submitterLockedPhaseDisplay = useMemo( + () => { + if (!submitterLockedPhaseName) { + return 'This review phase' + } + + const trimmed = submitterLockedPhaseName.trim() + if (!trimmed) { + return 'This review phase' + } + + return trimmed.toLowerCase() + .endsWith('phase') + ? trimmed + : `${trimmed} phase` + }, + [submitterLockedPhaseName], + ) + + const isEdit = useMemo( + () => (isEditPhase) && !isReviewCompleted, + [isEditPhase, isReviewCompleted], + ) + + /** + * Cancel edit + */ + const onCancelEdit = useCallback(() => { + if (isChanged && isEdit) { + setShowCloseConfirmation(true) + } else { + navigate(-1, { + fallback: './../../../../challenge-details', + }) + } + }, [isChanged, isEdit, navigate]) + + const hasChallengeAdminRole = useMemo( + () => myChallengeResources.some( + resource => resource.roleName?.toLowerCase() === ADMIN.toLowerCase(), + ), + [myChallengeResources], + ) + + const hasTopcoderAdminRole = useMemo( + () => myChallengeRoles.some( + role => role?.toLowerCase() + .includes('admin'), + ), + [myChallengeRoles], + ) + + const hasChallengeManagerRole = useMemo( + () => myChallengeResources.some( + resource => resource.roleName?.toLowerCase() === MANAGER.toLowerCase(), + ), + [myChallengeResources], + ) + + const hasChallengeCopilotRole = useMemo( + () => myChallengeResources.some( + resource => resource.roleName?.toLowerCase() === COPILOT.toLowerCase(), + ), + [myChallengeResources], + ) + + const canEditScorecard = useMemo(() => { + const challengeStatus = (challengeInfo?.status ?? '') + .toString() + .trim() + .toUpperCase() + const isChallengeClosed = challengeStatus.includes('COMPLETED') + || challengeStatus.startsWith('CANCELLED') + + if (isChallengeClosed) { + return false + } + + return Boolean( + reviewInfo?.committed + && (hasChallengeAdminRole + || hasTopcoderAdminRole + || hasChallengeManagerRole + || hasChallengeCopilotRole), + ) + }, [ + challengeInfo?.status, + hasChallengeAdminRole, + hasChallengeCopilotRole, + hasChallengeManagerRole, + hasTopcoderAdminRole, + reviewInfo?.committed, + ]) + + useEffect(() => { + if (!canEditScorecard && isManagerEdit) { + setIsManagerEdit(false) + } + }, [canEditScorecard, isManagerEdit]) + + const toggleManagerEdit = useCallback(() => { + setIsManagerEdit(prev => !prev) + }, []) + + return ( +
+
+
+ + {actionChallengeRole === ADMIN + || actionChallengeRole === COPILOT + || actionChallengeRole === MANAGER + ? ( + + ) : ( + + )} +
+ + {isSubmitterPhaseLocked && ( +
+ + {submitterLockedPhaseDisplay} + {' '} + is still in progress. + + + Feedback becomes available once the phase closes. Please check back later. + +
+ )} + {!isSubmitterPhaseLocked && ( + + )} +
+ {isEdit && ( + +
Are you sure you want to discard the changes?
+
+ )} +
+ ) +} + +export default ReviewViewer diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/index.ts b/src/apps/review/src/pages/reviews/components/ReviewViewer/index.ts new file mode 100644 index 000000000..e004da63a --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/index.ts @@ -0,0 +1 @@ +export { default as ReviewViewer } from './ReviewViewer' diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.module.scss b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.module.scss similarity index 94% rename from src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.module.scss rename to src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.module.scss index a4970d041..54197a0e6 100644 --- a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.module.scss +++ b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.module.scss @@ -1,206 +1,205 @@ -@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; - gap: $sp-4; - padding: $sp-4; - background-color: #f9fafa; - cursor: pointer; - position: relative; - - 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; - - &Wrap { - flex: 1 1 auto; - } - @include ltelg { - max-width: calc(90vw - 140px); - } -} - -.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; - } - } - } -} - -.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; - } - - @include gtexl { - display: none; - } -} - +@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; + gap: $sp-4; + padding: $sp-4; + background-color: #f9fafa; + cursor: pointer; + position: relative; + + 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; + + &Wrap { + flex: 1 1 auto; + } + @include ltelg { + max-width: calc(90vw - 140px); + } +} + +.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; + } + } + } +} + +.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; + } + + @include gtexl { + display: none; + } +} diff --git a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.tsx b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx similarity index 88% rename from src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.tsx rename to src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx index dc145cf63..ab6bcc95a 100644 --- a/src/apps/review/src/pages/active-review-assignements/Reviews/ReviewsSidebar.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx @@ -1,16 +1,16 @@ import { FC, useCallback, useState } from 'react' -import { Link, useParams } from 'react-router-dom' +import { Link } from 'react-router-dom' import classNames from 'classnames' -import { AiScorecardContextModel } from '~/apps/review/src/lib/models' +import { ReviewsContextModel } from '~/apps/review/src/lib/models' import { AiWorkflowRunStatus } from '~/apps/review/src/lib/components/AiReviewsTable' import { IconAiReview } from '~/apps/review/src/lib/assets/icons' import { IconOutline, IconSolid } from '~/libs/ui' import StatusLabel from '~/apps/review/src/lib/components/AiReviewsTable/StatusLabel' -import { useAiScorecardContext } from '../../ai-scorecards/AiScorecardContext' import styles from './ReviewsSidebar.module.scss' +import { useReviewsContext } from '../../ReviewsContext' interface ReviewsSidebarProps { className?: string @@ -18,15 +18,14 @@ interface ReviewsSidebarProps { const ReviewsSidebar: FC = props => { const [isMobileOpen, setIsMobileOpen] = useState(false) - const { reviewId, submissionId } = useParams<{ reviewId: string, submissionId: string }>() const { workflow, workflowRun, workflowRuns, workflowId, - submissionId: contextSubmissionId, - }: AiScorecardContextModel = useAiScorecardContext() - + submissionId, + reviewId, + }: ReviewsContextModel = useReviewsContext() const isReviewActive = !workflowRun; const toggleOpen = useCallback(() => { @@ -37,8 +36,6 @@ const ReviewsSidebar: FC = props => { setIsMobileOpen(false) }, []) - const currentSubmissionId = submissionId || contextSubmissionId - return (
{workflow && workflowRun && ( @@ -73,7 +70,7 @@ const ReviewsSidebar: FC = props => { key={run.id} > @@ -83,7 +80,8 @@ const ReviewsSidebar: FC = props => { ))} - {currentSubmissionId && ( + + {submissionId && reviewId && (
  • = props => { )} > @@ -136,4 +134,3 @@ const ReviewsSidebar: FC = props => { } export default ReviewsSidebar - diff --git a/src/apps/review/src/pages/reviews/components/ReviewsSidebar/index.ts b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/index.ts new file mode 100644 index 000000000..b06bf5506 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/index.ts @@ -0,0 +1 @@ +export { default as ReviewsSidebar } from './ReviewsSidebar' diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss new file mode 100644 index 000000000..b84af1272 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss @@ -0,0 +1,133 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + width: 100%; + color: #0A0A0A; +} + +.headerWrap { + display: flex; + align-items: flex-start; + + @include ltemd { + flex-direction: column; + align-items: stretch; + gap: $sp-6; + } +} + +.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; + @include ltemd { + width: 56px; + height: 56px; + } +} + +.workflowName { + h3 { + font-family: "Figtree", sans-serif; + font-size: 26px; + font-weight: 700; + line-height: 30px; + color: #0A0A0A; + margin-bottom: $sp-2; + } + + span { + color: $link-blue-dark; + font-family: "Nunito Sans", sans-serif; + font-weight: bold; + font-size: 16px; + line-height: 22px; + } + + .modelName { + cursor: pointer; + } + + @include ltemd { + h3 { + font-size: 22px; + line-height: 26px; + } + } +} + +.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); + } + + @include ltemd { + margin-left: 0; + } +} + +.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/reviews/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx new file mode 100644 index 000000000..d8c8c1bb9 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx @@ -0,0 +1,87 @@ +import { FC, useCallback, useMemo, useState } from 'react' +import moment, { Duration } from 'moment' + +import { ReviewsContextModel } from '~/apps/review/src/lib/models' + +import { IconClock, IconPremium } from '../../../../lib/assets/icons' +import { AiModelModal } from '../AiModelModal' +import AiModelIcon from '../AiModelIcon' + +import styles from './ScorecardHeader.module.scss' +import { useReviewsContext } from '../../ReviewsContext' + +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, workflowRun }: ReviewsContextModel = useReviewsContext() + const runDuration = useMemo(() => ( + workflowRun && workflowRun.completedAt && workflowRun.startedAt && 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.llm.name} +
    +
    +
    + + + + Minimum passing score: + {' '} + {workflow.scorecard?.minimumPassingScore.toFixed(2)} + + + + + + Duration: + {' '} + {!!runDuration && formatDuration(runDuration)} + + +
    +
    +

    + {workflow.description} +

    + {/* */} + + {modelDetailsModalVisible && ( + + )} +
    + ) +} + +export default ScorecardHeader diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/index.ts b/src/apps/review/src/pages/reviews/components/ScorecardHeader/index.ts new file mode 100644 index 000000000..5cafe1a64 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/index.ts @@ -0,0 +1 @@ +export { default as ScorecardHeader } from './ScorecardHeader' diff --git a/src/apps/review/src/pages/reviews/components/index.ts b/src/apps/review/src/pages/reviews/components/index.ts new file mode 100644 index 000000000..d5fe282dd --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/index.ts @@ -0,0 +1 @@ +export * from './ReviewsSidebar' diff --git a/src/apps/review/src/pages/reviews/index.ts b/src/apps/review/src/pages/reviews/index.ts new file mode 100644 index 000000000..620a2d35c --- /dev/null +++ b/src/apps/review/src/pages/reviews/index.ts @@ -0,0 +1 @@ +export * from './reviews.routes' diff --git a/src/apps/review/src/pages/reviews/reviews.routes.tsx b/src/apps/review/src/pages/reviews/reviews.routes.tsx new file mode 100644 index 000000000..54d55b26f --- /dev/null +++ b/src/apps/review/src/pages/reviews/reviews.routes.tsx @@ -0,0 +1,31 @@ +import { getRoutesContainer, lazyLoad, LazyLoadedComponent, PlatformRoute } from '~/libs/core' + +import { reviewsRouteId } from '../../config/routes.config' + +const ReviewsViewer: LazyLoadedComponent = lazyLoad( + () => import('./ReviewsViewer'), + 'ReviewsViewer', +) + +const ReviewsContextProvider: LazyLoadedComponent = lazyLoad( + () => import('./ReviewsContext'), + 'ReviewsContextProvider', +) + +export const reviewsChildRoutes: ReadonlyArray = [ + { + authRequired: false, + element: , + id: 'view-reviews-page', + route: '', + }, +] + +export const reviewsRoutes: ReadonlyArray = [ + { + children: [...reviewsChildRoutes], + element: getRoutesContainer(reviewsChildRoutes, ReviewsContextProvider), + id: reviewsRouteId, + route: `${reviewsRouteId}/:submissionId`, + }, +] From e0a9addb570a8e66cabfb630e2663a4ee989b43f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 11 Nov 2025 18:00:54 +0200 Subject: [PATCH 044/125] PM-2178 - fix score calculations --- .../ScorecardGroup/ScorecardGroup.tsx | 11 +-- .../AiFeedback/AiFeedback.tsx | 6 +- .../ReviewAnswer/ReviewAnswer.tsx | 4 +- .../ScorecardQuestion/ScorecardQuestion.tsx | 1 + .../ScorecardQuestionEdit.tsx | 4 +- .../ScorecardScore/ScorecardScore.tsx | 33 +++----- .../ScorecardSection/ScorecardSection.tsx | 17 +--- .../ScorecardViewer.context.tsx | 8 +- .../ScorecardViewer/ScorecardViewer.tsx | 3 +- .../hooks/useProgressCalculation.ts | 39 ++-------- .../ScorecardViewer/hooks/useReviewForm.ts | 22 +++--- .../Scorecard/ScorecardViewer/utils.ts | 77 +++++-------------- 12 files changed, 71 insertions(+), 154 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx index d7353ac6a..0a64981e2 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx @@ -7,7 +7,7 @@ import { ScorecardGroup as ScorecardGroupModel } from '../../../../models' import { ScorecardSection } from '../ScorecardSection' import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' import { ScorecardScore } from '../ScorecardScore' -import { calcGroupScore, createReviewItemMapping } from '../utils' +import { createReviewItemMapping } from '../utils' import styles from './ScorecardGroup.module.scss' @@ -18,17 +18,13 @@ interface ScorecardGroupProps { } const ScorecardGroup: FC = props => { - const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() + const { aiFeedbackItems, scoreMap }: ScorecardViewerContextValue = useScorecardContext() const allFeedbackItems = aiFeedbackItems || [] const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardContext() const isVissible = !toggledItems[props.group.id] const toggle = useCallback(() => toggleItem(props.group.id), [props.group, toggleItem]) - const score = useMemo(() => ( - calcGroupScore(props.group, allFeedbackItems) - ), [props.group, allFeedbackItems]) - return (
    @@ -42,8 +38,7 @@ const ScorecardGroup: FC = props => { diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx index e02cfe4e0..5400829d4 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx @@ -7,6 +7,7 @@ import { ScorecardViewerContextValue, useScorecardContext } from '../../Scorecar import { ScorecardQuestionRow } from '../ScorecardQuestionRow' import { ScorecardScore } from '../../ScorecardScore' import { MarkdownReview } from '../../../../MarkdownReview' +import { calculateProgressAndScore } from '../../utils' import styles from './AiFeedback.module.scss' @@ -15,7 +16,7 @@ interface AiFeedbackProps { } const AiFeedback: FC = props => { - const { aiFeedbackItems }: ScorecardViewerContextValue = useScorecardContext() + const { aiFeedbackItems, scoreMap, }: ScorecardViewerContextValue = useScorecardContext() const feedback = useMemo(() => ( aiFeedbackItems?.find(r => r.scorecardQuestionId === props.question.id) ), [props.question.id, aiFeedbackItems]) @@ -33,8 +34,7 @@ const AiFeedback: FC = props => { className={styles.wrap} score={( )} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.tsx index e9b0c37dd..67618e441 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAnswer/ReviewAnswer.tsx @@ -19,6 +19,7 @@ const ReviewAnswer: FC = props => { isManagerEdit, isSavingManagerComment, // addManagerComment, + scoreMap, }: ScorecardViewerContextValue = useScorecardContext() const answer = useMemo(() => ( @@ -83,8 +84,7 @@ const ReviewAnswer: FC = props => { className={styles.wrap} score={( )} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx index 33f488e65..c1406086d 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx @@ -27,6 +27,7 @@ const ScorecardQuestion: FC = props => { toggleItem, toggledItems, mappingAppeals, + scoreMap, }: ScorecardViewerContextValue = useScorecardContext() const normalizedQuestionId = useMemo( diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx index 51c6babb6..8947ea265 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx @@ -59,6 +59,7 @@ export const ScorecardQuestionEdit: FC = props => { isTouched, setIsTouched, formTrigger, + scoreMap, }: ScorecardViewerContextValue = useScorecardContext() const isExpanded = toggledItems[props.question.id!] ?? false @@ -204,8 +205,7 @@ export const ScorecardQuestionEdit: FC = props => { index='Answer' score={( )} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx index 083063fb2..6cff5a4c8 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardScore/ScorecardScore.tsx @@ -4,31 +4,20 @@ import styles from './ScorecardScore.module.scss' interface ScorecardScoreProps { score: number - scaleMax: number + scaleMax?: number weight: number } -export const calcScore = (score: number, scaleMax: number, weight: number): number => ( - (score / (scaleMax || 1)) * weight +const ScorecardScore: FC = props => ( +
    + + {props.score.toFixed(2)} + + / + + {props.weight.toFixed(2)} + +
    ) -const ScorecardScore: FC = props => { - let score = calcScore(props.score, props.scaleMax, props.weight)?.toFixed(2) - if (props.score.toString() === 'NaN') { - score = '-'; - } - - return ( -
    - - {score} - - / - - {props.weight.toFixed(2)} - -
    - ) -} - export default ScorecardScore diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx index abbf19c70..6d6b76781 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardSection/ScorecardSection.tsx @@ -1,10 +1,10 @@ -import { FC, useMemo } from 'react' +import { FC } from 'react' import { ScorecardSection as ScorecardSectionModel } from '../../../../models' import { ScorecardQuestion } from '../ScorecardQuestion' import { ScorecardScore } from '../ScorecardScore' import { ScorecardViewerContextValue, useScorecardContext } from '../ScorecardViewer.context' -import { calcSectionScore, createReviewItemMapping } from '../utils' +import { createReviewItemMapping } from '../utils' import styles from './ScorecardSection.module.scss' @@ -15,15 +15,7 @@ interface ScorecardSectionProps { } const ScorecardSection: FC = props => { - const { aiFeedbackItems, reviewInfo, form }: ScorecardViewerContextValue = useScorecardContext() - const formValues = form?.getValues(); - const allFeedbackItems = useMemo(() => ( - formValues?.reviews || aiFeedbackItems || reviewInfo?.reviewItems || [] - ), [formValues, aiFeedbackItems, reviewInfo]) - - const score = useMemo(() => ( - calcSectionScore(props.section, allFeedbackItems) - ), [props.section, allFeedbackItems, formValues]) + const { scoreMap }: ScorecardViewerContextValue = useScorecardContext() return (
    @@ -38,8 +30,7 @@ const ScorecardSection: FC = props => { diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx index 3f8f3a8c1..e179c55f3 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx @@ -13,7 +13,7 @@ import { } from '../../../models' import { ReviewItemComment } from '../../../models/ReviewItemComment.model' -import { useProgressCalculation } from './hooks/useProgressCalculation' +import { useProgressCalculation, UseProgressCalculationValue } from './hooks/useProgressCalculation' import { useReviewForm } from './hooks/useReviewForm' import { useToggleItems } from './hooks/useToggleItems' @@ -106,15 +106,13 @@ export type ScorecardViewerContextValue = { ) => void // Form control related form?: UseFormReturn - reviewProgress: number - totalScore: number isTouched: { [key: string]: boolean } setIsTouched: React.Dispatch> recalculateReviewProgress: () => void touchedAllFields: () => void formErrors?: FieldErrors formTrigger?: UseFormTrigger -}; +} & UseProgressCalculationValue; const ScorecardViewerContext = createContext({} as ScorecardViewerContextValue) @@ -123,7 +121,7 @@ export const ScorecardViewerContextProvider: FC = p const reviewFormCtx = useReviewForm({ onFormChange: props.onFormChange, - reviewInfo: props.reviewInfo, + reviewItems: props.reviewInfo?.reviewItems ?? props.aiFeedbackItems ?? [], scorecardInfo: props.scorecard, }) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index edc8013c2..4e33c491d 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -109,7 +109,6 @@ const ScorecardViewerContent: FC = props => { : 'There were validation errors. Check above.' const onSubmit = useCallback(() => { - debugger if (props.saveReviewInfo) { props.saveReviewInfo( isDirty ? form?.getValues() : undefined, @@ -212,7 +211,7 @@ const ScorecardViewerContent: FC = props => { /> ))} - + {props.isEdit && (
    diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useProgressCalculation.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useProgressCalculation.ts index e0753e333..cd0581e09 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useProgressCalculation.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useProgressCalculation.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useMemo } from 'react' import { UseFormReturn, useWatch } from 'react-hook-form' import { FormReviews, Scorecard, ScorecardInfo } from '../../../../models' @@ -9,9 +9,7 @@ interface UseProgressCalculationProps { scorecard: Scorecard | ScorecardInfo } -export interface UseProgressCalculationValue { - totalScore: number - reviewProgress: number +export interface UseProgressCalculationValue extends ProgressAndScore { recalculateReviewProgress: () => void } @@ -19,41 +17,20 @@ export const useProgressCalculation = ({ form, scorecard, }: UseProgressCalculationProps): UseProgressCalculationValue => { - const [reviewProgress, setReviewProgress] = useState(0) - const [totalScore, setTotalScore] = useState(0) - // Watch form values to automatically recalculate progress const watchedReviews = useWatch({ control: form?.control, name: 'reviews', }) - // Recalculate progress when form values change - useEffect(() => { - if (!form || !scorecard) { - return - } - - const reviewFormDatas = watchedReviews ?? form.getValues().reviews - const { progress, score }: ProgressAndScore = calculateProgressAndScore(reviewFormDatas, scorecard) - setReviewProgress(progress) - setTotalScore(score) - }, [watchedReviews, form, scorecard]) - const recalculateReviewProgress = useCallback(() => { - if (!form || !scorecard) { - return - } - - const reviewFormDatas = form.getValues().reviews - const { progress, score }: ProgressAndScore = calculateProgressAndScore(reviewFormDatas, scorecard) - setReviewProgress(progress) - setTotalScore(score) + const reviewFormDatas = form?.getValues()?.reviews ?? [] + return calculateProgressAndScore(reviewFormDatas, scorecard) }, [form, scorecard]) - return { + return useMemo(() => ({ + ...recalculateReviewProgress(), recalculateReviewProgress, - reviewProgress, - totalScore, - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }), [watchedReviews, recalculateReviewProgress]) } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts index 3c49bafcd..0449d38d5 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/hooks/useReviewForm.ts @@ -4,11 +4,11 @@ import { forEach } from 'lodash' import { yupResolver } from '@hookform/resolvers/yup' -import { FormReviews, ReviewInfo, Scorecard, ScorecardInfo } from '../../../../models' +import { AiFeedbackItem, FormReviews, ReviewInfo, Scorecard, ScorecardInfo } from '../../../../models' import { formReviewsSchema } from '../../../../utils' interface UseReviewFormProps { - reviewInfo?: ReviewInfo + reviewItems?: ReviewInfo['reviewItems'] | AiFeedbackItem[] scorecardInfo?: Scorecard | ScorecardInfo onFormChange?: (isDirty: boolean) => void } @@ -21,7 +21,7 @@ export interface UseReviewForm { } export const useReviewForm = ({ - reviewInfo, + reviewItems, onFormChange, }: UseReviewFormProps): UseReviewForm => { const [isTouched, setIsTouched] = useState<{ [key: string]: boolean }>({}) @@ -41,28 +41,32 @@ export const useReviewForm = ({ }, [isDirty, onFormChange]) useEffect(() => { - if (reviewInfo) { + if (reviewItems?.length) { const newFormData = { - reviews: reviewInfo.reviewItems.map( + reviews: reviewItems.map( (reviewItem, reviewItemIndex) => ({ - comments: reviewItem.reviewItemComments.map( + comments: 'reviewItemComments' in reviewItem ? reviewItem.reviewItemComments?.map( (commentItem, commentIndex) => ({ content: commentItem.content ?? '', id: commentItem.id, index: commentIndex, type: commentItem.type ?? '', }), - ), + ) : [], id: reviewItem.id, index: reviewItemIndex, - initialAnswer: reviewItem.finalAnswer || reviewItem.initialAnswer, + initialAnswer: ( + ('finalAnswer' in reviewItem && reviewItem.finalAnswer) + || ('initialAnswer' in reviewItem && reviewItem.initialAnswer) + || ('questionScore' in reviewItem && reviewItem.questionScore) + ) as string, scorecardQuestionId: reviewItem.scorecardQuestionId, }), ), } reset(newFormData) } - }, [reviewInfo, reset]) + }, [reviewItems, reset]) const touchedAllFields = useCallback(() => { const formData = getValues() diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts index a5aeff93d..f647a4010 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/utils.ts @@ -1,54 +1,12 @@ -import { filter, map, reduce } from 'lodash' +import { filter, reduce } from 'lodash' import { - AiFeedbackItem, - FormReviews, ReviewItemInfo, Scorecard, - ScorecardGroup, ScorecardInfo, - ScorecardSection, } from '../../../models' import { roundWith2DecimalPlaces } from '../../../utils' -export const calcSectionScore = ( - section: ScorecardSection, - feedbackItems: ( - Pick[] | - Pick[] - ), -): number => { - const feedbackItemsMap = Object.fromEntries(feedbackItems.map(r => [r.scorecardQuestionId, r])) - - return section.questions.reduce((sum, question) => { - const item = feedbackItemsMap[question.id as string] - let score = 0 - - if (item && ('initialAnswer' in item || 'finalAnswer' in item)) { - score += Number(item.finalAnswer || item.initialAnswer) || 0 - } - - if (item && 'questionScore' in item) { - score += Number(item.questionScore) || 0 - } - - return sum + ( - score / (question.scaleMax || 1) - ) * (question.weight / 100) - }, 0) -} - -export const calcGroupScore = ( - group: ScorecardGroup, - feedbackItems: Pick[], -): number => ( - group.sections.reduce((sum, section) => ( - sum + ( - calcSectionScore(section, feedbackItems) - ) * (section.weight / 100) - ), 0) -) - /** * Normalize scorecard question ID for consistent comparison */ @@ -99,23 +57,26 @@ export const createReviewItemMapping = ( } export interface ProgressAndScore { - progress: number; - score: number; + reviewProgress: number; + totalScore: number; + scoreMap: Map } /** * Calculate progress and score from review form data */ export const calculateProgressAndScore = ( - reviewFormDatas: FormReviews['reviews'], + reviewFormDatas: {scorecardQuestionId: string; initialAnswer: string;}[], scorecard: Scorecard | ScorecardInfo, ): ProgressAndScore => { + const scoreMap = new Map() + if (!scorecard || reviewFormDatas.length === 0) { - return { progress: 0, score: 0 } + return { reviewProgress: 0, scoreMap, totalScore: 0 } } const mappingResult: { - [scorecardQuestionId: string]: string + [scorecardQuestionId: string]: string | number } = {} const newReviewProgress = Math.round( @@ -152,38 +113,40 @@ export const calculateProgressAndScore = ( if ( question.type === 'YES_NO' - && initialAnswer === 'Yes' + && (initialAnswer === 'Yes' || initialAnswer === 1) ) { questionPoint = 100 } else if ( question.type === 'SCALE' && !!initialAnswer ) { - const totalPoint = question.scaleMax - question.scaleMin - const initialAnswerNumber = parseInt(initialAnswer, 10) - question.scaleMin + const totalPoint = question.scaleMax + const initialAnswerNumber = parseInt(initialAnswer as string, 10) questionPoint = totalPoint > 0 ? (initialAnswerNumber * 100) / totalPoint : 0 } - return ( - questionResult - + (questionPoint * question.weight) / 100 - ) + const score = (questionPoint * question.weight) / 100 + scoreMap.set(question.id as string, score) + return questionResult + score }, 0, ) * section.weight) / 100 + scoreMap.set(section.id as string, sectionPoint) return sectionResult + sectionPoint }, 0, ) * group.weight) / 100 + scoreMap.set(group.id as string, groupPoint) return groupResult + groupPoint }, 0, ) return { - progress: newReviewProgress, - score: roundWith2DecimalPlaces(groupsScore), + reviewProgress: newReviewProgress, + scoreMap, + totalScore: roundWith2DecimalPlaces(groupsScore), } } From f835cd038c0b22fce263990941f10f1d3adbd201 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 12 Nov 2025 09:12:50 +0200 Subject: [PATCH 045/125] remove ai-scorcards pages --- .../AiScorecardContextProvider.tsx | 80 ------- .../ai-scorecards/AiScorecardContext/index.ts | 1 - .../AiScorecardViewer.module.scss | 37 ---- .../AiScorecardViewer/AiScorecardViewer.tsx | 77 ------- .../ai-scorecards/AiScorecardViewer/index.ts | 1 - .../ai-scorecards/ai-scorecard.routes.tsx | 34 --- .../ai-scorecards/components/AiModelIcon.tsx | 27 --- .../AiModelModal/AiModelModal.module.scss | 66 ------ .../components/AiModelModal/AiModelModal.tsx | 44 ---- .../components/AiModelModal/index.ts | 1 - .../AiWorkflowsSidebar.module.scss | 205 ------------------ .../AiWorkflowsSidebar/AiWorkflowsSidebar.tsx | 117 ---------- .../components/AiWorkflowsSidebar/index.ts | 1 - .../ScorecardHeader.module.scss | 133 ------------ .../ScorecardHeader/ScorecardHeader.tsx | 87 -------- .../components/ScorecardHeader/index.ts | 1 - .../pages/ai-scorecards/components/index.ts | 1 - .../review/src/pages/ai-scorecards/index.ts | 1 - 18 files changed, 914 deletions(-) delete mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx delete mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardContext/index.ts delete mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss delete mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx delete mode 100644 src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/index.ts delete mode 100644 src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/index.ts delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/index.ts delete mode 100644 src/apps/review/src/pages/ai-scorecards/components/index.ts delete mode 100644 src/apps/review/src/pages/ai-scorecards/index.ts diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx b/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx deleted file mode 100644 index b1d01519a..000000000 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/AiScorecardContextProvider.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/** - * Context provider for challenge detail page - */ -import { Context, createContext, FC, PropsWithChildren, useContext, useMemo, useState } from 'react' -import { useParams, useSearchParams } 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 { submissionId: submissionIdParam = '' }: { - submissionId?: string, - } = useParams<{ - submissionId: string, - }>() - const [params] = useSearchParams() - const workflowId = params.get('workflowId') ?? '' - const reviewId = params.get('reviewId') ?? '' - - const [submissionId, setSubmissionId] = useState(submissionIdParam); - - 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 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, - workflowRun, - workflowRuns, - setSubmissionId, - }), - [ - challengeDetailsCtx, - isLoadingCtxData, - scorecard, - submissionId, - workflow, - workflowId, - workflowRun, - workflowRuns, - ], - ) - - return ( - - {props.children} - - ) -} - -export const useAiScorecardContext = (): AiScorecardContextModel => 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 deleted file mode 100644 index 03eae189c..000000000 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardContext/index.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 1b3b36e66..000000000 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.module.scss +++ /dev/null @@ -1,37 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.container { - display: flex; - flex-direction: column; -} - -.pageContentWrap { - display: flex; - flex-direction: row; - gap: $sp-10; - - > .sidebar { - flex-grow: 0; - flex-shrink: 0; - flex-basis: 256px; - } - - @include ltelg { - flex-direction: column; - gap: $sp-6; - - > .sidebar { - flex-basis: auto; - width: 100%; - } - } -} - -.contentWrap { - width: 100%; -} - -.tabs { - justify-content: center; - margin: $sp-6 0; -} diff --git a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx b/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx deleted file mode 100644 index 7b27c1fef..000000000 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/AiScorecardViewer.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { FC, useEffect, useMemo, useState } from 'react' - -import { NotificationContextType, useNotification } from '~/libs/shared' - -import { ScorecardHeader } from '../components/ScorecardHeader' -import { IconAiReview } from '../../../lib/assets/icons' -import { PageWrapper, Tabs } from '../../../lib' -import { useAiScorecardContext } from '../AiScorecardContext' -import { AiScorecardContextModel, SelectOption } from '../../../lib/models' -import { AiWorkflowsSidebar } from '../components/AiWorkflowsSidebar' -import { ScorecardViewer } from '../../../lib/components/Scorecard' -import { AiWorkflowRunItemsResponse, useFetchAiWorkflowsRunItems } from '../../../lib/hooks' -import { ScorecardAttachments } from '../../../lib/components/Scorecard/ScorecardAttachments' - -import styles from './AiScorecardViewer.module.scss' - -const tabItems: SelectOption[] = [ - { label: 'Scorecard', value: 'scorecard' }, - { label: 'Attachments', value: 'attachments' }, -] - -const AiScorecardViewer: FC = () => { - const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() - const { challengeInfo, scorecard, workflowId, workflowRun }: AiScorecardContextModel = useAiScorecardContext() - const { runItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(workflowId, workflowRun?.id) - - const breadCrumb = useMemo( - () => [{ index: 1, label: 'My Active Challenges' }], - [], - ) - - const [selectedTab, setSelectedTab] = useState('scorecard') - - 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 ( - -
    - -
    - - - {!!scorecard && selectedTab === 'scorecard' && ( - - )} - - {selectedTab === 'attachments' && ( - - )} -
    -
    -
    - ) -} - -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 deleted file mode 100644 index bc2998589..000000000 --- a/src/apps/review/src/pages/ai-scorecards/AiScorecardViewer/index.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index fe872896b..000000000 --- a/src/apps/review/src/pages/ai-scorecards/ai-scorecard.routes.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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', -) - -export const aiScorecardChildRoutes: ReadonlyArray = [ - { - authRequired: false, - element: , - id: 'view-ai-scorecard-page', - route: '', - }, -] - -export const aiScorecardRoutes: ReadonlyArray = [ - { - children: [...aiScorecardChildRoutes], - element: getRoutesContainer(aiScorecardChildRoutes, AiScorecardContextProvider), - id: aiScorecardRouteId, - rolesRequired: [ - // UserRole.administrator, - ], - route: `${aiScorecardRouteId}/:submissionId`, - }, -] diff --git a/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx b/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx deleted file mode 100644 index 9d296780d..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/AiModelIcon.tsx +++ /dev/null @@ -1,27 +0,0 @@ -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.module.scss b/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss deleted file mode 100644 index 3f0514beb..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.module.scss +++ /dev/null @@ -1,66 +0,0 @@ -@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 { - 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; - } - - @include ltemd { - h3 { - font-size: 22px; - line-height: 26px; - } - } -} - -.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 deleted file mode 100644 index 8aa2e02ae..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/AiModelModal.tsx +++ /dev/null @@ -1,44 +0,0 @@ -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 AiModelIcon from '../AiModelIcon' - -import styles from './AiModelModal.module.scss' - -interface AiModelModalProps { - model: AiWorkflow['llm'] - onClose: () => void -} - -const AiModelModal: FC = props => ( - -
    -
    -
    - -
    -
    -

    {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 deleted file mode 100644 index 948754b83..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/AiModelModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as AiModelModal } from './AiModelModal' 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 deleted file mode 100644 index 54197a0e6..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.module.scss +++ /dev/null @@ -1,205 +0,0 @@ -@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; - gap: $sp-4; - padding: $sp-4; - background-color: #f9fafa; - cursor: pointer; - position: relative; - - 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; - - &Wrap { - flex: 1 1 auto; - } - @include ltelg { - max-width: calc(90vw - 140px); - } -} - -.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; - } - } - } -} - -.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; - } - - @include gtexl { - display: none; - } -} 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 deleted file mode 100644 index 724c6e973..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/AiWorkflowsSidebar.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { FC, useCallback, useState } from 'react' -import { Link } from 'react-router-dom' -import classNames from 'classnames' - -import { AiScorecardContextModel } from '~/apps/review/src/lib/models' -import { AiWorkflowRunStatus } from '~/apps/review/src/lib/components/AiReviewsTable' -import { IconAiReview } from '~/apps/review/src/lib/assets/icons' -import { IconOutline, IconSolid } from '~/libs/ui' -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 toggleOpen = useCallback(() => { - setIsMobileOpen(wasOpen => !wasOpen) - }, []) - - const close = useCallback(() => { - setIsMobileOpen(false) - }, []) - - return ( -
    - {workflow && workflowRun && ( -
    -
    - - - {workflow.name} - - -
    - -
    -
    -
    - )} - -
    -
    - -
    -
    -
      - {workflowRuns.map(run => ( -
    • - - - - {run.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 deleted file mode 100644 index 1c0991661..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/AiWorkflowsSidebar/index.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index b84af1272..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.module.scss +++ /dev/null @@ -1,133 +0,0 @@ -@import '@libs/ui/styles/includes'; - -.wrap { - width: 100%; - color: #0A0A0A; -} - -.headerWrap { - display: flex; - align-items: flex-start; - - @include ltemd { - flex-direction: column; - align-items: stretch; - gap: $sp-6; - } -} - -.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; - @include ltemd { - width: 56px; - height: 56px; - } -} - -.workflowName { - h3 { - font-family: "Figtree", sans-serif; - font-size: 26px; - font-weight: 700; - line-height: 30px; - color: #0A0A0A; - margin-bottom: $sp-2; - } - - span { - color: $link-blue-dark; - font-family: "Nunito Sans", sans-serif; - font-weight: bold; - font-size: 16px; - line-height: 22px; - } - - .modelName { - cursor: pointer; - } - - @include ltemd { - h3 { - font-size: 22px; - line-height: 26px; - } - } -} - -.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); - } - - @include ltemd { - margin-left: 0; - } -} - -.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 deleted file mode 100644 index 442ee1232..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/ScorecardHeader.tsx +++ /dev/null @@ -1,87 +0,0 @@ -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, IconPremium } from '../../../../lib/assets/icons' -import { AiModelModal } from '../AiModelModal' -import AiModelIcon from '../AiModelIcon' - -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, workflowRun }: AiScorecardContextModel = useAiScorecardContext() - const runDuration = useMemo(() => ( - workflowRun && workflowRun.completedAt && workflowRun.startedAt && 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.llm.name} -
    -
    -
    - - - - Minimum passing score: - {' '} - {workflow.scorecard?.minimumPassingScore.toFixed(2)} - - - - - - Duration: - {' '} - {!!runDuration && formatDuration(runDuration)} - - -
    -
    -

    - {workflow.description} -

    - {/* */} - - {modelDetailsModalVisible && ( - - )} -
    - ) -} - -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 deleted file mode 100644 index 5cafe1a64..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/ScorecardHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index 3bbdff305..000000000 --- a/src/apps/review/src/pages/ai-scorecards/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AiWorkflowsSidebar' diff --git a/src/apps/review/src/pages/ai-scorecards/index.ts b/src/apps/review/src/pages/ai-scorecards/index.ts deleted file mode 100644 index aec1c4968..000000000 --- a/src/apps/review/src/pages/ai-scorecards/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './ai-scorecard.routes' From 57fcb0d5c0de17f51fe1926a210a48c7f5d9c58b Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 12 Nov 2025 10:54:07 +0200 Subject: [PATCH 046/125] PM-2178 - Update routing & fix validation --- .../AiReviewsTable/AiReviewsTable.tsx | 2 +- .../ScorecardQuestion/ScorecardQuestion.tsx | 1 - .../ScorecardQuestionEdit.module.scss | 34 ++++++++---- .../ScorecardQuestionEdit.tsx | 54 +++++++------------ .../ScorecardViewer.module.scss | 6 +-- .../ScorecardViewer/ScorecardViewer.tsx | 16 +++--- .../ScorecardViewer/hooks/useReviewForm.ts | 1 + .../Scorecard/ScorecardViewer/utils.ts | 4 +- .../TableAppealsResponse.tsx | 2 +- .../TableCheckpointSubmissions.tsx | 2 +- .../TableIterativeReview.tsx | 2 +- .../components/TableReview/TableReview.tsx | 2 +- .../common/TableColumnRenderers.tsx | 4 +- src/apps/review/src/lib/utils/routes.ts | 5 +- .../ScorecardDetailsPage.tsx | 2 +- 15 files changed, 66 insertions(+), 71 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index e3fd0b774..88be43a17 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -145,7 +145,7 @@ const AiReviewsTable: FC = props => { {run.status === 'SUCCESS' ? ( run.workflow.scorecard ? ( {run.score} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx index c1406086d..33f488e65 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestion.tsx @@ -27,7 +27,6 @@ const ScorecardQuestion: FC = props => { toggleItem, toggledItems, mappingAppeals, - scoreMap, }: ScorecardViewerContextValue = useScorecardContext() const normalizedQuestionId = useMemo( diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.module.scss index d17c28677..995aeed6d 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.module.scss @@ -5,10 +5,6 @@ border: 1px solid #e0e0e0; border-radius: 4px; padding: 16px; - - &.hasError { - border-color: #d32f2f; - } } .header { @@ -38,10 +34,7 @@ .errorMessage { color: #d32f2f; font-size: 14px; - margin: 8px 0; - padding: 8px; - background-color: #ffebee; - border-radius: 4px; + font-weight: bold; } .answerSection { @@ -50,6 +43,14 @@ padding: 0 $sp-4 $sp-4; } +.hasError { + border: 1px solid #d32f2f; + margin-left: -1*$sp-4; + margin-right: -1*$sp-4; + padding-left: $sp-8; + padding-right: $sp-8; +} + .commentForm { margin-bottom: 24px; } @@ -66,10 +67,25 @@ border-top: 0 none; } } + + &.editorError { + :global(.EasyMDEContainer) { + :global(.editor-toolbar) { + border-top-color: #d34e3b; + border-left-color: #d34e3b; + border-right-color: #d34e3b; + } + } + } } .answerWrap { - width: 120px; + display: flex; + align-items: center; + gap: $sp-4; + .answerInput { + width: 120px; + } } .responseTypeWrap { diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx index 8947ea265..3c8f79bd1 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ScorecardQuestionEdit/ScorecardQuestionEdit.tsx @@ -30,18 +30,6 @@ import { ScorecardScore } from '../../ScorecardScore' import styles from './ScorecardQuestionEdit.module.scss' -const normalizeAnswerValue = (questionType: ScorecardQuestion['type'], value: string | number): number => { - if (['undefined', 'null'].includes(typeof value)) { - return Number(value); - } - - if (questionType === 'YES_NO') { - return Number(`${value}`.toLowerCase() === 'yes') - } - - return Number(`${value}`) -} - interface ScorecardQuestionEditProps { question: ScorecardQuestion reviewItem: ReviewItemInfo @@ -101,21 +89,12 @@ export const ScorecardQuestionEdit: FC = props => { name: `reviews.${props.fieldIndex}.comments` as 'reviews.0.comments', }) - const errorMessage = useMemo( - () => { - if (touched[ - `reviews.${props.fieldIndex}.initialAnswer.message` - ]) { - return _.get( - errors, - `reviews.${props.fieldIndex}.initialAnswer.message`, - ) - } - - return '' - }, - [touched, errors, props.fieldIndex], - ) + const errorMessage = touched[ + `reviews.${props.fieldIndex}.initialAnswer.message` + ] ? _.get( + errors, + `reviews.${props.fieldIndex}.initialAnswer.message`, + ) : '' const initCommentContents = useMemo<{ [key: string]: string }>(() => { const results: { [key: string]: string } = {} @@ -141,7 +120,8 @@ export const ScorecardQuestionEdit: FC = props => { : '' }) return result - }, [touched, errors, props.fieldIndex, fields]) + }, [touched, errors, errors?.reviews, props.fieldIndex, fields]) +console.log('here', initCommentContents, errorCommentsMessage); const hasErrors = !!errorMessage || !isEmpty(compact(Object.values(errorCommentsMessage))) @@ -159,7 +139,7 @@ export const ScorecardQuestionEdit: FC = props => { } return ( -
    +
    = props => { {props.question.description} - {errorMessage && ( -
    - {errorMessage} -
    - )} - {isExpanded && (
    = props => { weight={props.question.weight} /> )} + className={classNames(hasErrors && styles.hasError)} >
    - ) : ( -

    - {answer} -

    - )} +

    + {answer} +

    ) } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.module.scss index 3b756edc2..2ef28a1bb 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.module.scss @@ -1,112 +1,98 @@ -.wrap { +@import '@libs/ui/styles/includes'; + +.container { display: flex; flex-direction: column; - gap: 4px; - margin-left: 16px; -} - -.appealRow { + gap: $sp-4; + align-items: flex-start; + :global(.borderButton), + :global(.filledButton) { + font-size: 14px; + } + :global(.filledButton) { + font-size: 16px; + } } -.responseRow { +.blockAppealComment { + background-color: var(--Appeal); } -.content { +.blockAppealResponse { + background-color: var(--TableColumn); } -.status { - font-weight: bold; - margin-top: 8px; +.blockAppealResponse, +.blockAppealComment { + padding: $sp-4; + display: flex; + flex-direction: column; + gap: $sp-2; + width: 100%; } -.responseActions { - display: flex; - gap: 8px; - margin-top: 8px; +.markdownEditor { + width: 100%; } -.respondButton, -.editResponseButton { - padding: 6px 12px; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - cursor: pointer; +.textTitle { + color: var(--FontColor); + font-family: "Nunito Sans", sans-serif; font-size: 14px; - - &:hover:not(:disabled) { - background-color: #0056b3; - } - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } + font-weight: 700; + line-height: 20px; } -.responseForm { +.blockBtns { display: flex; - flex-direction: column; - gap: 8px; - margin-top: 8px; - padding: 12px; - border: 1px solid #e0e0e0; - border-radius: 4px; - background-color: #f8f9fa; + gap: 10px; + padding-top: 8px; + padding-bottom: 16px; + flex-wrap: wrap; } -.responseFormHeader { +.blockForm { + background-color: var(--TableColumn); display: flex; flex-direction: column; - gap: 8px; + padding: $sp-6; + gap: $sp-4; + width: 100%; + @include ltemd { + padding: $sp-4; + } + label { + color: var(--FontColor); + font-family: "Nunito Sans", sans-serif; + font-size: 16px; + font-weight: 700; + line-height: 20px; + } } -.responseFormLabel { +.status { font-weight: bold; - font-size: 14px; + margin-top: 8px; } .responseSelect { min-width: 200px; } -.markdownEditor { -} - -.responseFormActions { +.linkStyleBtn { display: flex; - gap: 8px; -} - -.submitButton, -.cancelButton { - padding: 6px 12px; - border: none; - border-radius: 4px; - cursor: pointer; + align-items: center; + gap: $sp-2; + color: #0D61BF; font-size: 14px; + font-family: "Nunito Sans", sans-serif; + font-weight: 700; - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } -} - -.submitButton { - background-color: #28a745; - color: white; - - &:hover:not(:disabled) { - background-color: #218838; + > svg path { + fill: currentColor; } -} - -.cancelButton { - background-color: #6c757d; - color: white; - &:hover:not(:disabled) { - background-color: #5a6268; + &:hover { + color: darken(#0D61BF, 10%); } } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx index 4e9da7075..7dbe97b8a 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { FC, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react' import { Controller, ControllerRenderProps, @@ -21,46 +21,34 @@ import { } from '../../../../../../models' import { formAppealResponseSchema, isAppealsResponsePhase } from '../../../../../../utils' import { QUESTION_YES_NO_OPTIONS } from '../../../../../../../config/index.config' -import { ChallengeDetailContext } from '../../../../../../contexts' +import { ChallengeDetailContext, useChallengeDetailsContext } from '../../../../../../contexts' import { FieldMarkdownEditor } from '../../../../../FieldMarkdownEditor' +import { MarkdownReview } from '../../../../../MarkdownReview' import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../../ScorecardViewer.context' -import { ScorecardQuestionRow } from '../../ScorecardQuestionRow' import styles from './ReviewAppeal.module.scss' +import { IconAppealResponse, IconEdit } from '~/apps/review/src/lib/assets/icons' -interface ReviewAppealProps { +interface ReviewAppealProps extends PropsWithChildren { appeal: AppealInfo reviewItem?: ReviewItemInfo scorecardQuestion?: ScorecardQuestion + canRespondToAppeal: boolean } const ReviewAppeal: FC = props => { const { - actionChallengeRole, addAppealResponse, isSavingAppealResponse, }: ScorecardViewerContextValue = useScorecardViewerContext() - const { challengeInfo }: ChallengeDetailContextModel = useContext( - ChallengeDetailContext, - ) - - // Determine if user can respond to appeal (reviewer/copilot/admin roles) - const canRespondToAppeal = useMemo(() => { - const role = actionChallengeRole?.toLowerCase() || '' - return role.includes('reviewer') || role.includes('admin') || role.includes('copilot') - }, [actionChallengeRole]) + const { challengeInfo }: ChallengeDetailContextModel = useChallengeDetailsContext() const canAddAppealResponse = useMemo( - () => canRespondToAppeal && isAppealsResponsePhase(challengeInfo), - [challengeInfo, canRespondToAppeal], + () => props.canRespondToAppeal && isAppealsResponsePhase(challengeInfo), + [challengeInfo, props.canRespondToAppeal], ) - // const isReviewerRole = useMemo(() => { - // const role = actionChallengeRole?.toLowerCase() || '' - // return role.includes('reviewer') || role.includes('admin') || role.includes('copilot') - // }, [actionChallengeRole]) - const [showResponseForm, setShowResponseForm] = useState(false) const [appealResponse, setAppealResponse] = useState(props.appeal.appealResponse?.content || '') const [updatedResponse, setUpdatedResponse] = useState>() @@ -134,77 +122,70 @@ const ReviewAppeal: FC = props => { }, []) return ( -
    - -
    - {props.appeal.content} -
    -
    +
    +
    + + Appeal + + + {props.children} +
    {props.appeal.appealResponse && !showResponseForm && ( - -
    - {props.appeal.appealResponse.content} -
    +
    + Appeal Response + {props.appeal.appealResponse.success !== undefined && (
    {props.appeal.appealResponse.success ? 'Accepted' : 'Rejected'}
    )} {canAddAppealResponse && ( -
    +
    )} - +
    )} {!props.appeal.appealResponse && !showResponseForm && canAddAppealResponse && ( -
    - -
    + )} {showResponseForm && canAddAppealResponse && ( -
    - - {props.scorecardQuestion && responseOptions.length > 0 && ( - + )} = props => { ) }} /> -
    +
    - {isSubmitter && canAddAppeal && ( - <> - {!props.appeal && !showAppealForm && ( -
    - -
    - )} - - {props.appeal && ( -
    + + {isSubmitter && canAddAppeal && (!props.appeal && !showAppealForm && ( + + ))} + + {showAppealForm && ( + + + + }) { + return ( + + ) + }} + /> +
    - )} - - {showAppealForm && ( - - - - }) { - return ( - - ) - }} - /> -
    - + + )} + + + {props.appeal && !showAppealForm && (isSubmitter || isReviewerRole || isManagerEdit) && ( + + {isSubmitter && canAddAppeal && ( +
    - - )} - - )} + )} +
    + )} + +
    ) } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx index d510b3d8c..ac97b31e1 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx @@ -18,10 +18,6 @@ const ReviewComments: FC = props => { (props.reviewItem.reviewItemComments || []).filter(c => c.content || c.appeal || props.mappingAppeals?.[c.id]) ).sort((a, b) => a.sortOrder - b.sortOrder), [props.reviewItem.reviewItemComments, props.mappingAppeals]) - if (!comments.length && !props.reviewItem.managerComment) { - return <> - } - return (
    {comments.map((comment, index) => { @@ -31,15 +27,10 @@ const ReviewComments: FC = props => { - {appeal && ( - - )}
    ) })} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.module.scss index 689274a54..827e57d38 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.module.scss @@ -1,13 +1,29 @@ -.wrap { -} +@import '@libs/ui/styles/includes'; + +.linkStyleBtn { + display: flex; + align-items: center; + gap: $sp-2; + color: #0D61BF; + font-size: 14px; + font-family: "Nunito Sans", sans-serif; + font-weight: 700; + + > svg path { + fill: currentColor; + } -.content { + &:hover { + color: darken(#0D61BF, 10%); + } } .displayContainer { display: flex; flex-direction: column; gap: 8px; + background-color: #ECF4FA; + padding: $sp-4; } .editButton, @@ -32,10 +48,30 @@ } } -.commentForm { +.blockForm { + background-color: var(--TableColumn); display: flex; flex-direction: column; - gap: 8px; + padding: $sp-6; + gap: $sp-4; + width: 100%; + @include ltemd { + padding: $sp-4; + } + label { + color: var(--FontColor); + font-family: "Nunito Sans", sans-serif; + font-size: 16px; + font-weight: 700; + line-height: 20px; + } + :global(.borderButton), + :global(.filledButton) { + font-size: 14px; + } + :global(.filledButton) { + font-size: 16px; + } } .commentFormHeader { @@ -44,54 +80,15 @@ gap: 8px; } -.commentFormLabel { - font-weight: bold; - font-size: 14px; -} - .scoreSelect { min-width: 200px; } -.select { -} - -.markdownEditor { -} - -.commentFormActions { +.blockBtns { display: flex; - gap: 8px; + gap: 10px; + padding-top: 8px; + padding-bottom: 16px; + flex-wrap: wrap; } -.submitButton, -.cancelButton { - padding: 6px 12px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; - - &:disabled { - opacity: 0.5; - cursor: not-allowed; - } -} - -.submitButton { - background-color: #28a745; - color: white; - - &:hover:not(:disabled) { - background-color: #218838; - } -} - -.cancelButton { - background-color: #6c757d; - color: white; - - &:hover:not(:disabled) { - background-color: #5a6268; - } -} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.tsx index a81da7842..57cbf1f37 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewManagerComment/ReviewManagerComment.tsx @@ -19,6 +19,7 @@ import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../.. import { ScorecardQuestionRow } from '../../ScorecardQuestionRow' import styles from './ReviewManagerComment.module.scss' +import { IconPhaseReview } from '~/apps/review/src/lib/assets/icons' interface ReviewManagerCommentProps { managerComment?: string @@ -66,8 +67,8 @@ const ReviewManagerComment: FC = props => { formState: { errors }, }: UseFormReturn = useForm({ defaultValues: { - finalScore: '', - response: '', + finalScore: props.reviewItem?.finalAnswer ?? props.reviewItem?.initialAnswer ?? '', + response: props.managerComment ?? '', }, mode: 'all', resolver: yupResolver(formManagerCommentSchema), @@ -134,19 +135,20 @@ const ReviewManagerComment: FC = props => { type='button' onClick={handleShowCommentForm} disabled={isSavingManagerComment} - className={styles.addButton} + className={styles.linkStyleBtn} > + Add a Manager Comment )} {showCommentForm && isManagerEdit && (
    - + {props.scorecardQuestion && responseOptions.length > 0 && (
    = props => { ) }} /> -
    +
    - {isMemberReview && ( + {!isMemberReview && ( = (props: Props) => { classNameWrapper={styles.inputField} /> )} - {isMemberReview && ( + {!isMemberReview && ( <> = (props: Props) => { name='opportunityType' control={control} render={function render(controlProps: { - field: ControllerRenderProps - }) { + field: ControllerRenderProps + }) { return ( = (props: Props) => { )} { - isMemberReview && ( + !isMemberReview && (
    = (props: Props) => {
    ) } - {!isMemberReview && ( + {isMemberReview && ( = (props: Props) => { field: ControllerRenderProps }) { return ( - + ) }} /> From d8cbcbbaffcea919ed5867b2d6a5454b3984f956 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Wed, 12 Nov 2025 18:05:29 +0100 Subject: [PATCH 051/125] fix: default challenge reviewer form --- .../DefaultReviewersAddForm/DefaultReviewersAddForm.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx b/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx index bbbca2984..ec7504c9b 100644 --- a/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx +++ b/src/apps/admin/src/lib/components/DefaultReviewersAddForm/DefaultReviewersAddForm.tsx @@ -153,6 +153,11 @@ export const DefaultReviewersAddForm: FC = (props: Props) => { requestBody.baseCoefficient = undefined requestBody.incrementalCoefficient = undefined requestBody.opportunityType = undefined + // The reason for flipping the value is that + // in UI the checkbox is shown as "is AI review" + // but in the database its denoted as member review + // so we are just flipping the boolean value + requestBody.isMemberReview = false } else { // eslint-disable-next-line unicorn/no-null requestBody.aiWorkflowId = null From 1359f92f52415c8fa13a415ae62f547a38a21863 Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 13 Nov 2025 15:47:33 +1100 Subject: [PATCH 052/125] Much better marathon match submission management --- src/apps/admin/src/config/busEvent.config.ts | 6 ++- .../SubmissionTable.module.scss | 22 ++++++++ .../SubmissionTable/SubmissionTable.tsx | 50 ++++++++++++++++++- .../SubmissionTableActions.tsx | 26 ++++++++++ .../common/Layout/Layout.module.scss | 4 ++ .../lib/components/common/Layout/Layout.tsx | 1 + .../PageContent/PageContent.module.scss | 4 +- .../common/PageHeader/PageHeader.module.scss | 4 +- .../hooks/useManageChallengeSubmissions.ts | 7 ++- .../admin/src/lib/models/Submission.model.ts | 9 ++-- .../src/lib/services/submissions.service.ts | 18 ++++++- src/apps/admin/src/lib/utils/others.ts | 17 +++---- src/config/environments/default.env.ts | 6 +-- src/config/environments/prod.env.ts | 6 +-- .../content-layout/ContentLayout.module.scss | 14 ++++++ .../content-layout/ContentLayout.tsx | 25 ++++++++-- 16 files changed, 184 insertions(+), 35 deletions(-) diff --git a/src/apps/admin/src/config/busEvent.config.ts b/src/apps/admin/src/config/busEvent.config.ts index 85888b5dc..a6290e4f3 100644 --- a/src/apps/admin/src/config/busEvent.config.ts +++ b/src/apps/admin/src/config/busEvent.config.ts @@ -3,6 +3,8 @@ */ import { v4 as uuidv4 } from 'uuid' +import { EnvironmentConfig } from '~/config' + import { RequestBusAPI, RequestBusAPIAVScan, @@ -41,9 +43,9 @@ export const CREATE_BUS_EVENT_AV_RESCAN = ( payload: RequestBusAPIAVScanPayload, ): RequestBusAPIAVScan => ({ 'mime-type': 'application/json', - originator: 'submission-processor', + originator: 'review-api-v6', payload, timestamp: new Date() .toISOString(), - topic: 'avscan.action.scan', + topic: EnvironmentConfig.ADMIN.AVSCAN_TOPIC, }) diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss index 4b3979e46..a81b70662 100644 --- a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.module.scss @@ -24,3 +24,25 @@ vertical-align: middle; } } + +.virusScanStatus { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 24px; + min-width: 24px; + color: $black-80; + + svg { + height: 20px; + width: 20px; + } +} + +.virusScanStatusSuccess { + color: $green-120; +} + +.virusScanStatusFailed { + color: $red-110; +} diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx index 5217a13b4..54503dd18 100644 --- a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTable.tsx @@ -6,7 +6,7 @@ import _ from 'lodash' import classNames from 'classnames' import { useWindowSize, WindowSize } from '~/libs/shared' -import { ConfirmModal, Table, TableColumn } from '~/libs/ui' +import { ConfirmModal, IconOutline, Table, TableColumn } from '~/libs/ui' import { EnvironmentConfig } from '~/config' import { getRatingColor } from '~/libs/core' @@ -20,6 +20,40 @@ import SubmissionTableActions from './SubmissionTableActions' import SubmissionTableActionsNonMM from './SubmissionTableActionsNonMM' import styles from './SubmissionTable.module.scss' +const renderVirusScanStatus = (submission: Submission): JSX.Element => { + if (submission.virusScan === true) { + return ( + + + + ) + } + + if (submission.virusScan === false) { + return ( + + + + ) + } + + return -- +} + interface Props { className?: string data: Submission[] @@ -89,6 +123,11 @@ export const SubmissionTable: FC = (props: Props) => { propertyName: 'id', type: 'text', }, + { + label: 'Virus Scan', + renderer: renderVirusScanStatus, + type: 'element', + }, { label: 'Submission date', propertyName: 'submittedDateString', @@ -163,6 +202,10 @@ export const SubmissionTable: FC = (props: Props) => { isRemovingReviewSummations={ props.isRemovingReviewSummations } + isDownloading={props.isDownloading} + downloadSubmission={props.downloadSubmission} + isDoingAvScan={props.isDoingAvScan} + doPostBusEventAvScan={props.doPostBusEventAvScan} setShowConfirmDeleteSubmissionDialog={ setShowConfirmDeleteSubmissionDialog } @@ -189,6 +232,11 @@ export const SubmissionTable: FC = (props: Props) => { propertyName: 'id', type: 'text', }, + { + label: 'Virus Scan', + renderer: renderVirusScanStatus, + type: 'element', + }, { label: 'Time submitted', propertyName: 'submittedDateString', diff --git a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActions.tsx b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActions.tsx index 71fea7aa4..a5a33c19f 100644 --- a/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActions.tsx +++ b/src/apps/admin/src/lib/components/SubmissionTable/SubmissionTableActions.tsx @@ -17,6 +17,10 @@ interface Props { isRunningTest: IsRemovingType isRemovingSubmission: IsRemovingType isRemovingReviewSummations: IsRemovingType + isDownloading: IsRemovingType + isDoingAvScan: IsRemovingType + downloadSubmission: (submissionId: string) => void + doPostBusEventAvScan: (submission: Submission) => void doPostBusEvent: DoPostBusEvent setShowConfirmDeleteSubmissionDialog: Dispatch< SetStateAction @@ -82,6 +86,28 @@ export const SubmissionTableActions: FC = (props: Props) => { > Run Provisional Test
  • +
  • + Download +
  • +
  • + AV Rescan +
  • = props => ( = (props: ContentLayoutProps) => ( -
    - -
    - -
    +
    + +
    + +
    {!!props.title && (
    From e772539dee7d0e26327e863578a9c2b5a5e1983d Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 12 Nov 2025 17:11:28 +0200 Subject: [PATCH 053/125] lint fixes --- .../AiReviewsTable/AiWorkflowRunStatus.tsx | 2 +- .../ScorecardGroup/ScorecardGroup.tsx | 5 ++- .../AiFeedback/AiFeedback.tsx | 1 - .../ReviewAppeal/ReviewAppeal.tsx | 6 ++-- .../ReviewComment/ReviewComment.tsx | 34 ++++++++----------- .../ReviewComments/ReviewComments.tsx | 3 -- .../ReviewManagerComment.tsx | 3 +- .../ScorecardQuestionEdit.tsx | 8 +++-- .../ScorecardScore/ScorecardScore.tsx | 1 - .../ScorecardTotal/ScorecardTotal.tsx | 1 - .../ScorecardViewer.context.tsx | 2 +- .../ScorecardViewer/ScorecardViewer.tsx | 5 ++- .../lib/hooks/useFetchSubmissionReviews.ts | 1 - .../ScorecardDetailsPage.tsx | 3 +- .../ReviewsContext/ReviewsContextProvider.tsx | 4 +-- .../reviews/ReviewsViewer/ReviewsViewer.tsx | 4 ++- .../AiReviewViewer/AiReviewViewer.tsx | 7 ++-- .../components/ReviewViewer/ReviewViewer.tsx | 16 +++++---- .../ReviewsSidebar/ReviewsSidebar.tsx | 1 + .../ScorecardHeader/ScorecardHeader.tsx | 2 +- 20 files changed, 53 insertions(+), 56 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx index 70833eddd..8f5f2b055 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx @@ -33,7 +33,7 @@ export const AiWorkflowRunStatus: FC = props => { } if (props.run) { - return aiRunStatus(props.run); + return aiRunStatus(props.run) } return '' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx index 318ed56c6..f30afc130 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useMemo } from 'react' +import { FC, useCallback } from 'react' import classNames from 'classnames' import { IconOutline } from '~/libs/ui' @@ -18,8 +18,7 @@ interface ScorecardGroupProps { } const ScorecardGroup: FC = props => { - const { aiFeedbackItems, scoreMap }: ScorecardViewerContextValue = useScorecardViewerContext() - const allFeedbackItems = aiFeedbackItems || [] + const { scoreMap }: ScorecardViewerContextValue = useScorecardViewerContext() const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardViewerContext() const isVissible = !toggledItems[props.group.id] diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx index cc204124e..5b57e6f8f 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx @@ -7,7 +7,6 @@ import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../Sc import { ScorecardQuestionRow } from '../ScorecardQuestionRow' import { ScorecardScore } from '../../ScorecardScore' import { MarkdownReview } from '../../../../MarkdownReview' -import { calculateProgressAndScore } from '../../utils' import styles from './AiFeedback.module.scss' diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx index 7dbe97b8a..cba1cdd36 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewAppeal/ReviewAppeal.tsx @@ -1,4 +1,4 @@ -import { FC, PropsWithChildren, useCallback, useContext, useEffect, useMemo, useState } from 'react' +import { FC, PropsWithChildren, useCallback, useEffect, useMemo, useState } from 'react' import { Controller, ControllerRenderProps, @@ -10,6 +10,7 @@ import Select, { SingleValue } from 'react-select' import classNames from 'classnames' import { yupResolver } from '@hookform/resolvers/yup' +import { IconAppealResponse, IconEdit } from '~/apps/review/src/lib/assets/icons' import { AppealInfo, @@ -21,13 +22,12 @@ import { } from '../../../../../../models' import { formAppealResponseSchema, isAppealsResponsePhase } from '../../../../../../utils' import { QUESTION_YES_NO_OPTIONS } from '../../../../../../../config/index.config' -import { ChallengeDetailContext, useChallengeDetailsContext } from '../../../../../../contexts' +import { useChallengeDetailsContext } from '../../../../../../contexts' import { FieldMarkdownEditor } from '../../../../../FieldMarkdownEditor' import { MarkdownReview } from '../../../../../MarkdownReview' import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../../ScorecardViewer.context' import styles from './ReviewAppeal.module.scss' -import { IconAppealResponse, IconEdit } from '~/apps/review/src/lib/assets/icons' interface ReviewAppealProps extends PropsWithChildren { appeal: AppealInfo diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx index a5eedce3d..1d8c717c1 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx @@ -8,19 +8,25 @@ import { import { get, includes } from 'lodash' import { yupResolver } from '@hookform/resolvers/yup' +import { IconAppeal, IconEdit } from '~/apps/review/src/lib/assets/icons' +import { ADMIN, COPILOT, REVIEWER } from '~/apps/review/src/config/index.config' -import { AppealInfo, ChallengeDetailContextModel, FormAppealResponse, ReviewItemInfo, ScorecardQuestion } from '../../../../../../models' +import { + AppealInfo, + ChallengeDetailContextModel, + FormAppealResponse, + ReviewItemInfo, + ScorecardQuestion, +} from '../../../../../../models' import { ReviewItemComment } from '../../../../../../models/ReviewItemComment.model' import { formAppealResponseSchema, isAppealsPhase } from '../../../../../../utils' import { ChallengeDetailContext } from '../../../../../../contexts' import { FieldMarkdownEditor } from '../../../../../FieldMarkdownEditor' import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../../ScorecardViewer.context' import { ScorecardQuestionRow } from '../../ScorecardQuestionRow' +import { ReviewAppeal } from '../ReviewAppeal' import styles from './ReviewComment.module.scss' -import { ReviewAppeal } from '../ReviewAppeal' -import { IconAppeal, IconEdit } from '~/apps/review/src/lib/assets/icons' -import { ADMIN, COPILOT, REVIEWER } from '~/apps/review/src/config/index.config' interface ReviewCommentProps { comment: ReviewItemComment @@ -30,12 +36,12 @@ interface ReviewCommentProps { question: ScorecardQuestion } +// eslint-disable-next-line complexity const ReviewComment: FC = props => { const { isManagerEdit, actionChallengeRole, addAppeal, - doDeleteAppeal, isSavingAppeal, }: ScorecardViewerContextValue = useScorecardViewerContext() @@ -48,9 +54,9 @@ const ReviewComment: FC = props => { const [appealContent, setAppealContent] = useState(props.appeal?.content || '') const [showAppealForm, setShowAppealForm] = useState(false) - const isReviewerRole = useMemo(() => { - return includes([REVIEWER, COPILOT, ADMIN], actionChallengeRole) - }, [actionChallengeRole]) + const isReviewerRole = useMemo(() => ( + includes([REVIEWER, COPILOT, ADMIN], actionChallengeRole) + ), [actionChallengeRole]) const { handleSubmit, @@ -85,14 +91,6 @@ const ReviewComment: FC = props => { setShowAppealForm(true) }, []) - const handleDeleteAppeal = useCallback(() => { - if (doDeleteAppeal && window.confirm('Are you sure you want to delete this appeal?')) { - doDeleteAppeal(props.appeal, () => { - setAppealContent('') - }) - } - }, [doDeleteAppeal, props.appeal]) - const handleCancelAppealForm = useCallback(() => { setShowAppealForm(false) setAppealContent(props.appeal?.content || '') @@ -109,8 +107,7 @@ const ReviewComment: FC = props => {
    - + {isSubmitter && canAddAppeal && (!props.appeal && !showAppealForm && (
    diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx index 5e59a8b92..02ba27ac9 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.context.tsx @@ -157,8 +157,8 @@ export const ScorecardViewerContextProvider: FC = p reviewInfo: props.reviewInfo, saveReviewInfo: props.saveReviewInfo, setIsTouched: reviewFormCtx.setIsTouched, - touchedAllFields: reviewFormCtx.touchedAllFields, setReviewStatus: props.setReviewStatus, + touchedAllFields: reviewFormCtx.touchedAllFields, }), [ props.aiFeedbackItems, props.reviewInfo, diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index 14bbfc6dc..c50d27a01 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -94,7 +94,6 @@ const ScorecardViewerContent: FC = props => { const { form, totalScore, - reviewProgress, isTouched, touchedAllFields, formErrors, @@ -153,7 +152,7 @@ const ScorecardViewerContent: FC = props => { useEffect(() => { if (props.setReviewStatus && props.scorecard) { const isCompleted = props.reviewInfo?.status === 'COMPLETED' - const score = isCompleted ? props.reviewInfo?.finalScore! : totalScore + const score = isCompleted ? props.reviewInfo!.finalScore! : totalScore let status: 'passed' |'failed-score' |'pending' = ( score >= (props.scorecard.minimumPassingScore ?? 50) ? 'passed' : 'failed-score' ) @@ -167,7 +166,7 @@ const ScorecardViewerContent: FC = props => { status, }) } - }, [totalScore, props.scorecard]); + }, [totalScore, props.scorecard]) if (props.isLoading) { return diff --git a/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts b/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts index 5e0715778..ba7e384b3 100644 --- a/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts +++ b/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts @@ -15,7 +15,6 @@ import { } from 'react' import { find, forEach, map } from 'lodash' import { toast } from 'react-toastify' -import { useParams } from 'react-router-dom' import useSWR, { SWRResponse } from 'swr' import { handleError } from '~/apps/admin/src/lib/utils' diff --git a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx index 9c2140a55..fcf4658fb 100644 --- a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx @@ -28,12 +28,11 @@ import { PageWrapper, ScorecardDetails, } from '../../../lib' -import { BreadCrumbData, ChallengeDetailContextModel, ScorecardInfo } from '../../../lib/models' +import { BreadCrumbData, ChallengeDetailContextModel } from '../../../lib/models' import { SubmissionBarInfo } from '../../../lib/components/SubmissionBarInfo' import { ChallengeLinksForAdmin } from '../../../lib/components/ChallengeLinksForAdmin' import { ADMIN, COPILOT, MANAGER } from '../../../config/index.config' import { activeReviewAssignmentsRouteId, rootRoute } from '../../../config/routes.config' -import { ScorecardViewer } from '../../../lib/components/Scorecard' import { useIsEditReview, useIsEditReviewProps } from '../../../lib/hooks/useIsEditReview' import styles from './ScorecardDetailsPage.module.scss' diff --git a/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx b/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx index cc2489f2c..a44d76aca 100644 --- a/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx +++ b/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx @@ -21,7 +21,7 @@ export const ReviewsContextProvider: FC = props => { const workflowId = searchParams.get('workflowId') ?? '' const reviewId = searchParams.get('reviewId') ?? '' - const [reviewStatus, setReviewStatus] = useState({} as ReviewCtxStatus); + const [reviewStatus, setReviewStatus] = useState({} as ReviewCtxStatus) const challengeDetailsCtx = useContext(ChallengeDetailContext) const { challengeInfo }: ChallengeDetailContextModel = challengeDetailsCtx @@ -63,7 +63,7 @@ export const ReviewsContextProvider: FC = props => { [ challengeDetailsCtx, isLoadingCtxData, - reviewId,, + reviewId, scorecard, submissionId, workflow, diff --git a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx index 6f047e579..d667b480d 100644 --- a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx +++ b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx @@ -1,4 +1,5 @@ import { FC, useEffect, useMemo } from 'react' +import { useLocation } from 'react-router-dom' import { NotificationContextType, useNotification } from '~/libs/shared' @@ -7,16 +8,17 @@ import { PageWrapper } from '../../../lib' import { BreadCrumbData, ReviewsContextModel } from '../../../lib/models' import { ReviewsSidebar } from '../components/ReviewsSidebar' import { useReviewsContext } from '../ReviewsContext' - import { AiReviewViewer } from '../components/AiReviewViewer' import { activeReviewAssignmentsRouteId, rootRoute } from '../../../config/routes.config' import { ReviewViewer } from '../components/ReviewViewer' + import styles from './ReviewsViewer.module.scss' const ReviewsViewer: FC = () => { const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() const { challengeInfo, submissionId, workflowRun }: ReviewsContextModel = useReviewsContext() + const location = useLocation() const containsPastChallenges = location.pathname.indexOf('/past-challenges/') const breadCrumb = useMemo(() => [ diff --git a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx index 14fb25281..5fb1c8324 100644 --- a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx @@ -5,19 +5,18 @@ import { ScorecardViewer } from '~/apps/review/src/lib/components/Scorecard' import { ScorecardAttachments } from '~/apps/review/src/lib/components/Scorecard/ScorecardAttachments' import { AiWorkflowRunItemsResponse, useFetchAiWorkflowsRunItems } from '~/apps/review/src/lib/hooks' import { ReviewsContextModel, SelectOption } from '~/apps/review/src/lib/models' + import { ScorecardHeader } from '../ScorecardHeader' import { useReviewsContext } from '../../ReviewsContext' -import styles from './AiReviewViewer.module.scss' -interface AiReviewViewerProps { -} +import styles from './AiReviewViewer.module.scss' const tabItems: SelectOption[] = [ { label: 'Scorecard', value: 'scorecard' }, { label: 'Attachments', value: 'attachments' }, ] -const AiReviewViewer: FC = props => { +const AiReviewViewer: FC = () => { const { scorecard, workflowId, workflowRun }: ReviewsContextModel = useReviewsContext() const [selectedTab, setSelectedTab] = useState('scorecard') const { runItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(workflowId, workflowRun?.id) diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx index 65dd4d9a2..9fafde4f0 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx @@ -3,18 +3,23 @@ import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { SubmissionBarInfo } from '~/apps/review/src/lib/components/SubmissionBarInfo' import { ChallengeLinksForAdmin } from '~/apps/review/src/lib/components/ChallengeLinksForAdmin' import { ScorecardViewer } from '~/apps/review/src/lib/components/Scorecard' -import { useAppNavigate, useFetchSubmissionReviews, useFetchSubmissionReviewsProps, useRole, useRoleProps } from '~/apps/review/src/lib/hooks' +import { + useAppNavigate, + useFetchSubmissionReviews, + useFetchSubmissionReviewsProps, + useRole, + useRoleProps, +} from '~/apps/review/src/lib/hooks' import { ChallengeDetailContextModel, ReviewsContextModel } from '~/apps/review/src/lib/models' import { ChallengeLinks, ConfirmModal, useChallengeDetailsContext } from '~/apps/review/src/lib' import { useIsEditReview, useIsEditReviewProps } from '~/apps/review/src/lib/hooks/useIsEditReview' + import { ADMIN, COPILOT, MANAGER } from '../../../../config/index.config' import { useReviewsContext } from '../../ReviewsContext' -import styles from './ReviewViewer.module.scss' -interface ReviewViewerProps { -} +import styles from './ReviewViewer.module.scss' -const ReviewViewer: FC = props => { +const ReviewViewer: FC = () => { const navigate = useAppNavigate() const { reviewId, setReviewStatus }: ReviewsContextModel = useReviewsContext() @@ -29,7 +34,6 @@ const ReviewViewer: FC = props => { const { challengeInfo, - isLoadingChallengeInfo, }: ChallengeDetailContextModel = useChallengeDetailsContext() const { isEdit: isEditPhase }: useIsEditReviewProps = useIsEditReview() diff --git a/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx index 3d3f725c0..12a9a5f25 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx @@ -9,6 +9,7 @@ import { IconOutline, IconSolid } from '~/libs/ui' import StatusLabel from '~/apps/review/src/lib/components/AiReviewsTable/StatusLabel' import { useReviewsContext } from '../../ReviewsContext' + import styles from './ReviewsSidebar.module.scss' interface ReviewsSidebarProps { diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx index 5bb242a5b..588615d7a 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx @@ -5,9 +5,9 @@ import { ReviewsContextModel } from '~/apps/review/src/lib/models' import { IconClock, IconPremium } from '../../../../lib/assets/icons' import { AiModelModal } from '../AiModelModal' +import { useReviewsContext } from '../../ReviewsContext' import AiModelIcon from '../AiModelIcon' -import { useReviewsContext } from '../../ReviewsContext' import styles from './ScorecardHeader.module.scss' const formatDuration = (duration: Duration): string => [ From 274048e3f220f04de62de1840e92ad13dfcf99a8 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Thu, 13 Nov 2025 16:11:11 +0200 Subject: [PATCH 054/125] Fix typo & fix behavior on score click --- .../src/lib/components/AiReviewsTable/AiReviewsTable.tsx | 8 ++++++-- .../ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx | 9 ++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index 88be43a17..a3e2a5c26 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -1,4 +1,4 @@ -import { FC, useMemo } from 'react' +import { FC, MouseEvent as ReactMouseEvent, useMemo } from 'react' import moment from 'moment' import { useWindowSize, WindowSize } from '~/libs/shared' @@ -22,6 +22,10 @@ interface AiReviewsTableProps { reviewers: { aiWorkflowId: string }[] } +const stopPropagation = (ev: ReactMouseEvent): void => { + ev.stopPropagation() +} + const AiReviewsTable: FC = props => { const aiWorkflowIds = useMemo(() => props.reviewers.map(r => r.aiWorkflowId), [props.reviewers]) const { runs, isLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(props.submission.id, aiWorkflowIds) @@ -102,7 +106,7 @@ const AiReviewsTable: FC = props => { } return ( -
    +
    diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx index f30afc130..1639fc42b 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardGroup/ScorecardGroup.tsx @@ -18,15 +18,14 @@ interface ScorecardGroupProps { } const ScorecardGroup: FC = props => { - const { scoreMap }: ScorecardViewerContextValue = useScorecardViewerContext() - const { toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardViewerContext() + const { scoreMap, toggleItem, toggledItems }: ScorecardViewerContextValue = useScorecardViewerContext() - const isVissible = !toggledItems[props.group.id] + const isVisible = !toggledItems[props.group.id] const toggle = useCallback(() => toggleItem(props.group.id), [props.group, toggleItem]) return (
    -
    +
    {props.index} . @@ -46,7 +45,7 @@ const ScorecardGroup: FC = props => {
    - {isVissible && props.group.sections.map((section, index) => ( + {isVisible && props.group.sections.map((section, index) => ( Date: Thu, 13 Nov 2025 16:18:30 +0200 Subject: [PATCH 055/125] update scorecard confirmation text --- .../Scorecard/ScorecardViewer/ScorecardViewer.tsx | 10 ++++++---- .../components/AiReviewViewer/AiReviewViewer.tsx | 1 - 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index c50d27a01..e2778b8d8 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -34,7 +34,6 @@ import styles from './ScorecardViewer.module.scss' interface ScorecardViewerProps { scorecard: Scorecard | ScorecardInfo aiFeedback?: AiFeedbackItem[] - score?: number reviewInfo?: ReviewInfo isEdit?: boolean isManagerEdit?: boolean @@ -185,11 +184,12 @@ const ScorecardViewerContent: FC = props => {
    )} - {!!props.score && !props.reviewInfo && ( + {totalScore && !!props.aiFeedback && (
    Conclusion

    - Congratulations! You earned a score of + {(totalScore > (props.scorecard.minimumPassingScore ?? 0)) && 'Congratulations!'} + You earned a score of {' '} {totalScore.toFixed(2)} @@ -201,7 +201,9 @@ const ScorecardViewerContent: FC = props => { {(props.scorecard as Scorecard).maxScore?.toFixed(2)} . - You did a good job on passing the scorecard criteria. + {(totalScore > (props.scorecard.minimumPassingScore ?? 0)) + ? 'You did a good job on passing the scorecard criteria.' + : 'You did not pass the scorecard criteria.'} Please check the below sections to see if there is any place for improvement.

    diff --git a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx index 5fb1c8324..f3df4dac5 100644 --- a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx @@ -35,7 +35,6 @@ const AiReviewViewer: FC = () => { )} From 29a514005c80063692af60808f3c7b368944abe8 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 13 Nov 2025 19:29:00 +0100 Subject: [PATCH 056/125] fix: default reviewer schema validation --- src/apps/admin/src/lib/utils/validation-schemas.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/apps/admin/src/lib/utils/validation-schemas.ts b/src/apps/admin/src/lib/utils/validation-schemas.ts index 7f0f60905..3ed8bb800 100644 --- a/src/apps/admin/src/lib/utils/validation-schemas.ts +++ b/src/apps/admin/src/lib/utils/validation-schemas.ts @@ -46,7 +46,7 @@ export const formAddDefaultReviewerSchema: Yup.ObjectSchema schema.optional(), then: schema => schema .required('Member Reviewer Count is required when Is Member Review is checked') @@ -59,7 +59,13 @@ export const formAddDefaultReviewerSchema: Yup.ObjectSchema schema.optional(), + then: schema => schema + .required('Scorecard is required'), + }), shouldOpenOpportunity: Yup.boolean() .required(), timelineTemplateId: Yup.string() From 00a8aac6d8a2acbfa14e35c43a09533c2563f9fb Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Fri, 14 Nov 2025 12:20:25 +1100 Subject: [PATCH 057/125] Have admin access be priority over resource role on challenge, and fix visibility of checkpoint submissions that fail screening --- .../src/lib/hooks/useFetchScreeningReview.ts | 19 +++++++++++++++++++ src/apps/review/src/lib/hooks/useRole.ts | 5 ++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/apps/review/src/lib/hooks/useFetchScreeningReview.ts b/src/apps/review/src/lib/hooks/useFetchScreeningReview.ts index 21ac26f0f..226a73b9f 100644 --- a/src/apps/review/src/lib/hooks/useFetchScreeningReview.ts +++ b/src/apps/review/src/lib/hooks/useFetchScreeningReview.ts @@ -1502,6 +1502,25 @@ export function useFetchScreeningReview(): useFetchScreeningReviewProps { reviewEntries: reviewsToRender, }) + const hasCheckpointReview = rowsForSubmission.some(row => Boolean(row.reviewId)) + const submissionStatus = (item.status ?? '') + .toString() + .trim() + .toUpperCase() + const failedCheckpointScreening = submissionStatus === 'FAILED_CHECKPOINT_SCREENING' + + if (failedCheckpointScreening && !hasCheckpointReview) { + if (debugCheckpointPhases) { + debugLog('checkpointReview.skipSubmission', { + reason: 'failedCheckpointScreeningWithoutReview', + submissionId: item.id, + submissionStatus, + }) + } + + return rows + } + rows.push(...rowsForSubmission) return rows diff --git a/src/apps/review/src/lib/hooks/useRole.ts b/src/apps/review/src/lib/hooks/useRole.ts index a67304c27..6e6bcac5a 100644 --- a/src/apps/review/src/lib/hooks/useRole.ts +++ b/src/apps/review/src/lib/hooks/useRole.ts @@ -69,7 +69,10 @@ const useRole = (): useRoleProps => { return '' } - const normalizedRoles = myRoles.map(role => role.toLowerCase()) + const normalizedRoles = [ + ...myRoles.map(role => role.toLowerCase()), + ...(isTopcoderAdmin ? ['admin'] : []), + ] const rolePriority: ChallengeRole[] = [ 'Admin', 'Manager', From 0c62a26dd6bbabbee15766d2e943728f32f81581 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 14 Nov 2025 17:42:55 +0100 Subject: [PATCH 058/125] fix: lint --- src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts b/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts index 2841abc1d..39f1946d4 100644 --- a/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts +++ b/src/apps/admin/src/lib/models/DefaultChallengeReviewer.model.ts @@ -31,7 +31,7 @@ export interface FormAddDefaultReviewer { typeId: string; trackId: string; timelineTemplateId?: string; - scorecardId: string; + scorecardId?: string; isMemberReview: boolean; memberReviewerCount?: number; phaseName: string; From d05abaac4a922459131ddd297ba6d52c9af2d37f Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Sun, 16 Nov 2025 21:25:03 +0200 Subject: [PATCH 059/125] Various QA Fixes for ai workflwos --- src/apps/review/src/lib/assets/icons/index.ts | 2 ++ .../AiReviewsTable/AiReviewsTable.module.scss | 8 +++--- .../AiReviewsTable/AiReviewsTable.tsx | 9 ++++--- .../AiReviewsTable/AiWorkflowRunStatus.tsx | 2 ++ .../ScorecardViewer/ScorecardViewer.tsx | 3 ++- .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 7 +++-- .../reviews/ReviewsViewer/ReviewsViewer.tsx | 15 +---------- .../AiReviewViewer/AiReviewViewer.tsx | 27 ++++++++++++------- .../ScorecardHeader.module.scss | 9 +++++++ .../ScorecardHeader/ScorecardHeader.tsx | 19 ++++++++++++- .../NotificationsContainer.module.scss | 2 +- .../banner/NotificationBanner.module.scss | 2 +- 12 files changed, 68 insertions(+), 37 deletions(-) diff --git a/src/apps/review/src/lib/assets/icons/index.ts b/src/apps/review/src/lib/assets/icons/index.ts index a1e65cceb..dffc1a502 100644 --- a/src/apps/review/src/lib/assets/icons/index.ts +++ b/src/apps/review/src/lib/assets/icons/index.ts @@ -14,6 +14,7 @@ import { ReactComponent as IconClock } from './icon-clock.svg' import { ReactComponent as IconPremium } from './icon-premium.svg' import { ReactComponent as IconComment } from './icon-comment.svg' import { ReactComponent as IconEdit } from './icon-edit.svg' +import { ReactComponent as IconFile } from './icon-file.svg' export * from './editor/bold' export * from './editor/code' @@ -47,6 +48,7 @@ export { IconPremium, IconComment, IconEdit, + IconFile, } export const phasesIcons = { 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 06fa68e1a..b5c55d397 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.module.scss @@ -10,10 +10,11 @@ width: 100%; border-collapse: collapse; - th { + + &.reviewsTable thead tr th { border-top: 1px solid #A8A8A8; font-weight: bold; - background: #E0E4E8; + background: none; } th, td { @@ -60,7 +61,8 @@ padding-left: $sp-4; padding-right: $sp-4; > * { - flex: 1 1 50%; + flex: 0 0 50%; + white-space: normal; } } .label { diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index a3e2a5c26..6a8a90d6d 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -41,11 +41,14 @@ const AiReviewsTable: FC = props => { { completedAt: (props.submission as BackendSubmission).submittedDate, id: '-1', - score: props.submission.virusScan === true ? 100 : 0, + score: props.submission.virusScan === true ? 0 : 0, status: AiWorkflowRunStatusEnum.SUCCESS, workflow: { description: '', name: 'Virus Scan', + scorecard: { + minimumPassingScore: 1 + } }, } as AiWorkflowRun, ], [runs, props.submission]) @@ -112,7 +115,7 @@ const AiReviewsTable: FC = props => {
    - + @@ -147,7 +150,7 @@ const AiReviewsTable: FC = props => {
    AI Reviewer Review DateScoreScore Result
    {run.status === 'SUCCESS' ? ( - run.workflow.scorecard ? ( + run.workflow.id ? ( diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx index 8f5f2b055..d9928bde6 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiWorkflowRunStatus.tsx @@ -73,7 +73,9 @@ export const AiWorkflowRunStatus: FC = props => { {status === 'failed' && ( } + hideLabel={props.hideLabel} status={status} + label='Failure' score={score} /> )} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index e2778b8d8..4039c0670 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -184,7 +184,7 @@ const ScorecardViewerContent: FC = props => { )} - {totalScore && !!props.aiFeedback && ( + {!!totalScore && !!props.aiFeedback && (
    Conclusion

    @@ -209,6 +209,7 @@ const ScorecardViewerContent: FC = props => {

    )} + isAdmin || !aiRunFailed(r)), + runs, } } diff --git a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx index d667b480d..8bce5249e 100644 --- a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx +++ b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx @@ -1,9 +1,7 @@ -import { FC, useEffect, useMemo } from 'react' +import { FC, useMemo } from 'react' import { useLocation } from 'react-router-dom' -import { NotificationContextType, useNotification } from '~/libs/shared' -import { IconAiReview } from '../../../lib/assets/icons' import { PageWrapper } from '../../../lib' import { BreadCrumbData, ReviewsContextModel } from '../../../lib/models' import { ReviewsSidebar } from '../components/ReviewsSidebar' @@ -15,7 +13,6 @@ import { ReviewViewer } from '../components/ReviewViewer' import styles from './ReviewsViewer.module.scss' const ReviewsViewer: FC = () => { - const { showBannerNotification, removeNotification }: NotificationContextType = useNotification() const { challengeInfo, submissionId, workflowRun }: ReviewsContextModel = useReviewsContext() const location = useLocation() @@ -41,16 +38,6 @@ const ReviewsViewer: FC = () => { }, ], [challengeInfo?.name, challengeInfo?.id, submissionId, containsPastChallenges]) - 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 ( { selected={selectedTab} onChange={setSelectedTab} /> - {!!scorecard && selectedTab === 'scorecard' && ( - - )} - {selectedTab === 'attachments' && ( - + {workflowRun && [AiWorkflowRunStatusEnum.CANCELLED, AiWorkflowRunStatusEnum.FAILURE].includes(workflowRun.status) ? ( +
    + AI run failed - no scorecard results are available +
    + ) : ( + <> + {!!scorecard && selectedTab === 'scorecard' && ( + + )} + + {selectedTab === 'attachments' && ( + + )} + )} ) diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss index b84af1272..c4d1af9a7 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss @@ -97,6 +97,15 @@ color: var(--FontColor); } + svg { + &.sm { + height: 16px; + } + path { + fill: #0A0A0A; + } + } + @include ltemd { margin-left: 0; } diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx index 588615d7a..876b9f4e2 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx @@ -2,8 +2,9 @@ import { FC, useCallback, useMemo, useState } from 'react' import moment, { Duration } from 'moment' import { ReviewsContextModel } from '~/apps/review/src/lib/models' +import { useRolePermissions, UseRolePermissionsResult } from '~/apps/review/src/lib/hooks' -import { IconClock, IconPremium } from '../../../../lib/assets/icons' +import { IconClock, IconFile, IconPremium } from '../../../../lib/assets/icons' import { AiModelModal } from '../AiModelModal' import { useReviewsContext } from '../../ReviewsContext' import AiModelIcon from '../AiModelIcon' @@ -19,6 +20,7 @@ const formatDuration = (duration: Duration): string => [ const ScorecardHeader: FC = () => { const { workflow, workflowRun }: ReviewsContextModel = useReviewsContext() + const { isAdmin }: UseRolePermissionsResult = useRolePermissions() const runDuration = useMemo(() => ( workflowRun && workflowRun.completedAt && workflowRun.startedAt && moment.duration( +new Date(workflowRun.completedAt) - +new Date(workflowRun.startedAt), @@ -64,6 +66,21 @@ const ScorecardHeader: FC = () => { {!!runDuration && formatDuration(runDuration)} + {isAdmin && ( + + + + Git log: + {' '} + {workflowRun.gitRunUrl && ( +
    + # + {workflowRun.gitRunId} + + )} + + + )}

    diff --git a/src/libs/shared/lib/components/notifications/NotificationsContainer.module.scss b/src/libs/shared/lib/components/notifications/NotificationsContainer.module.scss index 9cbc394ef..de21a4cd9 100644 --- a/src/libs/shared/lib/components/notifications/NotificationsContainer.module.scss +++ b/src/libs/shared/lib/components/notifications/NotificationsContainer.module.scss @@ -3,5 +3,5 @@ .wrap { position: relative; width: 100%; - z-index: 1000; + z-index: 20; } 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 d5fc6bfdd..33ae97e5e 100644 --- a/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss +++ b/src/libs/ui/lib/components/notification/banner/NotificationBanner.module.scss @@ -35,7 +35,7 @@ .icon { flex: 0 0; margin-right: $sp-2; - > svg path { + > svg path[fill] { fill: $tc-white; } } From 3c3146b1dde308c9fafdfc7f5462028aeed35eb9 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Sun, 16 Nov 2025 21:51:02 +0200 Subject: [PATCH 060/125] lint fixes --- .../components/AiReviewsTable/AiReviewsTable.tsx | 4 ++-- .../ScorecardViewer/ScorecardViewer.tsx | 1 - .../reviews/ReviewsViewer/ReviewsViewer.tsx | 1 - .../components/AiReviewViewer/AiReviewViewer.tsx | 16 +++++++++++++--- .../ScorecardHeader/ScorecardHeader.tsx | 2 +- 5 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index 6a8a90d6d..19e2cf73e 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -47,8 +47,8 @@ const AiReviewsTable: FC = props => { description: '', name: 'Virus Scan', scorecard: { - minimumPassingScore: 1 - } + minimumPassingScore: 1, + }, }, } as AiWorkflowRun, ], [runs, props.submission]) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index 4039c0670..2e685c3ae 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -209,7 +209,6 @@ const ScorecardViewerContent: FC = props => { )} - { const { scorecard, workflowId, workflowRun }: ReviewsContextModel = useReviewsContext() const [selectedTab, setSelectedTab] = useState('scorecard') const { runItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(workflowId, workflowRun?.id) + const isFailedRun = useMemo(() => ( + workflowRun && [ + AiWorkflowRunStatusEnum.CANCELLED, + AiWorkflowRunStatusEnum.FAILURE, + ].includes(workflowRun.status) + ), [workflowRun]) return (

    @@ -32,7 +42,7 @@ const AiReviewViewer: FC = () => { onChange={setSelectedTab} /> - {workflowRun && [AiWorkflowRunStatusEnum.CANCELLED, AiWorkflowRunStatusEnum.FAILURE].includes(workflowRun.status) ? ( + {isFailedRun ? (
    AI run failed - no scorecard results are available
    diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx index 876b9f4e2..f6ca3a270 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx @@ -73,7 +73,7 @@ const ScorecardHeader: FC = () => { Git log: {' '} {workflowRun.gitRunUrl && ( - + # {workflowRun.gitRunId} From 318692975c429414b3a1645b1298f39cc4e308bc Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Sun, 16 Nov 2025 22:04:39 +0200 Subject: [PATCH 061/125] fix score --- .../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 19e2cf73e..865137aed 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -41,7 +41,7 @@ const AiReviewsTable: FC = props => { { completedAt: (props.submission as BackendSubmission).submittedDate, id: '-1', - score: props.submission.virusScan === true ? 0 : 0, + score: props.submission.virusScan === true ? 100 : 0, status: AiWorkflowRunStatusEnum.SUCCESS, workflow: { description: '', From cc781f38afa33318b7c6db0ca008b3d194251852 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Sun, 16 Nov 2025 22:05:56 +0200 Subject: [PATCH 062/125] noopener --- .../reviews/components/ScorecardHeader/ScorecardHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx index f6ca3a270..ac84ecb63 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx @@ -73,7 +73,7 @@ const ScorecardHeader: FC = () => { Git log: {' '} {workflowRun.gitRunUrl && ( - + # {workflowRun.gitRunId} From 0876ac421307baf4726088b050893a398ca0220f Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 17 Nov 2025 17:16:14 +1100 Subject: [PATCH 063/125] Use updated tc-auth-lib --- package.json | 2 +- yarn.lock | 26 +++++++++++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 1f2c0c9a0..fab70bf67 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "sass": "^1.79.0", "styled-components": "^5.3.6", "swr": "^1.3.0", - "tc-auth-lib": "topcoder-platform/tc-auth-lib#master", + "tc-auth-lib": "topcoder-platform/tc-auth-lib#v2.0", "tinymce": "^7.9.1", "typescript": "^4.8.4", "universal-navigation": "https://github.com/topcoder-platform/universal-navigation#9fc50d938be7182", diff --git a/yarn.lock b/yarn.lock index d7ceec7c7..44008233a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10624,7 +10624,7 @@ is-bun-module@^2.0.0: dependencies: semver "^7.7.1" -is-callable@^1.2.7: +is-callable@^1.1.3, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== @@ -17573,7 +17573,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17587,6 +17587,13 @@ strip-ansi@^3.0.0: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" @@ -17956,9 +17963,9 @@ tar@^6.2.1: mkdirp "^1.0.3" yallist "^4.0.0" -tc-auth-lib@topcoder-platform/tc-auth-lib#master: +tc-auth-lib@topcoder-platform/tc-auth-lib#v2.0: version "1.0.2" - resolved "https://codeload.github.com/topcoder-platform/tc-auth-lib/tar.gz/1c9be61eb32583beeb74f596fe58bb3ada97462d" + resolved "https://codeload.github.com/topcoder-platform/tc-auth-lib/tar.gz/56996006ee5918b3e77fc5a8ab005ae738b4de12" dependencies: lodash "^4.17.19" @@ -19623,7 +19630,7 @@ workbox-window@6.6.1: "@types/trusted-types" "^2.0.2" workbox-core "6.6.1" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19641,6 +19648,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From e4b1ea7d9d503459619d49617abade1ebade9b1f Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 17 Nov 2025 08:48:53 +0200 Subject: [PATCH 064/125] new scales with 0 --- .../review/src/lib/models/Scorecard.model.ts | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/apps/review/src/lib/models/Scorecard.model.ts b/src/apps/review/src/lib/models/Scorecard.model.ts index 5014b0e96..01faa9e29 100644 --- a/src/apps/review/src/lib/models/Scorecard.model.ts +++ b/src/apps/review/src/lib/models/Scorecard.model.ts @@ -5,20 +5,21 @@ import { ScorecardGroup } from './ScorecardGroup.model' export enum ProjectType { - DEVELOPMENT = 'DEVELOPMENT', - DATA_SCIENCE = 'DATA_SCIENCE', - DESIGN = 'DESIGN', - QUALITY_ASSURANCE = 'QUALITY_ASSURANCE', + DEVELOPMENT = 'DEVELOPMENT', + DATA_SCIENCE = 'DATA_SCIENCE', + DESIGN = 'DESIGN', + QUALITY_ASSURANCE = 'QUALITY_ASSURANCE', } -export const ProjectTypeLabels: Record = { - [ProjectType.DEVELOPMENT]: 'Development', - [ProjectType.DATA_SCIENCE]: 'Data Science', - [ProjectType.DESIGN]: 'Design', - [ProjectType.QUALITY_ASSURANCE]: 'Quality Assurance', - DEVELOP: 'Development', - QA: 'Quality Assurance', -} +export const ProjectTypeLabels: Record = + { + [ProjectType.DEVELOPMENT]: 'Development', + [ProjectType.DATA_SCIENCE]: 'Data Science', + [ProjectType.DESIGN]: 'Design', + [ProjectType.QUALITY_ASSURANCE]: 'Quality Assurance', + DEVELOP: 'Development', + QA: 'Quality Assurance', + } export const categoryByProjectType = { DATA_SCIENCE: ['Marathon Match'], @@ -57,13 +58,12 @@ export const categoryByProjectType = { ], } satisfies Record -export const scorecardCategories = Object.values(categoryByProjectType) - .flat() +export const scorecardCategories = Object.values(categoryByProjectType).flat() export enum ScorecardStatus { - ACTIVE = 'ACTIVE', - INACTIVE = 'INACTIVE', - DELETED = 'DELETED', + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', + DELETED = 'DELETED', } export const ScorecardStatusLabels: Record = { @@ -73,14 +73,14 @@ export const ScorecardStatusLabels: Record = { } export enum ScorecardType { - SCREENING = 'SCREENING', - REVIEW = 'REVIEW', - APPROVAL = 'APPROVAL', - POST_MORTEM = 'POST_MORTEM', - SPECIFICATION_REVIEW = 'SPECIFICATION_REVIEW', - CHECKPOINT_SCREENING = 'CHECKPOINT_SCREENING', - CHECKPOINT_REVIEW = 'CHECKPOINT_REVIEW', - ITERATIVE_REVIEW = 'ITERATIVE_REVIEW', + SCREENING = 'SCREENING', + REVIEW = 'REVIEW', + APPROVAL = 'APPROVAL', + POST_MORTEM = 'POST_MORTEM', + SPECIFICATION_REVIEW = 'SPECIFICATION_REVIEW', + CHECKPOINT_SCREENING = 'CHECKPOINT_SCREENING', + CHECKPOINT_REVIEW = 'CHECKPOINT_REVIEW', + ITERATIVE_REVIEW = 'ITERATIVE_REVIEW', } export const ScorecardTypeLabels: Record = { @@ -95,6 +95,10 @@ export const ScorecardTypeLabels: Record = { } export const ScorecardScales = { + 'scale(0-4)': 'Scale 0-4', + 'scale(0-5)': 'Scale 0-5', + 'scale(0-10)': 'Scale 0-10', + 'scale(0-100)': 'Scale 0-100', 'scale(1-4)': 'Scale 1-4', 'scale(1-5)': 'Scale 1-5', 'scale(1-10)': 'Scale 1-10', From 4c30a9a60f8e1f34b60ecef74cbd21b75f8736df Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 17 Nov 2025 08:58:21 +0200 Subject: [PATCH 065/125] lint fixes --- src/apps/review/src/lib/models/Scorecard.model.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/apps/review/src/lib/models/Scorecard.model.ts b/src/apps/review/src/lib/models/Scorecard.model.ts index 01faa9e29..123fc2520 100644 --- a/src/apps/review/src/lib/models/Scorecard.model.ts +++ b/src/apps/review/src/lib/models/Scorecard.model.ts @@ -11,8 +11,8 @@ export enum ProjectType { QUALITY_ASSURANCE = 'QUALITY_ASSURANCE', } -export const ProjectTypeLabels: Record = - { +export const ProjectTypeLabels: Record + = { [ProjectType.DEVELOPMENT]: 'Development', [ProjectType.DATA_SCIENCE]: 'Data Science', [ProjectType.DESIGN]: 'Design', @@ -58,7 +58,8 @@ export const categoryByProjectType = { ], } satisfies Record -export const scorecardCategories = Object.values(categoryByProjectType).flat() +export const scorecardCategories = Object.values(categoryByProjectType) + .flat() export enum ScorecardStatus { ACTIVE = 'ACTIVE', From 165e193842a698d2bdbdf6482a01be853e3e8663 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 17 Nov 2025 18:28:27 +0530 Subject: [PATCH 066/125] Scorecards attachments ui --- .../ScorecardAttachments.module.scss | 34 +++++ .../ScorecardAttachments.tsx | 138 +++++++++++++++++- .../src/lib/components/common/columnUtils.ts | 25 ++++ src/apps/review/src/lib/constants.ts | 1 + .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 113 +++++++++++++- 5 files changed, 303 insertions(+), 8 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss index e69de29bb..450f8f115 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss @@ -0,0 +1,34 @@ +@import '@libs/ui/styles/includes'; + +.tableCell { + vertical-align: middle; +} + +.filenameCell { + display: flex; + align-items: center; + gap: 6px; + color: $link-blue-dark; + cursor: pointer; + opacity: 1; + transition: opacity 0.15s ease; + + &:hover { + text-decoration: underline; + } +} + +.downloading { + cursor: wait; + opacity: 0.6; +} + +.expired { + cursor: not-allowed; + opacity: 0.4; + color: #999; + + &:hover { + text-decoration: none; + } +} \ No newline at end of file diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx index a0091861c..e21937bd8 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx @@ -1,15 +1,139 @@ -import { FC } from 'react' +import { FC, useCallback, useMemo } from 'react' +import { noop } from 'lodash' +import classNames from 'classnames' +import moment from 'moment' -// import styles from './ScorecardAttachments.module.scss' +import { IconOutline, Table, TableColumn } from '~/libs/ui' +import { useAiScorecardContext } from '~/apps/review/src/pages/ai-scorecards/AiScorecardContext' + +import { AiScorecardContextModel } from '../../../models' +import { AiWorkflowRunArtifact, + AiWorkflowRunArtifactDownloadResponse, + AiWorkflowRunAttachmentsResponse, + useDownloadAiWorkflowsRunArtifact, useFetchAiWorkflowsRunAttachments } from '../../../hooks' +import { TableWrapper } from '../../TableWrapper' +import { TABLE_DATE_FORMAT } from '../../../constants' +import { formatFileSize } from '../../common' + +import styles from './ScorecardAttachments.module.scss' interface ScorecardAttachmentsProps { className?: string } -const ScorecardAttachments: FC = props => ( -
    - attachments -
    -) +const ScorecardAttachments: FC = (props: ScorecardAttachmentsProps) => { + const className = props.className + // const { width: screenWidth }: WindowSize = useWindowSize() + // const isTablet = useMemo(() => screenWidth <= 1000, [screenWidth]) + const { workflowId, workflowRun }: AiScorecardContextModel = useAiScorecardContext() + const { artifacts }: AiWorkflowRunAttachmentsResponse + = useFetchAiWorkflowsRunAttachments(workflowId, workflowRun?.id) + const { download, isDownloading }: AiWorkflowRunArtifactDownloadResponse = useDownloadAiWorkflowsRunArtifact( + workflowId, + workflowRun?.id, + ) + + const handleDownload = useCallback( + async (artifactId: number): Promise => { + await download(artifactId) + }, + [download], + ) + + const createDownloadHandler = useCallback( + (id: number) => () => handleDownload(id), + [handleDownload], + ) + + console.log('attachments', artifacts) + + const columns = useMemo[]>( + () => [ + { + className: classNames(styles.tableCell), + label: 'Filename', + propertyName: 'name', + renderer: (attachment: AiWorkflowRunArtifact) => { + const isExpired = attachment.expired + + return ( +
    + {attachment.name} + {isExpired && Expired} +
    + ) + }, + type: 'element', + }, + { + className: classNames(styles.tableCell), + label: 'Type', + renderer: () => ( +
    + + Artifact +
    + ), + type: 'element', + }, + { + className: classNames(styles.tableCell), + label: 'Size', + propertyName: 'sizeInBytes', + renderer: (attachment: AiWorkflowRunArtifact) => ( +
    {formatFileSize(attachment.size_in_bytes)}
    + ), + type: 'element', + }, + { + className: styles.tableCell, + label: 'Attached Date', + renderer: (attachment: AiWorkflowRunArtifact) => ( + + {moment(attachment.created_at) + .local() + .format(TABLE_DATE_FORMAT)} + + ), + type: 'element', + }, + ], + [], + ) + + return ( + + {artifacts ? ( + + ) : ( +
    No attachments
    + )} + + + ) + +} export default ScorecardAttachments diff --git a/src/apps/review/src/lib/components/common/columnUtils.ts b/src/apps/review/src/lib/components/common/columnUtils.ts index 0bfceebcc..d50da47f6 100644 --- a/src/apps/review/src/lib/components/common/columnUtils.ts +++ b/src/apps/review/src/lib/components/common/columnUtils.ts @@ -69,3 +69,28 @@ export const getProfileUrl = (handle: string): string => { return `${normalizedBase}/${encodeURIComponent(handle)}` } + +/** + * converts size_in_bytes into KB / MB / GB with correct formatting. + */ +export const formatFileSize = (bytes: number): string => { + if (!bytes || bytes < 0) return '0 B' + + const KB = 1024 + const MB = KB * 1024 + const GB = MB * 1024 + + if (bytes >= GB) { + return `${(bytes / GB).toFixed(2)} GB` + } + + if (bytes >= MB) { + return `${(bytes / MB).toFixed(2)} MB` + } + + if (bytes >= KB) { + return `${(bytes / KB).toFixed(2)} KB` + } + + return `${bytes} B` +} diff --git a/src/apps/review/src/lib/constants.ts b/src/apps/review/src/lib/constants.ts index 89f4cbd37..b03395e68 100644 --- a/src/apps/review/src/lib/constants.ts +++ b/src/apps/review/src/lib/constants.ts @@ -1,2 +1,3 @@ export const SUBMISSION_TYPE_CONTEST = 'CONTEST_SUBMISSION' export const SUBMISSION_TYPE_CHECKPOINT = 'CHECKPOINT_SUBMISSION' +export const TABLE_DATE_FORMAT = 'MMM DD, HH:mm A' diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 3120c869a..05b529f24 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import useSWR, { SWRResponse } from 'swr' import { EnvironmentConfig } from '~/config' @@ -46,6 +46,23 @@ export interface AiWorkflowRun { workflow: AiWorkflow } +export interface AiWorkflowRunArtifact { + id: number + name: string + size_in_bytes: number + url: string + archive_download_url: string + expired: boolean + workflow_run: { + id: number + repository_id: number + head_sha: string + } + created_at: string + updated_at: string + expires_at: string +} + export type AiWorkflowRunItem = AiFeedbackItem const TC_API_BASE_URL = EnvironmentConfig.API.V6 @@ -60,6 +77,22 @@ export interface AiWorkflowRunItemsResponse { isLoading: boolean } +export interface AiWorkflowRunAttachmentsApiResponse { + artifacts: AiWorkflowRunArtifact[] + total_count: number +} + +export interface AiWorkflowRunAttachmentsResponse { + artifacts: AiWorkflowRunArtifact[] + totalCount: number + isLoading: boolean +} + +export interface AiWorkflowRunArtifactDownloadResponse { + download: (artifactId: number) => Promise + isDownloading: boolean +} + export const aiRunInProgress = (aiRun: Pick): boolean => [ AiWorkflowRunStatusEnum.INIT, AiWorkflowRunStatusEnum.QUEUED, @@ -139,3 +172,81 @@ export function useFetchAiWorkflowsRunItems( runItems, } } + +export function useFetchAiWorkflowsRunAttachments( + workflowId: string, + runId: string | undefined, +): AiWorkflowRunAttachmentsResponse { + const { + data, + error: fetchError, + isValidating: isLoading, + }: SWRResponse = useSWR< + AiWorkflowRunAttachmentsApiResponse, + Error + >( + `${TC_API_BASE_URL}/workflows/${workflowId}/runs/${runId}/attachments`, + { + isPaused: () => !workflowId || !runId, + }, + ) + + useEffect(() => { + if (fetchError) { + handleError(fetchError) + } + }, [fetchError]) + + return { + artifacts: data?.artifacts ?? [], + isLoading, + totalCount: data?.total_count ?? 0, + } +} + +export function useDownloadAiWorkflowsRunArtifact( + workflowId: string, + runId: string | undefined, +): AiWorkflowRunArtifactDownloadResponse { + const [isDownloading, setIsDownloading] = useState(false) + + const download = async (artifactId: number): Promise => { + if (!workflowId || !runId || !artifactId) return + + try { + setIsDownloading(true) + + const url = `${TC_API_BASE_URL}/workflows/${workflowId}/runs/${runId}/attachments/${artifactId}/zip` + + const response = await fetch(url, { + credentials: 'include', + method: 'GET', + }) + + if (!response.ok) { + throw new Error(`Download failed with status ${response.status}`) + } + + const blob = await response.blob() + + // Create a blob URL and trigger browser download + const objectUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = objectUrl + link.download = `artifact-${artifactId}.zip` + link.click() + + // Cleanup + window.URL.revokeObjectURL(objectUrl) + } catch (err) { + handleError(err as Error) + } finally { + setIsDownloading(false) + } + } + + return { + download, + isDownloading, + } +} From 2d9be6f58ce3ad61de734e77754e71f72a67f978 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 17 Nov 2025 18:58:55 +0530 Subject: [PATCH 067/125] Fix updated context --- .../ScorecardAttachments/ScorecardAttachments.tsx | 6 +++--- src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx index e21937bd8..5b4aabfd6 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx @@ -4,9 +4,8 @@ import classNames from 'classnames' import moment from 'moment' import { IconOutline, Table, TableColumn } from '~/libs/ui' -import { useAiScorecardContext } from '~/apps/review/src/pages/ai-scorecards/AiScorecardContext' +import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext' -import { AiScorecardContextModel } from '../../../models' import { AiWorkflowRunArtifact, AiWorkflowRunArtifactDownloadResponse, AiWorkflowRunAttachmentsResponse, @@ -14,6 +13,7 @@ import { AiWorkflowRunArtifact, import { TableWrapper } from '../../TableWrapper' import { TABLE_DATE_FORMAT } from '../../../constants' import { formatFileSize } from '../../common' +import { ReviewsContextModel } from '../../../models' import styles from './ScorecardAttachments.module.scss' @@ -25,7 +25,7 @@ const ScorecardAttachments: FC = (props: ScorecardAtt const className = props.className // const { width: screenWidth }: WindowSize = useWindowSize() // const isTablet = useMemo(() => screenWidth <= 1000, [screenWidth]) - const { workflowId, workflowRun }: AiScorecardContextModel = useAiScorecardContext() + const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext() const { artifacts }: AiWorkflowRunAttachmentsResponse = useFetchAiWorkflowsRunAttachments(workflowId, workflowRun?.id) const { download, isDownloading }: AiWorkflowRunArtifactDownloadResponse = useDownloadAiWorkflowsRunArtifact( diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 2b33d3bbe..706a7b961 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -172,8 +172,8 @@ export function useFetchAiWorkflowsRunItems( } export function useFetchAiWorkflowsRunAttachments( - workflowId: string, - runId: string | undefined, + workflowId?: string, + runId?: string | undefined, ): AiWorkflowRunAttachmentsResponse { const { data, @@ -203,8 +203,8 @@ export function useFetchAiWorkflowsRunAttachments( } export function useDownloadAiWorkflowsRunArtifact( - workflowId: string, - runId: string | undefined, + workflowId?: string, + runId?: string | undefined, ): AiWorkflowRunArtifactDownloadResponse { const [isDownloading, setIsDownloading] = useState(false) From 9b29f7d44cca98ce1ea623e5306940dc4ed4493c Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 17 Nov 2025 20:07:26 +0530 Subject: [PATCH 068/125] PM-2179 update logic --- .../ScorecardAttachments.module.scss | 11 +++++- .../ScorecardAttachments.tsx | 6 +-- .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 38 +++++++++---------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss index 450f8f115..559bee5d9 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss @@ -25,10 +25,19 @@ .expired { cursor: not-allowed; - opacity: 0.4; color: #999; &:hover { text-decoration: none; } +} + +.artifactType { + display: flex; + gap: 5px; + align-items: center; + + .artifactIcon { + stroke: #00797A; + } } \ No newline at end of file diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx index 5b4aabfd6..c1f129da5 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx @@ -45,8 +45,6 @@ const ScorecardAttachments: FC = (props: ScorecardAtt [handleDownload], ) - console.log('attachments', artifacts) - const columns = useMemo[]>( () => [ { @@ -68,7 +66,7 @@ const ScorecardAttachments: FC = (props: ScorecardAtt onClick={!isExpired ? createDownloadHandler(attachment.id) : undefined} > {attachment.name} - {isExpired && Expired} + {isExpired && (Link Expired)} ) }, @@ -79,7 +77,7 @@ const ScorecardAttachments: FC = (props: ScorecardAtt label: 'Type', renderer: () => (
    - + Artifact
    ), diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index 706a7b961..db90c738a 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react' import useSWR, { SWRResponse } from 'swr' import { EnvironmentConfig } from '~/config' -import { xhrGetAsync } from '~/libs/core' +import { xhrGetAsync, xhrGetBlobAsync } from '~/libs/core' import { handleError } from '~/libs/shared/lib/utils/handle-error' import { AiFeedbackItem, Scorecard } from '../models' @@ -216,26 +216,22 @@ export function useDownloadAiWorkflowsRunArtifact( const url = `${TC_API_BASE_URL}/workflows/${workflowId}/runs/${runId}/attachments/${artifactId}/zip` - const response = await fetch(url, { - credentials: 'include', - method: 'GET', - }) - - if (!response.ok) { - throw new Error(`Download failed with status ${response.status}`) - } - - const blob = await response.blob() - - // Create a blob URL and trigger browser download - const objectUrl = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = objectUrl - link.download = `artifact-${artifactId}.zip` - link.click() - - // Cleanup - window.URL.revokeObjectURL(objectUrl) + xhrGetBlobAsync(url) + .then(blob => { + const objectUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = objectUrl + link.download = `artifact-${artifactId}.zip` + + document.body.appendChild(link) + link.click() + link.remove() + + window.URL.revokeObjectURL(objectUrl) + }) + .catch(err => { + handleError(err as Error) + }) } catch (err) { handleError(err as Error) } finally { From 6eb0aa6f039eb5beeba41b21c1a737d3c7eadf6a Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 17 Nov 2025 20:21:47 +0530 Subject: [PATCH 069/125] Add screen height --- .../ScorecardAttachments/ScorecardAttachments.module.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss index 559bee5d9..5a979bac5 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss @@ -1,5 +1,9 @@ @import '@libs/ui/styles/includes'; +.container { + min-height: calc($content-height - 250px); +} + .tableCell { vertical-align: middle; } From e4be6d9fa24f7c406bd1a13018ace556d2e16e13 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Tue, 18 Nov 2025 13:51:47 +0530 Subject: [PATCH 070/125] Add mobile view, fix issues from PR feedback --- .../ScorecardAttachments.module.scss | 44 +++++++++- .../ScorecardAttachments.tsx | 83 +++++++++++++++++-- .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 54 ++++++------ .../AiReviewViewer/AiReviewViewer.tsx | 14 ++-- 4 files changed, 152 insertions(+), 43 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss index 5a979bac5..8eda551a0 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.module.scss @@ -1,7 +1,19 @@ @import '@libs/ui/styles/includes'; -.container { +.tableWrapper { min-height: calc($content-height - 250px); + font-size: 14px; + + &:global(.enhanced-table) { + table { + th, + td { + text-align: left; + background-color: white; + } + + } + } } .tableCell { @@ -44,4 +56,32 @@ .artifactIcon { stroke: #00797A; } -} \ No newline at end of file +} + +.noAttachmentText { + text-align: center; +} + +.mobileRow { + padding: 16px 8px; + border-bottom: 1px solid #A8A8A8; +} + +.mobileHeader { + display: flex; + gap: 12px; +} + +.mobileExpanded { + padding: 16px 20px 0px 32px; +} + +.rowItem { + display: flex; + justify-content: space-between; +} + +.rowItemHeading { + font-weight: 700; + color: #0a0a0a; +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx index c1f129da5..54b78f74e 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardAttachments/ScorecardAttachments.tsx @@ -1,10 +1,11 @@ -import { FC, useCallback, useMemo } from 'react' +import { FC, useCallback, useMemo, useState } from 'react' import { noop } from 'lodash' import classNames from 'classnames' import moment from 'moment' import { IconOutline, Table, TableColumn } from '~/libs/ui' import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext' +import { useWindowSize, WindowSize } from '~/libs/shared' import { AiWorkflowRunArtifact, AiWorkflowRunArtifactDownloadResponse, @@ -23,8 +24,8 @@ interface ScorecardAttachmentsProps { const ScorecardAttachments: FC = (props: ScorecardAttachmentsProps) => { const className = props.className - // const { width: screenWidth }: WindowSize = useWindowSize() - // const isTablet = useMemo(() => screenWidth <= 1000, [screenWidth]) + const { width: screenWidth }: WindowSize = useWindowSize() + const isTablet = useMemo(() => screenWidth <= 1000, [screenWidth]) const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext() const { artifacts }: AiWorkflowRunAttachmentsResponse = useFetchAiWorkflowsRunAttachments(workflowId, workflowRun?.id) @@ -105,28 +106,94 @@ const ScorecardAttachments: FC = (props: ScorecardAtt type: 'element', }, ], + [createDownloadHandler, isDownloading], + ) + + const [openRow, setOpenRow] = useState(undefined) + const toggleRow = useCallback( + (id: number) => () => { + setOpenRow(prev => (prev === id ? undefined : id)) + }, [], ) + const renderMobileRow = (attachment: AiWorkflowRunArtifact): JSX.Element => { + const isExpired = attachment.expired + const downloading = isDownloading + const isOpen = openRow === attachment.id + + return ( +
    + {/* Top collapsed row */} +
    + +
    + {attachment.name} +
    + +
    + + {/* Expanded content */} + {isOpen && ( +
    +
    + Type: +
    + + Artifact +
    +
    + +
    + Size: + {formatFileSize(attachment.size_in_bytes)} +
    + +
    + Date: + {moment(attachment.created_at) + .local() + .format(TABLE_DATE_FORMAT)} +
    +
    + )} +
    + ) + } return ( - {artifacts ? ( + {!artifacts || artifacts.length === 0 ? ( +
    No attachments
    + ) : isTablet ? ( +
    + {artifacts.map(renderMobileRow)} +
    + ) : (
    - ) : ( -
    No attachments
    )} diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index db90c738a..c7afa253d 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useCallback, useEffect, useState } from 'react' import useSWR, { SWRResponse } from 'swr' import { EnvironmentConfig } from '~/config' @@ -204,40 +204,38 @@ export function useFetchAiWorkflowsRunAttachments( export function useDownloadAiWorkflowsRunArtifact( workflowId?: string, - runId?: string | undefined, + runId?: string, ): AiWorkflowRunArtifactDownloadResponse { const [isDownloading, setIsDownloading] = useState(false) - const download = async (artifactId: number): Promise => { - if (!workflowId || !runId || !artifactId) return + const download = useCallback( + async (artifactId: number): Promise => { + if (!workflowId || !runId || !artifactId) return - try { setIsDownloading(true) - const url = `${TC_API_BASE_URL}/workflows/${workflowId}/runs/${runId}/attachments/${artifactId}/zip` - xhrGetBlobAsync(url) - .then(blob => { - const objectUrl = window.URL.createObjectURL(blob) - const link = document.createElement('a') - link.href = objectUrl - link.download = `artifact-${artifactId}.zip` - - document.body.appendChild(link) - link.click() - link.remove() - - window.URL.revokeObjectURL(objectUrl) - }) - .catch(err => { - handleError(err as Error) - }) - } catch (err) { - handleError(err as Error) - } finally { - setIsDownloading(false) - } - } + try { + const blob = await xhrGetBlobAsync(url) + + const objectUrl = window.URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = objectUrl + link.download = `artifact-${artifactId}.zip` + + document.body.appendChild(link) + link.click() + link.remove() + + window.URL.revokeObjectURL(objectUrl) + } catch (err) { + handleError(err as Error) + } finally { + setIsDownloading(false) + } + }, + [workflowId, runId], + ) return { download, diff --git a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx index ca1f14802..fe94c883d 100644 --- a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx @@ -4,8 +4,10 @@ import { Tabs } from '~/apps/review/src/lib' import { ScorecardViewer } from '~/apps/review/src/lib/components/Scorecard' import { ScorecardAttachments } from '~/apps/review/src/lib/components/Scorecard/ScorecardAttachments' import { + AiWorkflowRunAttachmentsResponse, AiWorkflowRunItemsResponse, AiWorkflowRunStatusEnum, + useFetchAiWorkflowsRunAttachments, useFetchAiWorkflowsRunItems, } from '~/apps/review/src/lib/hooks' import { ReviewsContextModel, SelectOption } from '~/apps/review/src/lib/models' @@ -15,15 +17,17 @@ import { useReviewsContext } from '../../ReviewsContext' import styles from './AiReviewViewer.module.scss' -const tabItems: SelectOption[] = [ - { label: 'Scorecard', value: 'scorecard' }, - { label: 'Attachments', value: 'attachments' }, -] - const AiReviewViewer: FC = () => { const { scorecard, workflowId, workflowRun }: ReviewsContextModel = useReviewsContext() const [selectedTab, setSelectedTab] = useState('scorecard') const { runItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(workflowId, workflowRun?.id) + const { totalCount }: AiWorkflowRunAttachmentsResponse + = useFetchAiWorkflowsRunAttachments(workflowId, workflowRun?.id) + + const tabItems: SelectOption[] = [ + { label: 'Scorecard', value: 'scorecard' }, + { label: `Attachments (${totalCount ?? 0})`, value: 'attachments' }, + ] const isFailedRun = useMemo(() => ( workflowRun && [ AiWorkflowRunStatusEnum.CANCELLED, From 072f6fb055378049e0ee0694314ef1168595fd76 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Nov 2025 13:00:08 +0200 Subject: [PATCH 071/125] Fixes to ai scorcard viewer --- .../AiReviewsTable/AiReviewsTable.tsx | 9 ++- .../ReviewComment/ReviewComment.tsx | 18 +++-- .../ReviewComments/ReviewComments.tsx | 2 +- .../ScorecardViewer/ScorecardViewer.tsx | 78 +++++++++++++------ .../SubmissionBarInfo.module.scss | 54 ++++++------- .../SubmissionBarInfo/SubmissionBarInfo.tsx | 59 +++++++------- .../TableCheckpointSubmissions.tsx | 6 +- .../TableIterativeReview.tsx | 6 +- .../TableSubmissionScreening.tsx | 4 +- src/apps/review/src/lib/hooks/index.ts | 1 + .../src/lib/hooks/useFetchSubmissionInfo.ts | 34 ++++++++ .../lib/hooks/useFetchSubmissionReviews.ts | 26 +------ .../src/lib/models/ReviewsContext.model.ts | 6 ++ .../ScorecardDetailsPage.tsx | 4 +- .../ReviewsContext/ReviewsContextProvider.tsx | 15 +++- .../ReviewsViewer/ReviewsViewer.module.scss | 7 ++ .../reviews/ReviewsViewer/ReviewsViewer.tsx | 16 +++- .../AiReviewViewer/AiReviewViewer.tsx | 3 +- .../components/ReviewViewer/ReviewViewer.tsx | 32 +++++++- .../ReviewsSidebar/ReviewsSidebar.tsx | 20 ++++- 20 files changed, 264 insertions(+), 136 deletions(-) create mode 100644 src/apps/review/src/lib/hooks/useFetchSubmissionInfo.ts diff --git a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx index 865137aed..c0605200e 100644 --- a/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx +++ b/src/apps/review/src/lib/components/AiReviewsTable/AiReviewsTable.tsx @@ -1,4 +1,5 @@ import { FC, MouseEvent as ReactMouseEvent, useMemo } from 'react' +import { Link } from 'react-router-dom' import moment from 'moment' import { useWindowSize, WindowSize } from '~/libs/shared' @@ -90,7 +91,7 @@ const AiReviewsTable: FC = props => {
    {run.status === 'SUCCESS' ? ( run.workflow.scorecard ? ( - {run.score} + {run.score} ) : run.score ) : '-'}
    @@ -151,11 +152,11 @@ const AiReviewsTable: FC = props => {
    diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx index 1d8c717c1..9459d842f 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComment/ReviewComment.tsx @@ -98,14 +98,16 @@ const ReviewComment: FC = props => { return (
    - -
    - {props.comment.content} -
    -
    + {props.comment.content && ( + +
    + {props.comment.content} +
    +
    + )} {isSubmitter && canAddAppeal && (!props.appeal && !showAppealForm && ( diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx index a26237f0a..d18bdda56 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/ReviewResponse/ReviewComments/ReviewComments.tsx @@ -12,7 +12,7 @@ interface ReviewCommentsProps { const ReviewComments: FC = props => { const comments = useMemo(() => ( - (props.reviewItem.reviewItemComments || []).filter(c => c.content || c.appeal || props.mappingAppeals?.[c.id]) + (props.reviewItem.reviewItemComments || []) ).sort((a, b) => a.sortOrder - b.sortOrder), [props.reviewItem.reviewItemComments, props.mappingAppeals]) return ( diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index 2e685c3ae..673b2b8e4 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -1,6 +1,7 @@ /* eslint-disable complexity */ -import { FC, useCallback, useEffect, useMemo, useState } from 'react' +import { FC, ReactNode, useCallback, useEffect, useMemo, useState } from 'react' +import { Link, NavLink } from 'react-router-dom' import { isEmpty } from 'lodash' import classNames from 'classnames' @@ -44,6 +45,7 @@ interface ScorecardViewerProps { isSavingAppealResponse?: boolean isSavingManagerComment?: boolean setReviewStatus?: (status: ReviewCtxStatus) => void + setActionButtons?: (buttons?: ReactNode) => void saveReviewInfo?: ( updatedReview: FormReviews | undefined, fullReview: FormReviews | undefined, @@ -74,6 +76,7 @@ interface ScorecardViewerProps { success: () => void, ) => void onCancelEdit?: () => void + navigateBack?: (e?: React.MouseEvent) => void isLoading?: boolean setIsChanged?: (changed: boolean) => void } @@ -112,9 +115,7 @@ const ScorecardViewerContent: FC = props => { form?.getValues(), true, totalScore, - (): void => { - // Success callback - could navigate or show success message - }, + () => props.navigateBack?.(), ) } }, [ @@ -167,6 +168,44 @@ const ScorecardViewerContent: FC = props => { } }, [totalScore, props.scorecard]) + const actionButtons = useMemo(() => ( +
    + + +
    + ), [props.isEdit, handleSaveAsDraft, touchedAllFields, props.isSavingReview]) + + useEffect(() => { + props.setActionButtons?.(props.isEdit ? actionButtons : ( + <> + + Back to Challenge + + + )) + }, [actionButtons, props.setActionButtons]) + if (props.isLoading) { return } @@ -234,7 +273,7 @@ const ScorecardViewerContent: FC = props => { - {props.isEdit && ( + {props.isEdit ? (
    {errorMessage && ( @@ -259,27 +298,22 @@ const ScorecardViewerContent: FC = props => { Cancel )} - -
    + ) : (props.navigateBack && ( +
    +
    + - Mark as Complete - + Back to Challenge +
    - )} + ))} = (props: Props) => { type: 'link', value: submissionIdValue, }, - { - icon: 'icon-handle', - title: 'My Role', - value: ( -
    - {myChallengeRoles.map(item => ( - {item} - ))} -
    - ), - }, + // { + // icon: 'icon-handle', + // title: 'My Role', + // value: ( + //
    + // {myChallengeRoles.map(item => ( + // {item} + // ))} + //
    + // ), + // }, { href: getHandleUrl(useInfo), icon: 'icon-handle', @@ -69,24 +69,27 @@ export const SubmissionBarInfo: FC = (props: Props) => {
    {uiItems.map(item => (
    - - + + + + + + {item.title} + : + -
    - {item.title} - {item.type === 'link' ? ( - - {item.value} - - ) : ( - {item.value} - )} -
    + {item.type === 'link' ? ( + + {item.value} + + ) : ( + {item.value} + )}
    ))}
    diff --git a/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.tsx b/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.tsx index 36cf7a64c..8a1495866 100644 --- a/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.tsx +++ b/src/apps/review/src/lib/components/TableCheckpointSubmissions/TableCheckpointSubmissions.tsx @@ -362,7 +362,7 @@ export const TableCheckpointSubmissions: FC = (props: Props) => { return ( = (props: Props) => { key: `complete-${data.myReviewId}`, render: isLast => ( @@ -524,7 +524,7 @@ export const TableCheckpointSubmissions: FC = (props: Props) => { return ( = (props: Props) => { return ( @@ -1165,7 +1165,7 @@ export const TableIterativeReview: FC = (props: Props) => { return ( diff --git a/src/apps/review/src/lib/components/TableSubmissionScreening/TableSubmissionScreening.tsx b/src/apps/review/src/lib/components/TableSubmissionScreening/TableSubmissionScreening.tsx index e98d57738..ce875acee 100644 --- a/src/apps/review/src/lib/components/TableSubmissionScreening/TableSubmissionScreening.tsx +++ b/src/apps/review/src/lib/components/TableSubmissionScreening/TableSubmissionScreening.tsx @@ -301,7 +301,7 @@ const createMyReviewActions = ( key: `complete-${data.myReviewId}`, render: isLast => ( {scoreValue} diff --git a/src/apps/review/src/lib/hooks/index.ts b/src/apps/review/src/lib/hooks/index.ts index 5d2b5455f..f984003c8 100644 --- a/src/apps/review/src/lib/hooks/index.ts +++ b/src/apps/review/src/lib/hooks/index.ts @@ -19,3 +19,4 @@ export * from './useSubmissionDownloadAccess' export * from './useSubmissionHistory' export * from './useScorecardPassingScores' export * from './useFetchAiWorkflowRuns' +export * from './useFetchSubmissionInfo' diff --git a/src/apps/review/src/lib/hooks/useFetchSubmissionInfo.ts b/src/apps/review/src/lib/hooks/useFetchSubmissionInfo.ts new file mode 100644 index 000000000..05ab20e82 --- /dev/null +++ b/src/apps/review/src/lib/hooks/useFetchSubmissionInfo.ts @@ -0,0 +1,34 @@ +import { useEffect } from 'react' +import useSWR, { SWRResponse } from 'swr' + +import { handleError } from '~/libs/shared' + +import { BackendSubmission } from '../models' +import { fetchSubmission } from '../services' + +export const useFetchSubmissionInfo = (submissionId?: string): [ + BackendSubmission | undefined, + boolean +] => { + // Use swr hooks for submission info fetching + const { + data: submissionInfo, + error: fetchSubmissionError, + isValidating: isLoadingSubmission, + }: SWRResponse = useSWR( + `/submissions/${submissionId}`, + { + fetcher: () => fetchSubmission(submissionId as string), + isPaused: () => !submissionId, + }, + ) + + // Show backend error when fetching submission info + useEffect(() => { + if (fetchSubmissionError) { + handleError(fetchSubmissionError) + } + }, [fetchSubmissionError]) + + return [submissionInfo, isLoadingSubmission] +} diff --git a/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts b/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts index ba7e384b3..2420c85a8 100644 --- a/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts +++ b/src/apps/review/src/lib/hooks/useFetchSubmissionReviews.ts @@ -26,7 +26,6 @@ import { BackendRequestReviewItem, BackendReview, BackendReviewItem, - BackendSubmission, ChallengeDetailContextModel, convertBackendAppeal, convertBackendReviewToReviewInfo, @@ -46,7 +45,6 @@ import { fetchChallengeReviews, fetchReview, fetchScorecard, - fetchSubmission, updateAppeal, updateAppealResponse, updateReview, @@ -293,7 +291,6 @@ const resolveReviewOrThrow = ( export interface useFetchSubmissionReviewsProps { mappingAppeals: MappingAppeal scorecardInfo?: ScorecardInfo - submissionInfo?: BackendSubmission scorecardId: string isLoading: boolean reviewInfo?: ReviewInfo @@ -521,19 +518,6 @@ export function useFetchSubmissionReviews(reviewId: string = ''): useFetchSubmis }, ) - // Use swr hooks for submission info fetching - const { - data: submissionInfo, - error: fetchSubmissionError, - isValidating: isLoadingSubmission, - }: SWRResponse = useSWR( - `/submissions/${submissionId}`, - { - fetcher: () => fetchSubmission(submissionId), - isPaused: () => !submissionId, - }, - ) - /** * Get review info from backend and scorecard info */ @@ -593,13 +577,6 @@ export function useFetchSubmissionReviews(reviewId: string = ''): useFetchSubmis } }, [fetchScorecardError]) - // Show backend error when fetching submission info - useEffect(() => { - if (fetchSubmissionError) { - handleError(fetchSubmissionError) - } - }, [fetchSubmissionError]) - // Show backend error when fetching appeal info useEffect(() => { if (fetchAppealsError) { @@ -950,7 +927,7 @@ export function useFetchSubmissionReviews(reviewId: string = ''): useFetchSubmis addManagerComment, doDeleteAppeal, isLoading: - isLoadingReview || isLoadingScorecard || isLoadingSubmission, + isLoadingReview || isLoadingScorecard, isSavingAppeal, isSavingAppealResponse, isSavingManagerComment, @@ -962,7 +939,6 @@ export function useFetchSubmissionReviews(reviewId: string = ''): useFetchSubmis scorecardId, scorecardInfo, setReviewInfo: setUpdatedReviewInfo, - submissionInfo, submitterLockedPhaseName: submitterPhaseGate.phaseName, } } diff --git a/src/apps/review/src/lib/models/ReviewsContext.model.ts b/src/apps/review/src/lib/models/ReviewsContext.model.ts index fa113af25..c7e887a99 100644 --- a/src/apps/review/src/lib/models/ReviewsContext.model.ts +++ b/src/apps/review/src/lib/models/ReviewsContext.model.ts @@ -1,7 +1,10 @@ +import { ReactNode } from 'react' + import { AiWorkflow, AiWorkflowRun } from '../hooks' import { Scorecard } from './Scorecard.model' import { ChallengeDetailContextModel } from './ChallengeDetailContextModel.model' +import { BackendSubmission } from './BackendSubmission.model' export interface ReviewCtxStatus { status: 'passed' | 'pending' | 'failed-score'; @@ -19,4 +22,7 @@ export interface ReviewsContextModel extends ChallengeDetailContextModel { workflowRuns: AiWorkflowRun[] reviewStatus?: ReviewCtxStatus setReviewStatus: (status: ReviewCtxStatus) => void + actionButtons?: ReactNode + setActionButtons: (btns?: ReactNode) => void + submissionInfo?: BackendSubmission } diff --git a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx index fcf4658fb..ee77f50c5 100644 --- a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx @@ -16,6 +16,7 @@ import { TableLoading } from '~/apps/admin/src/lib' import { useAppNavigate, + useFetchSubmissionInfo, useFetchSubmissionReviews, useFetchSubmissionReviewsProps, useRole, @@ -283,10 +284,11 @@ export const ScorecardDetailsPage: FC = (props: Props) => { submitterLockedPhaseName, reviewInfo, scorecardInfo, - submissionInfo, saveReviewInfo, }: useFetchSubmissionReviewsProps = useFetchSubmissionReviews(reviewId) + const [submissionInfo] = useFetchSubmissionInfo(reviewInfo?.submissionId) + const isReviewCompleted = useMemo( () => { const statusUpper = (reviewInfo?.status || '') diff --git a/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx b/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx index a44d76aca..bada3213a 100644 --- a/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx +++ b/src/apps/review/src/pages/reviews/ReviewsContext/ReviewsContextProvider.tsx @@ -1,12 +1,12 @@ /** * Context provider for challenge detail page */ -import { Context, createContext, FC, PropsWithChildren, useContext, useMemo, useState } from 'react' +import { Context, createContext, FC, PropsWithChildren, ReactNode, useContext, useMemo, useState } from 'react' import { useParams, useSearchParams } from 'react-router-dom' import { ChallengeDetailContext } from '../../../lib' import { ChallengeDetailContextModel, ReviewCtxStatus, ReviewsContextModel } from '../../../lib/models' -import { AiWorkflowRunsResponse, useFetchAiWorkflowsRuns } from '../../../lib/hooks' +import { AiWorkflowRunsResponse, useFetchAiWorkflowsRuns, useFetchSubmissionInfo } from '../../../lib/hooks' export const ReviewsContext: Context = createContext({} as ReviewsContextModel) @@ -22,9 +22,13 @@ export const ReviewsContextProvider: FC = props => { const reviewId = searchParams.get('reviewId') ?? '' const [reviewStatus, setReviewStatus] = useState({} as ReviewCtxStatus) + const [actionButtons, setActionButtons] = useState() const challengeDetailsCtx = useContext(ChallengeDetailContext) const { challengeInfo }: ChallengeDetailContextModel = challengeDetailsCtx + + const [submissionInfo] = useFetchSubmissionInfo(submissionId) + const aiReviewers = useMemo(() => ( (challengeInfo?.reviewers ?? []).filter(r => !!r.aiWorkflowId) ), [challengeInfo?.reviewers]) @@ -49,28 +53,33 @@ export const ReviewsContextProvider: FC = props => { const value = useMemo( () => ({ ...challengeDetailsCtx, + actionButtons, isLoading: isLoadingCtxData, reviewId, reviewStatus, scorecard, + setActionButtons, setReviewStatus, submissionId, + submissionInfo, workflow, workflowId, workflowRun, workflowRuns, }), [ + actionButtons, challengeDetailsCtx, isLoadingCtxData, reviewId, + reviewStatus, scorecard, submissionId, + submissionInfo, workflow, workflowId, workflowRun, workflowRuns, - reviewStatus, ], ) diff --git a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.module.scss b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.module.scss index 1b3b36e66..410332adc 100644 --- a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.module.scss +++ b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.module.scss @@ -27,6 +27,13 @@ } } +.subHeader { + margin-bottom: 80px; + @include ltelg { + margin-bottom: 40px; + } +} + .contentWrap { width: 100%; } diff --git a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx index f5729195b..6bb3ae350 100644 --- a/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx +++ b/src/apps/review/src/pages/reviews/ReviewsViewer/ReviewsViewer.tsx @@ -1,6 +1,8 @@ import { FC, useMemo } from 'react' import { useLocation } from 'react-router-dom' +import { EnvironmentConfig } from '~/config' + import { PageWrapper } from '../../../lib' import { BreadCrumbData, ReviewsContextModel } from '../../../lib/models' import { ReviewsSidebar } from '../components/ReviewsSidebar' @@ -8,11 +10,18 @@ import { useReviewsContext } from '../ReviewsContext' import { AiReviewViewer } from '../components/AiReviewViewer' import { activeReviewAssignmentsRouteId, rootRoute } from '../../../config/routes.config' import { ReviewViewer } from '../components/ReviewViewer' +import { SubmissionBarInfo } from '../../../lib/components/SubmissionBarInfo' import styles from './ReviewsViewer.module.scss' const ReviewsViewer: FC = () => { - const { challengeInfo, submissionId, workflowRun }: ReviewsContextModel = useReviewsContext() + const { + challengeInfo, + submissionId, + workflowRun, + actionButtons, + submissionInfo, + }: ReviewsContextModel = useReviewsContext() const location = useLocation() const containsPastChallenges = location.pathname.indexOf('/past-challenges/') @@ -42,7 +51,12 @@ const ReviewsViewer: FC = () => { pageTitle={challengeInfo?.name ?? ''} className={styles.container} breadCrumb={breadCrumb} + titleUrl={`${EnvironmentConfig.REVIEW.CHALLENGE_PAGE_URL}/${challengeInfo?.id}`} + rightHeader={actionButtons} > +
    + +
    diff --git a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx index ca1f14802..65cc9ce69 100644 --- a/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/AiReviewViewer/AiReviewViewer.tsx @@ -21,7 +21,7 @@ const tabItems: SelectOption[] = [ ] const AiReviewViewer: FC = () => { - const { scorecard, workflowId, workflowRun }: ReviewsContextModel = useReviewsContext() + const { scorecard, workflowId, workflowRun, setActionButtons }: ReviewsContextModel = useReviewsContext() const [selectedTab, setSelectedTab] = useState('scorecard') const { runItems }: AiWorkflowRunItemsResponse = useFetchAiWorkflowsRunItems(workflowId, workflowRun?.id) const isFailedRun = useMemo(() => ( @@ -52,6 +52,7 @@ const AiReviewViewer: FC = () => { )} diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx index 9fafde4f0..5d52584cd 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx @@ -1,6 +1,6 @@ +import { mutate } from 'swr' import { FC, useCallback, useEffect, useMemo, useState } from 'react' -import { SubmissionBarInfo } from '~/apps/review/src/lib/components/SubmissionBarInfo' import { ChallengeLinksForAdmin } from '~/apps/review/src/lib/components/ChallengeLinksForAdmin' import { ScorecardViewer } from '~/apps/review/src/lib/components/Scorecard' import { @@ -13,6 +13,7 @@ import { import { ChallengeDetailContextModel, ReviewsContextModel } from '~/apps/review/src/lib/models' import { ChallengeLinks, ConfirmModal, useChallengeDetailsContext } from '~/apps/review/src/lib' import { useIsEditReview, useIsEditReviewProps } from '~/apps/review/src/lib/hooks/useIsEditReview' +import { rootRoute } from '~/apps/review/src/config/routes.config' import { ADMIN, COPILOT, MANAGER } from '../../../../config/index.config' import { useReviewsContext } from '../../ReviewsContext' @@ -21,7 +22,7 @@ import styles from './ReviewViewer.module.scss' const ReviewViewer: FC = () => { const navigate = useAppNavigate() - const { reviewId, setReviewStatus }: ReviewsContextModel = useReviewsContext() + const { reviewId, setReviewStatus, setActionButtons }: ReviewsContextModel = useReviewsContext() const { actionChallengeRole, @@ -52,7 +53,6 @@ const ReviewViewer: FC = () => { submitterLockedPhaseName, reviewInfo, scorecardInfo, - submissionInfo, saveReviewInfo, }: useFetchSubmissionReviewsProps = useFetchSubmissionReviews(reviewId) @@ -110,6 +110,28 @@ const ReviewViewer: FC = () => { } }, [isChanged, isEdit, navigate]) + const back = useCallback(async (e?: React.MouseEvent) => { + e?.preventDefault() + try { + if (challengeInfo?.id) { + // Ensure the challenge details reflect the latest data (e.g., active phase) + await mutate(`challengeBaseUrl/challenges/${challengeInfo?.id}`) + } + } catch { + // no-op: navigation should still occur even if revalidation fails + } + + const pastPrefix = '/past-challenges/' + // eslint-disable-next-line no-restricted-globals + const idx = location.pathname.indexOf(pastPrefix) + const url = idx > -1 + ? `${rootRoute}/past-challenges/${challengeInfo?.id}/challenge-details` + : `${rootRoute}/active-challenges/${challengeInfo?.id}/challenge-details` + navigate(url, { + fallback: './../../../../challenge-details', + }) + }, [challengeInfo?.id, mutate, navigate]) + const hasChallengeAdminRole = useMemo( () => myChallengeResources.some( resource => resource.roleName?.toLowerCase() === ADMIN.toLowerCase(), @@ -181,7 +203,7 @@ const ReviewViewer: FC = () => {
    - + {/* */} {actionChallengeRole === ADMIN || actionChallengeRole === COPILOT || actionChallengeRole === MANAGER @@ -219,6 +241,7 @@ const ReviewViewer: FC = () => { mappingAppeals={mappingAppeals} isEdit={isEdit} onCancelEdit={onCancelEdit} + navigateBack={back} setIsChanged={setIsChanged} isLoading={isLoading} isManagerEdit={isManagerEdit} @@ -232,6 +255,7 @@ const ReviewViewer: FC = () => { doDeleteAppeal={doDeleteAppeal} addManagerComment={addManagerComment} setReviewStatus={setReviewStatus} + setActionButtons={setActionButtons} /> )}
    diff --git a/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx index 12a9a5f25..640ee9753 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewsSidebar/ReviewsSidebar.tsx @@ -39,14 +39,26 @@ const ReviewsSidebar: FC = props => { return (
    - {workflow && workflowRun && ( + {((workflow && workflowRun) || reviewId) && (
    - - {workflow.name} + {reviewId ? : } + + {(workflow && workflowRun) ? workflow.name : 'Review'} + - + + {reviewStatus ? ( + + ) : ( + + )}
    From e8770f871fdd412efac173f621db05a09079811d Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Nov 2025 16:59:48 +0200 Subject: [PATCH 072/125] Scorecard review header --- .../ScorecardViewer/ScorecardViewer.tsx | 4 +- .../src/lib/models/ReviewsContext.model.ts | 1 + .../ReviewScorecardHeader.module.scss | 157 ++++++++++++++++++ .../ReviewViewer/ReviewScorecardHeader.tsx | 81 +++++++++ .../ReviewViewer/ReviewViewer.module.scss | 4 + .../components/ReviewViewer/ReviewViewer.tsx | 13 +- 6 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.module.scss create mode 100644 src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index 673b2b8e4..53180f168 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -96,6 +96,7 @@ const ScorecardViewerContent: FC = props => { const { form, totalScore, + reviewProgress, isTouched, touchedAllFields, formErrors, @@ -164,9 +165,10 @@ const ScorecardViewerContent: FC = props => { props.setReviewStatus({ score, status, + progress: reviewProgress, }) } - }, [totalScore, props.scorecard]) + }, [totalScore, reviewProgress, props.scorecard]) const actionButtons = useMemo(() => (
    diff --git a/src/apps/review/src/lib/models/ReviewsContext.model.ts b/src/apps/review/src/lib/models/ReviewsContext.model.ts index c7e887a99..b75928dd3 100644 --- a/src/apps/review/src/lib/models/ReviewsContext.model.ts +++ b/src/apps/review/src/lib/models/ReviewsContext.model.ts @@ -9,6 +9,7 @@ import { BackendSubmission } from './BackendSubmission.model' export interface ReviewCtxStatus { status: 'passed' | 'pending' | 'failed-score'; score: number; + progress: number; } export interface ReviewsContextModel extends ChallengeDetailContextModel { diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.module.scss b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.module.scss new file mode 100644 index 000000000..8afeaa3e7 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.module.scss @@ -0,0 +1,157 @@ +@import '@libs/ui/styles/includes'; + +.wrap { + width: 100%; + margin-bottom: $sp-6; +} + +.title { + font-family: "Figtree", sans-serif; + font-size: 26px; + font-weight: 700; + line-height: 30px; + color: #0A0A0A; +} + +.content { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: $sp-6; + + @include ltemd { + flex-direction: column; + gap: $sp-6; + } +} + +.leftSection { + display: flex; + align-items: flex-start; + gap: $sp-4; + flex: 1; +} + +.iconWrapper { + flex: 0 0 auto; +} + +.scorecardIcon { + width: 60px; + height: 60px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #A8A8A8; +} + +.aiReviewIcon { + width: 40px; + height: 40px; +} + +.infoSection { + display: flex; + flex-direction: column; + gap: $sp-3; + flex: 1; + margin-top: $sp-2; +} + +.infoRow { + display: flex; + align-items: center; + gap: $sp-2; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 19px; + font-weight: bold; + color: #0A0A0A; +} + +.personIcon { + display: flex; + align-items: center; + flex: 0 0 auto; + width: 20px; + height: 20px; + padding: 2px; + + > * { + width: 16px; + height: 16px; + } +} + +.whaleIcon { + display: flex; + align-items: center; + flex: 0 0 auto; + width: 16px; + height: 16px; + + svg { + width: 16px; + height: 16px; + } +} + +.rightSection { + display: flex; + flex-direction: column; + gap: $sp-1; + flex: 0 0 auto; + min-width: 200px; + + @include ltemd { + width: 100%; + min-width: auto; + } +} + +.scoreInfo { + display: flex; + align-items: center; + gap: $sp-2; + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + line-height: 19px; + font-weight: bold; + color: #0A0A0A; +} + +.trophyIcon { + flex: 0 0 auto; + width: 16px; + height: 16px; + + path { + fill: #0A0A0A; + } +} + +.scoreValue { + font-weight: 400; +} + +.progressSection { + display: flex; + align-items: center; + gap: $sp-2; + width: 100%; + + :global(.container) { + flex: 1; + } +} + +.progressText { + font-family: "Nunito Sans", sans-serif; + font-size: 14px; + font-weight: 400; + color: #0A0A0A; + white-space: nowrap; + flex: 0 0 auto; +} + diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx new file mode 100644 index 000000000..9da0cc311 --- /dev/null +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx @@ -0,0 +1,81 @@ +import { FC } from 'react' + +import { ProgressBar } from '~/apps/review/src/lib/components/ProgressBar' +import { IconPremium, IconDeepseekAi, IconAiReview, IconPhaseReview } from '~/apps/review/src/lib/assets/icons' +import { ReviewInfo, ScorecardInfo } from '~/apps/review/src/lib/models' +import { AiWorkflow } from '~/apps/review/src/lib/hooks' + +import styles from './ReviewScorecardHeader.module.scss' + +interface Props { + reviewInfo?: ReviewInfo + scorecardInfo?: ScorecardInfo + workflow?: AiWorkflow + reviewProgress?: number +} + +export const ReviewScorecardHeader: FC = (props: Props) => { + const { reviewInfo, scorecardInfo, workflow, reviewProgress = 0 } = props + + const reviewerHandle = reviewInfo?.reviewerHandle + const reviewerColor = reviewInfo?.reviewerHandleColor + const llmModelName = workflow?.llm?.name || 'N/A' + const minimumPassingScore = scorecardInfo?.minimumPassingScore ?? 0 + + return ( +
    +
    +
    +
    +
    + +
    +
    +
    +

    Edit Review Scorecard

    +
    + {reviewerHandle && ( +
    +
    + +
    + Reviewer: + + {reviewerHandle} + +
    + )} + {workflow?.llm && ( +
    +
    + +
    + LLM/AI Model: + + {llmModelName} + +
    + )} +
    +
    +
    +
    +
    + + Minimum passing score: + + {minimumPassingScore.toFixed(2)} + +
    +
    + + {reviewProgress}% +
    +
    +
    +
    + ) +} + +export default ReviewScorecardHeader + diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.module.scss b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.module.scss index ed9f89e8f..7f63c4ca2 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.module.scss +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.module.scss @@ -33,6 +33,10 @@ .summary { margin-bottom: $sp-6; + > * { + padding-top: 0; + justify-content: flex-end; + } } .lockedNotice { diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx index 5d52584cd..2c602833b 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx @@ -18,11 +18,12 @@ import { rootRoute } from '~/apps/review/src/config/routes.config' import { ADMIN, COPILOT, MANAGER } from '../../../../config/index.config' import { useReviewsContext } from '../../ReviewsContext' +import { ReviewScorecardHeader } from './ReviewScorecardHeader' import styles from './ReviewViewer.module.scss' const ReviewViewer: FC = () => { const navigate = useAppNavigate() - const { reviewId, setReviewStatus, setActionButtons }: ReviewsContextModel = useReviewsContext() + const { reviewId, setReviewStatus, setActionButtons, workflow, reviewStatus }: ReviewsContextModel = useReviewsContext() const { actionChallengeRole, @@ -234,7 +235,14 @@ const ReviewViewer: FC = () => {
    )} {!isSubmitterPhaseLocked && ( - + + { setReviewStatus={setReviewStatus} setActionButtons={setActionButtons} /> + )}
    {isEdit && ( From bd604424580c4154b736342de66d6493837ac0cb Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Nov 2025 17:10:43 +0200 Subject: [PATCH 073/125] lint fixes --- .../ScorecardViewer/ScorecardViewer.tsx | 2 +- .../ReviewViewer/ReviewScorecardHeader.tsx | 24 ++++----- .../components/ReviewViewer/ReviewViewer.tsx | 52 +++++++++++-------- 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx index 53180f168..cdba2d39c 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardViewer.tsx @@ -163,9 +163,9 @@ const ScorecardViewerContent: FC = props => { } props.setReviewStatus({ + progress: reviewProgress, score, status, - progress: reviewProgress, }) } }, [totalScore, reviewProgress, props.scorecard]) diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx index 9da0cc311..b45f8a7cc 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx @@ -1,7 +1,7 @@ import { FC } from 'react' import { ProgressBar } from '~/apps/review/src/lib/components/ProgressBar' -import { IconPremium, IconDeepseekAi, IconAiReview, IconPhaseReview } from '~/apps/review/src/lib/assets/icons' +import { IconDeepseekAi, IconPhaseReview, IconPremium } from '~/apps/review/src/lib/assets/icons' import { ReviewInfo, ScorecardInfo } from '~/apps/review/src/lib/models' import { AiWorkflow } from '~/apps/review/src/lib/hooks' @@ -15,12 +15,10 @@ interface Props { } export const ReviewScorecardHeader: FC = (props: Props) => { - const { reviewInfo, scorecardInfo, workflow, reviewProgress = 0 } = props - - const reviewerHandle = reviewInfo?.reviewerHandle - const reviewerColor = reviewInfo?.reviewerHandleColor - const llmModelName = workflow?.llm?.name || 'N/A' - const minimumPassingScore = scorecardInfo?.minimumPassingScore ?? 0 + const reviewerHandle = props.reviewInfo?.reviewerHandle + const reviewerColor = props.reviewInfo?.reviewerHandleColor + const llmModelName = props.workflow?.llm?.name || 'N/A' + const minimumPassingScore = props.scorecardInfo?.minimumPassingScore ?? 0 return (
    @@ -37,7 +35,7 @@ export const ReviewScorecardHeader: FC = (props: Props) => { {reviewerHandle && (
    - +
    Reviewer: @@ -45,7 +43,7 @@ export const ReviewScorecardHeader: FC = (props: Props) => {
    )} - {workflow?.llm && ( + {props.workflow?.llm && (
    @@ -68,8 +66,11 @@ export const ReviewScorecardHeader: FC = (props: Props) => {
    - - {reviewProgress}% + + + {props.reviewProgress} + % +
    @@ -78,4 +79,3 @@ export const ReviewScorecardHeader: FC = (props: Props) => { } export default ReviewScorecardHeader - diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx index 2c602833b..0b6009b6c 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx @@ -23,7 +23,13 @@ import styles from './ReviewViewer.module.scss' const ReviewViewer: FC = () => { const navigate = useAppNavigate() - const { reviewId, setReviewStatus, setActionButtons, workflow, reviewStatus }: ReviewsContextModel = useReviewsContext() + const { + reviewId, + setReviewStatus, + setActionButtons, + workflow, + reviewStatus, + }: ReviewsContextModel = useReviewsContext() const { actionChallengeRole, @@ -243,28 +249,28 @@ const ReviewViewer: FC = () => { reviewProgress={reviewStatus?.progress ?? reviewInfo?.reviewProgress ?? 0} /> + actionChallengeRole={actionChallengeRole} + scorecard={scorecardInfo as any} + reviewInfo={reviewInfo} + mappingAppeals={mappingAppeals} + isEdit={isEdit} + onCancelEdit={onCancelEdit} + navigateBack={back} + setIsChanged={setIsChanged} + isLoading={isLoading} + isManagerEdit={isManagerEdit} + isSavingReview={isSavingReview} + isSavingAppeal={isSavingAppeal} + isSavingAppealResponse={isSavingAppealResponse} + isSavingManagerComment={isSavingManagerComment} + saveReviewInfo={saveReviewInfo} + addAppeal={addAppeal} + addAppealResponse={addAppealResponse} + doDeleteAppeal={doDeleteAppeal} + addManagerComment={addManagerComment} + setReviewStatus={setReviewStatus} + setActionButtons={setActionButtons} + /> )}
    From 0f87d349fa59445b02c76eb0d96eaf8c5ebdcef4 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 18 Nov 2025 17:28:00 +0200 Subject: [PATCH 074/125] Correctly fetch reviewer resource for scorecard --- .../ReviewViewer/ReviewScorecardHeader.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx index b45f8a7cc..3998a967a 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewScorecardHeader.tsx @@ -1,8 +1,9 @@ -import { FC } from 'react' +import { FC, useMemo } from 'react' +import { useChallengeDetailsContext } from '~/apps/review/src/lib' import { ProgressBar } from '~/apps/review/src/lib/components/ProgressBar' import { IconDeepseekAi, IconPhaseReview, IconPremium } from '~/apps/review/src/lib/assets/icons' -import { ReviewInfo, ScorecardInfo } from '~/apps/review/src/lib/models' +import { ChallengeDetailContextModel, ReviewInfo, ScorecardInfo } from '~/apps/review/src/lib/models' import { AiWorkflow } from '~/apps/review/src/lib/hooks' import styles from './ReviewScorecardHeader.module.scss' @@ -15,8 +16,20 @@ interface Props { } export const ReviewScorecardHeader: FC = (props: Props) => { - const reviewerHandle = props.reviewInfo?.reviewerHandle - const reviewerColor = props.reviewInfo?.reviewerHandleColor + const { + resources, + }: ChallengeDetailContextModel = useChallengeDetailsContext() + + const reviewer = useMemo(() => { + if (!props.reviewInfo?.resourceId) { + return undefined + } + + return resources.find(r => r.id === props.reviewInfo?.resourceId) + }, [props.reviewInfo?.resourceId, resources]) + + const reviewerHandle = reviewer?.memberHandle + const reviewerColor = reviewer?.handleColor const llmModelName = props.workflow?.llm?.name || 'N/A' const minimumPassingScore = props.scorecardInfo?.minimumPassingScore ?? 0 From a8fe48ed126e07f5a02b1a97080b0154fc08e1b7 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Tue, 18 Nov 2025 22:29:24 +0530 Subject: [PATCH 075/125] Add input/output tokens for ai workflow runs --- .../src/lib/hooks/useFetchAiWorkflowRuns.ts | 4 ++++ .../ScorecardHeader.module.scss | 12 ++++++++-- .../ScorecardHeader/ScorecardHeader.tsx | 23 ++++++++++++++++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts index c7afa253d..e8a8ba1a0 100644 --- a/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts +++ b/src/apps/review/src/lib/hooks/useFetchAiWorkflowRuns.ts @@ -44,6 +44,10 @@ export interface AiWorkflowRun { gitRunUrl?: string; score: number; workflow: AiWorkflow + usage: { + input: number + output: number + } } export interface AiWorkflowRunArtifact { diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss index c4d1af9a7..e5ffaaa66 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.module.scss @@ -101,8 +101,16 @@ &.sm { height: 16px; } - path { - fill: #0A0A0A; + &.md { + height: 20px; + stroke: #0a0a0a; + } + + &.iconFile { + path { + fill: #0A0A0A; + } + height: 16px; } } diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx index ac84ecb63..2d767b563 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx @@ -3,6 +3,7 @@ import moment, { Duration } from 'moment' import { ReviewsContextModel } from '~/apps/review/src/lib/models' import { useRolePermissions, UseRolePermissionsResult } from '~/apps/review/src/lib/hooks' +import { ArrowCircleDownIcon, ArrowCircleUpIcon } from '@heroicons/react/outline' import { IconClock, IconFile, IconPremium } from '../../../../lib/assets/icons' import { AiModelModal } from '../AiModelModal' @@ -68,7 +69,7 @@ const ScorecardHeader: FC = () => { {isAdmin && ( - + Git log: {' '} @@ -81,6 +82,26 @@ const ScorecardHeader: FC = () => { )} + {isAdmin && ( + + + + Input Tokens: + {' '} + {workflowRun.usage.input} + + + )} + {isAdmin && ( + + + + Output Tokens: + {' '} + {workflowRun.usage.output} + + + )}

    From e7326d7b1b5d2790d0146365b94cdde3196f85d7 Mon Sep 17 00:00:00 2001 From: "vasilica.olariu" Date: Wed, 19 Nov 2025 13:36:31 +0200 Subject: [PATCH 076/125] Handle null usage for workflow runs --- .../reviews/components/ScorecardHeader/ScorecardHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx index 2d767b563..3e30609c3 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx @@ -88,7 +88,7 @@ const ScorecardHeader: FC = () => { Input Tokens: {' '} - {workflowRun.usage.input} + {workflowRun.usage?.input} )} @@ -98,7 +98,7 @@ const ScorecardHeader: FC = () => { Output Tokens: {' '} - {workflowRun.usage.output} + {workflowRun.usage?.output} )} From 19770cf008814556ec0fefa297edaea95222adcc Mon Sep 17 00:00:00 2001 From: "vasilica.olariu" Date: Wed, 19 Nov 2025 13:37:44 +0200 Subject: [PATCH 077/125] Handle null usage for workflow runs. add placeholder values --- .../reviews/components/ScorecardHeader/ScorecardHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx index 3e30609c3..6f0f02700 100644 --- a/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx +++ b/src/apps/review/src/pages/reviews/components/ScorecardHeader/ScorecardHeader.tsx @@ -88,7 +88,7 @@ const ScorecardHeader: FC = () => { Input Tokens: {' '} - {workflowRun.usage?.input} + {workflowRun.usage?.input ?? 'N/A'} )} @@ -98,7 +98,7 @@ const ScorecardHeader: FC = () => { Output Tokens: {' '} - {workflowRun.usage?.output} + {workflowRun.usage?.output ?? 'N/A'} )} From 3fae0afdcfcb8d4f7e959f9a9b935dcda0a4e53b Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 20 Nov 2025 01:02:48 +0100 Subject: [PATCH 078/125] feat: likes and dislikes on run items and comments --- .../lib/assets/icons/icon-thumb-up-filled.svg | 3 + .../src/lib/assets/icons/icon-thumb-up.svg | 3 + .../assets/icons/icon-thumbs-down-filled.svg | 3 + .../src/lib/assets/icons/icon-thumbs-down.svg | 3 + src/apps/review/src/lib/assets/icons/index.ts | 8 + .../AiFeedback/AiFeedback.module.scss | 45 ++++ .../AiFeedback/AiFeedback.tsx | 15 +- .../AiFeedbackActions.module.scss | 40 +++ .../AiFeedbackActions/AiFeedbackActions.tsx | 227 ++++++++++++++++++ .../AiFeedbackComments.module.scss | 29 +++ .../AiFeedbackComments/AiFeedbackComments.tsx | 57 +++++ .../src/lib/services/scorecards.service.ts | 29 ++- 12 files changed, 459 insertions(+), 3 deletions(-) create mode 100644 src/apps/review/src/lib/assets/icons/icon-thumb-up-filled.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-thumb-up.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-thumbs-down-filled.svg create mode 100644 src/apps/review/src/lib/assets/icons/icon-thumbs-down.svg create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx diff --git a/src/apps/review/src/lib/assets/icons/icon-thumb-up-filled.svg b/src/apps/review/src/lib/assets/icons/icon-thumb-up-filled.svg new file mode 100644 index 000000000..e862a480c --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-thumb-up-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-thumb-up.svg b/src/apps/review/src/lib/assets/icons/icon-thumb-up.svg new file mode 100644 index 000000000..8f31f2966 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-thumb-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-thumbs-down-filled.svg b/src/apps/review/src/lib/assets/icons/icon-thumbs-down-filled.svg new file mode 100644 index 000000000..5f8c2c03f --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-thumbs-down-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/apps/review/src/lib/assets/icons/icon-thumbs-down.svg b/src/apps/review/src/lib/assets/icons/icon-thumbs-down.svg new file mode 100644 index 000000000..abe7616a6 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-thumbs-down.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 dffc1a502..de1e549c0 100644 --- a/src/apps/review/src/lib/assets/icons/index.ts +++ b/src/apps/review/src/lib/assets/icons/index.ts @@ -15,6 +15,10 @@ import { ReactComponent as IconPremium } from './icon-premium.svg' import { ReactComponent as IconComment } from './icon-comment.svg' import { ReactComponent as IconEdit } from './icon-edit.svg' import { ReactComponent as IconFile } from './icon-file.svg' +import { ReactComponent as IconThumbsUp } from './icon-thumb-up.svg' +import { ReactComponent as IconThumbsDown } from './icon-thumbs-down.svg' +import { ReactComponent as IconThumbsUpFilled } from './icon-thumb-up-filled.svg' +import { ReactComponent as IconThumbsDownFilled } from './icon-thumbs-down-filled.svg' export * from './editor/bold' export * from './editor/code' @@ -49,6 +53,10 @@ export { IconComment, IconEdit, IconFile, + IconThumbsUp, + IconThumbsDown, + IconThumbsUpFilled, + IconThumbsDownFilled, } export const phasesIcons = { diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss index fe1718d67..bd3e71bc7 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.module.scss @@ -8,4 +8,49 @@ margin-bottom: $sp-2; } } + + .comment { + margin-top: $sp-3; + padding-left: $sp-4; + border-left: 2px solid black; + } + + .commentContent { + margin-bottom: $sp-1; + } + + .commentActions { + display: flex; + gap: $sp-2; + margin-bottom: $sp-2; + } + + .commentBtn { + display: inline-flex; + align-items: center; + gap: $sp-1; + background: transparent; + border: none; + cursor: pointer; + padding: $sp-1; + } + + .replies { + margin-left: $sp-4; + margin-top: $sp-2; + } + + .reply { + margin-top: $sp-2; + } + + .count { + font-size: 12px; + margin-left: $sp-1; + } + + .active { + color: blue; + font-weight: 600; + } } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx index 5b57e6f8f..2e1007dd5 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedback/AiFeedback.tsx @@ -7,6 +7,8 @@ import { ScorecardViewerContextValue, useScorecardViewerContext } from '../../Sc import { ScorecardQuestionRow } from '../ScorecardQuestionRow' import { ScorecardScore } from '../../ScorecardScore' import { MarkdownReview } from '../../../../MarkdownReview' +import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions' +import { AiFeedbackComments } from '../AiFeedbackComments/AiFeedbackComments' import styles from './AiFeedback.module.scss' @@ -16,10 +18,12 @@ interface AiFeedbackProps { const AiFeedback: FC = props => { const { aiFeedbackItems, scoreMap }: ScorecardViewerContextValue = useScorecardViewerContext() - const feedback = useMemo(() => ( - aiFeedbackItems?.find(r => r.scorecardQuestionId === props.question.id) + const feedback: any = useMemo(() => ( + aiFeedbackItems?.find((r: any) => r.scorecardQuestionId === props.question.id) ), [props.question.id, aiFeedbackItems]) + const commentsArr: any[] = (feedback?.comments) || [] + if (!aiFeedbackItems?.length || !feedback) { return <> } @@ -43,7 +47,14 @@ const AiFeedback: FC = props => { {feedback.questionScore ? 'Yes' : 'No'}

    )} + + + + + {commentsArr.length > 0 && ( + + )} ) } diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.module.scss new file mode 100644 index 000000000..ba7b4fcec --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.module.scss @@ -0,0 +1,40 @@ +@import '@libs/ui/styles/includes'; + +.actions { + display: flex; + gap: $sp-2; + margin-top: $sp-3; + .count { + font-family: $font-roboto; + color: #0D61BF; + font-weight: 700; + font-size: 14px; + } +} + +.actionBtn { + display: inline-flex; + align-items: center; + gap: $sp-2; + background: transparent; + border: none; + cursor: pointer; + color: black; + padding: $sp-1 $sp-2; + &.active { + svg { + path { + fill: $link-blue-dark; + } + } + } + + svg { + width: 20px; + height: 20px; + } + + &:hover { + opacity: 0.85; + } +} \ No newline at end of file diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx new file mode 100644 index 000000000..d7e71567b --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx @@ -0,0 +1,227 @@ +/* eslint-disable react/jsx-no-bind */ +import { FC, useCallback, useContext, useEffect, useState } from 'react' +import { mutate } from 'swr' + +import { + IconThumbsDown, + IconThumbsDownFilled, + IconThumbsUp, + IconThumbsUpFilled, +} from '~/apps/review/src/lib/assets/icons' +import { ReviewAppContext } from '~/apps/review/src/lib/contexts' +import { useReviewsContext } from '~/apps/review/src/pages/reviews/ReviewsContext' +import { updateLikesOrDislikesOnRunItem, updateLikesOrDislikesOnRunItemComment } from '~/apps/review/src/lib/services' +import { EnvironmentConfig } from '~/config' +import { ReviewAppContextModel, ReviewsContextModel } from '~/apps/review/src/lib/models' + +import { AiFeedbackComment } from '../AiFeedbackComments/AiFeedbackComments' + +import styles from './AiFeedbackActions.module.scss' + +export enum VOTE_TYPE { + UPVOTE = 'UPVOTE', + DOWNVOTE = 'DOWNVOTE' +} + +interface AiFeedbackActionsProps { + actionType: 'comment' | 'runItem' + comment?: AiFeedbackComment + feedback?: any +} + +export const AiFeedbackActions: FC = props => { + + const [userVote, setUserVote] = useState(undefined) + const [upVotes, setUpVotes] = useState(0) + const [downVotes, setDownVotes] = useState(0) + + const { loginUserInfo }: ReviewAppContextModel = useContext(ReviewAppContext) + const { workflowId, workflowRun }: ReviewsContextModel = useReviewsContext() + + const votesArr: any[] = (props.actionType === 'runItem' ? (props.feedback?.votes) : (props.comment?.votes)) || [] + + const setInitialVotesForFeedback = (): void => { + const initialUp = props.feedback?.upVotes ?? votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('up')).length + const initialDown = props.feedback?.downVotes ?? votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('down')).length + + const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) + setUpVotes(initialUp) + setDownVotes(initialDown) + setUserVote(myVote?.voteType ?? undefined) + } + + const setInitialVotesForComment = (): void => { + const initialUp = votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('up')).length + const initialDown = votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('down')).length + + const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) + setUpVotes(initialUp) + setDownVotes(initialDown) + setUserVote(myVote?.voteType ?? undefined) + } + + useEffect(() => { + if (props.actionType === 'runItem') { + setInitialVotesForFeedback() + } else { + setInitialVotesForComment() + } + }, [props.actionType, props.feedback?.id, votesArr.length, loginUserInfo?.userId]) + + const voteOnItem = useCallback(async (type: VOTE_TYPE) => { + if (!workflowId || !workflowRun?.id) return + const current = userVote + let up = false + let down = false + + if (current === type) { + // remove vote + up = false + down = false + } else if (!current) { + up = type === VOTE_TYPE.UPVOTE + down = type === VOTE_TYPE.DOWNVOTE + } else { + // switch vote + up = type === VOTE_TYPE.UPVOTE + down = type === VOTE_TYPE.DOWNVOTE + } + + // optimistic update + const prevUserVote = userVote + const prevUp = upVotes + const prevDown = downVotes + + if (current === type) { + // removing + if (type === VOTE_TYPE.UPVOTE) setUpVotes(Math.max(0, upVotes - 1)) + else setDownVotes(Math.max(0, downVotes - 1)) + setUserVote(undefined) + } else if (!current) { + if (type === VOTE_TYPE.UPVOTE) setUpVotes(upVotes + 1) + else setDownVotes(downVotes + 1) + setUserVote(type) + } else { + // switch + if (type === VOTE_TYPE.UPVOTE) { + setUpVotes(upVotes + 1) + setDownVotes(Math.max(0, downVotes - 1)) + } else { + setDownVotes(downVotes + 1) + setUpVotes(Math.max(0, upVotes - 1)) + } + + setUserVote(type) + } + + try { + await updateLikesOrDislikesOnRunItem(workflowId, workflowRun.id, props.feedback.id, { + downVote: down, + upVote: up, + }) + } catch (err) { + // rollback + setUserVote(prevUserVote) + setUpVotes(prevUp) + setDownVotes(prevDown) + } + }, [workflowId, workflowRun, props.feedback?.id, userVote, upVotes, downVotes]) + + const voteOnComment = useCallback(async (c: any, type: VOTE_TYPE) => { + if (!workflowId || !workflowRun?.id) return + const votes = (c.votes || []) + const my = votes.find((v: any) => String(v.createdBy) === String(loginUserInfo?.userId)) + const current = my?.voteType ?? undefined + + let up = false + let down = false + + if (current === type) { + up = false + down = false + } else if (!current) { + up = type === VOTE_TYPE.UPVOTE + down = type === VOTE_TYPE.DOWNVOTE + } else { + up = type === VOTE_TYPE.UPVOTE + down = type === VOTE_TYPE.DOWNVOTE + } + + const prevUserVote = userVote + const prevUp = upVotes + const prevDown = downVotes + + if (current === type) { + // removing + if (type === VOTE_TYPE.UPVOTE) setUpVotes(Math.max(0, upVotes - 1)) + else setDownVotes(Math.max(0, downVotes - 1)) + setUserVote(undefined) + } else if (!current) { + if (type === VOTE_TYPE.UPVOTE) setUpVotes(upVotes + 1) + else setDownVotes(downVotes + 1) + setUserVote(type) + } else { + // switch + if (type === VOTE_TYPE.UPVOTE) { + setUpVotes(upVotes + 1) + setDownVotes(Math.max(0, downVotes - 1)) + } else { + setDownVotes(downVotes + 1) + setUpVotes(Math.max(0, upVotes - 1)) + } + + setUserVote(type) + + } + + try { + await updateLikesOrDislikesOnRunItemComment(workflowId, workflowRun.id, props.feedback.id, c.id, { + downVote: down, + upVote: up, + }) + await mutate(`${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${workflowRun.id}/items`) + } catch (err) { + setUserVote(prevUserVote) + setUpVotes(prevUp) + setDownVotes(prevDown) + } + }, [workflowId, workflowRun, props.feedback?.id, loginUserInfo]) + + const onVote = (action: VOTE_TYPE): void => { + if (props.actionType === 'comment') { + voteOnComment(props.comment as AiFeedbackComment, action) + } else { + voteOnItem(action) + } + } + + return ( +
    + + + +
    + ) +} diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.module.scss b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.module.scss new file mode 100644 index 000000000..1a465a9b1 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.module.scss @@ -0,0 +1,29 @@ +@import '@libs/ui/styles/includes'; + +.comments { + font-family: "Nunito Sans", sans-serif; + .comment { + margin-top: 32px; + background-color: #E0E4E8; + padding: 16px; + .info { + margin: 16px 0; + font-size: 14px; + .reply { + color: #0A0A0A; + font-weight: $font-weight-bold; + } + .text { + font-weight: $font-weight-normal; + color: #767676; + } + .name { + color: #0A0A0A; + font-weight: $font-weight-bold; + } + .date { + color: #0A0A0A; + } + } + } +} \ No newline at end of file diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx new file mode 100644 index 000000000..28c9a1414 --- /dev/null +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx @@ -0,0 +1,57 @@ +import { FC } from 'react' + +import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions' + +import styles from './AiFeedbackComments.module.scss' + +export interface AiFeedbackVote { + id: string + parentId: string + voteType: string + createdAt: string + createdBy: string +} +export interface AiFeedbackComment { + id: string + content: string + parentId: string + createdBy: string + createdAt: string + createdUser: { + userId: string + handle: string + ratingColor: string + } + votes: AiFeedbackVote[] +} + +interface AiFeedbackCommentsProps { + comments: AiFeedbackComment[] + feedback: any +} + +export const AiFeedbackComments: FC = props => ( +
    + {props.comments.filter(c => !c.parentId) + .map((comment: AiFeedbackComment) => ( +
    +
    + Reply + by + + {comment.createdUser.handle} + + on + {comment.createdAt} +
    +
    {comment.content}
    + +
    + ))} +
    +) diff --git a/src/apps/review/src/lib/services/scorecards.service.ts b/src/apps/review/src/lib/services/scorecards.service.ts index 909527b3d..6ff09fb73 100644 --- a/src/apps/review/src/lib/services/scorecards.service.ts +++ b/src/apps/review/src/lib/services/scorecards.service.ts @@ -1,7 +1,7 @@ /** * Scorecards service */ -import { xhrPostAsync, xhrPutAsync } from '~/libs/core' +import { xhrPatchAsync, xhrPostAsync, xhrPutAsync } from '~/libs/core' import { EnvironmentConfig } from '~/config' import { Scorecard } from '../models' @@ -29,3 +29,30 @@ export const saveScorecard = async (scorecard: Scorecard): Promise => return xhrPutAsync(`${baseUrl}/${scorecard.id}`, scorecard) } + +export const updateLikesOrDislikesOnRunItem = ( + workflowId: string, + runId: string, + feedbackId: string, + body: { + upVote: boolean + downVote: boolean + }, +): Promise => xhrPatchAsync( + `${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${runId}/items/${feedbackId}`, + body, +) + +export const updateLikesOrDislikesOnRunItemComment = ( + workflowId: string, + runId: string, + feedbackId: string, + commentId: string, + body: { + upVote: boolean + downVote: boolean + }, +): Promise => xhrPatchAsync( + `${EnvironmentConfig.API.V6}/workflows/${workflowId}/runs/${runId}/items/${feedbackId}/comments/${commentId}`, + body, +) From d383431df84accd261832fee29f004a324581ba5 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 20 Nov 2025 01:10:37 +0100 Subject: [PATCH 079/125] feat: likes and dislikes on run items and comments --- .../AiFeedbackComments/AiFeedbackComments.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx index 28c9a1414..5916279fe 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx @@ -1,4 +1,5 @@ import { FC } from 'react' +import moment from 'moment' import { AiFeedbackActions } from '../AiFeedbackActions/AiFeedbackActions' @@ -47,7 +48,7 @@ export const AiFeedbackComments: FC = props => ( {comment.createdUser.handle} on - {comment.createdAt} + { moment(comment.createdAt).local().format('MMM DD, hh:mm A')}
    {comment.content}
    From 5dd1dc499f46491f862ddeb394ab5093614c842c Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Thu, 20 Nov 2025 01:11:50 +0100 Subject: [PATCH 080/125] feat: likes and dislikes on run items and comments --- .../AiFeedbackComments/AiFeedbackComments.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx index 5916279fe..46bb0ec91 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComments.tsx @@ -48,7 +48,11 @@ export const AiFeedbackComments: FC = props => ( {comment.createdUser.handle} on - { moment(comment.createdAt).local().format('MMM DD, hh:mm A')} + + { moment(comment.createdAt) + .local() + .format('MMM DD, hh:mm A')} +
    {comment.content}
    From 4c40f2a014c76a9b7fbcd513a0123d195c64f33c Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 20 Nov 2025 21:53:59 +1100 Subject: [PATCH 081/125] Initial reports pages implementation in system-admin --- src/apps/admin/src/admin-app.routes.tsx | 11 + src/apps/admin/src/config/routes.config.ts | 1 + .../Tab/config/system-admin-tabs-config.ts | 5 + src/apps/admin/src/lib/services/index.ts | 1 + .../admin/src/lib/services/reports.service.ts | 54 ++++ .../admin/src/reports/ReportsPage.module.scss | 58 ++++ src/apps/admin/src/reports/ReportsPage.tsx | 244 +++++++++++++++ .../ChallengeDetailsContent.tsx | 81 +++-- src/apps/review/src/lib/hooks/index.ts | 1 + .../lib/hooks/useFetchChallengeSubmissions.ts | 74 ++++- .../src/lib/hooks/useFetchScreeningReview.ts | 39 ++- .../src/lib/hooks/useReviewEditAccess.ts | 292 ++++++++++++++++++ .../ScorecardDetailsPage.tsx | 280 +---------------- .../components/ReviewViewer/ReviewViewer.tsx | 14 +- start.sh | 6 +- 15 files changed, 846 insertions(+), 315 deletions(-) create mode 100644 src/apps/admin/src/lib/services/reports.service.ts create mode 100644 src/apps/admin/src/reports/ReportsPage.module.scss create mode 100644 src/apps/admin/src/reports/ReportsPage.tsx create mode 100644 src/apps/review/src/lib/hooks/useReviewEditAccess.ts diff --git a/src/apps/admin/src/admin-app.routes.tsx b/src/apps/admin/src/admin-app.routes.tsx index 0d5163fb3..b5db82d55 100644 --- a/src/apps/admin/src/admin-app.routes.tsx +++ b/src/apps/admin/src/admin-app.routes.tsx @@ -17,6 +17,7 @@ import { paymentsRouteId, permissionManagementRouteId, platformRouteId, + reportsRouteId, rootRoute, termsRouteId, userManagementRouteId, @@ -168,6 +169,10 @@ const PaymentsPage: LazyLoadedComponent = lazyLoad( () => import('./payments/PaymentsPage'), 'PaymentsPage', ) +const ReportsPage: LazyLoadedComponent = lazyLoad( + () => import('./reports/ReportsPage'), + 'ReportsPage', +) export const toolTitle: string = ToolTitle.admin @@ -402,6 +407,12 @@ export const adminRoutes: ReadonlyArray = [ id: paymentsRouteId, route: paymentsRouteId, }, + // Reports Module + { + element: , + id: reportsRouteId, + route: reportsRouteId, + }, ], domain: AppSubdomain.admin, element: , diff --git a/src/apps/admin/src/config/routes.config.ts b/src/apps/admin/src/config/routes.config.ts index a2ebf2790..b1b523086 100644 --- a/src/apps/admin/src/config/routes.config.ts +++ b/src/apps/admin/src/config/routes.config.ts @@ -18,3 +18,4 @@ export const termsRouteId = 'terms' export const defaultReviewersRouteId = 'default-reviewers' export const platformRouteId = 'platform' export const paymentsRouteId = 'payments' +export const reportsRouteId = 'reports' diff --git a/src/apps/admin/src/lib/components/common/Tab/config/system-admin-tabs-config.ts b/src/apps/admin/src/lib/components/common/Tab/config/system-admin-tabs-config.ts index 8c07f6f6c..55c448402 100644 --- a/src/apps/admin/src/lib/components/common/Tab/config/system-admin-tabs-config.ts +++ b/src/apps/admin/src/lib/components/common/Tab/config/system-admin-tabs-config.ts @@ -10,6 +10,7 @@ import { paymentsRouteId, permissionManagementRouteId, platformRouteId, + reportsRouteId, termsRouteId, userManagementRouteId, } from '~/apps/admin/src/config/routes.config' @@ -82,6 +83,10 @@ export const SystemAdminTabsConfig: TabsNavItem[] = [ id: paymentsRouteId, title: 'Payments', }, + { + id: reportsRouteId, + title: 'Reports', + }, ] export function getTabIdFromPathName(pathname: string): string { diff --git a/src/apps/admin/src/lib/services/index.ts b/src/apps/admin/src/lib/services/index.ts index b47f23458..bf260a1b9 100644 --- a/src/apps/admin/src/lib/services/index.ts +++ b/src/apps/admin/src/lib/services/index.ts @@ -14,3 +14,4 @@ export * from './default-reviewers.service' export * from './timeline-templates.service' export * from './phases.service' export * from './scorecards.service' +export * from './reports.service' diff --git a/src/apps/admin/src/lib/services/reports.service.ts b/src/apps/admin/src/lib/services/reports.service.ts new file mode 100644 index 000000000..be460edfa --- /dev/null +++ b/src/apps/admin/src/lib/services/reports.service.ts @@ -0,0 +1,54 @@ +import type { AxiosInstance } from 'axios' + +import { EnvironmentConfig } from '~/config' +import { xhrCreateInstance, xhrGetAsync } from '~/libs/core/lib/xhr' + +export type ReportDefinition = { + name: string + path: string + description?: string + method: string +} + +export type ReportGroup = { + label: string + basePath: string + reports: ReportDefinition[] +} + +export type ReportsIndexResponse = Record + +const reportsDownloadClient: AxiosInstance = xhrCreateInstance() + +const buildReportUrl = (path: string): string => { + const normalizedPath = path.startsWith('/') ? path : `/${path}` + return `${EnvironmentConfig.API.V6}/reports${normalizedPath}` +} + +export const fetchReportsIndex = async (): Promise => ( + xhrGetAsync(`${EnvironmentConfig.API.V6}/reports`) +) + +const downloadReportBlob = async (path: string, accept: string): Promise => { + if (!path) { + throw new Error('Report path is required') + } + + const url = buildReportUrl(path) + const response = await reportsDownloadClient.get(url, { + headers: { + Accept: accept, + }, + responseType: 'blob', + }) + + return response.data +} + +export const downloadReportAsJson = (path: string): Promise => ( + downloadReportBlob(path, 'application/json') +) + +export const downloadReportAsCsv = (path: string): Promise => ( + downloadReportBlob(path, 'text/csv') +) diff --git a/src/apps/admin/src/reports/ReportsPage.module.scss b/src/apps/admin/src/reports/ReportsPage.module.scss new file mode 100644 index 000000000..4c84c4006 --- /dev/null +++ b/src/apps/admin/src/reports/ReportsPage.module.scss @@ -0,0 +1,58 @@ +.page { + display: flex; + flex-direction: column; + gap: 24px; +} + +.instructions { + color: #565a5f; + max-width: 720px; +} + +.selectors { + display: flex; + flex-wrap: wrap; + gap: 16px; +} + +.select { + min-width: 260px; + max-width: 340px; +} + +.reportDetails { + display: flex; + flex-direction: column; + gap: 4px; +} + +.reportTitle { + font-weight: 600; +} + +.reportDescription { + color: #494f55; +} + +.reportMeta { + font-family: 'Roboto Mono', monospace; + font-size: 13px; + color: #6b6f75; +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 12px; +} + +.spinnerWrapper { + padding: 40px 0; + display: flex; + justify-content: center; +} + +.emptyState { + font-style: italic; + color: #6b6f75; +} diff --git a/src/apps/admin/src/reports/ReportsPage.tsx b/src/apps/admin/src/reports/ReportsPage.tsx new file mode 100644 index 000000000..f3c7873af --- /dev/null +++ b/src/apps/admin/src/reports/ReportsPage.tsx @@ -0,0 +1,244 @@ +import { ChangeEvent, FC, useCallback, useEffect, useMemo, useState } from 'react' + +import { Button, InputSelect, InputSelectOption, LoadingSpinner, PageTitle } from '~/libs/ui' + +import { PageContent, PageHeader } from '../lib' +import { handleError } from '../lib/utils' +import { + downloadReportAsCsv, + downloadReportAsJson, + fetchReportsIndex, + ReportDefinition, + ReportGroup, + ReportsIndexResponse, +} from '../lib/services' + +import styles from './ReportsPage.module.scss' + +const pageTitle = 'Reports' + +const buildDownloadName = (name: string, extension: 'json' | 'csv'): string => { + const normalized = name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)+/g, '') + + const base = normalized || 'report' + return `${base}.${extension}` +} + +const formatMethod = (method?: string): string => ( + method ? method.toUpperCase() : 'GET' +) + +export const ReportsPage: FC = () => { + const [reportsIndex, setReportsIndex] = useState({}) + const [selectedBasePath, setSelectedBasePath] = useState('') + const [selectedReportPath, setSelectedReportPath] = useState('') + const [isLoading, setIsLoading] = useState(false) + const [downloadingFormat, setDownloadingFormat] = useState<'json' | 'csv' | undefined>(undefined) + + useEffect(() => { + let isMounted = true + setIsLoading(true) + + fetchReportsIndex() + .then(data => { + if (!isMounted) return + setReportsIndex(data ?? {}) + }) + .catch(error => { + if (!isMounted) return + handleError(error) + }) + .finally(() => { + if (isMounted) { + setIsLoading(false) + } + }) + + return () => { + isMounted = false + } + }, []) + + const basePathOptions = useMemo(() => { + const groups: ReportGroup[] = Object.values(reportsIndex ?? {}) + const options = groups.map(group => ({ + label: group.label || group.basePath, + value: group.basePath, + })) + + options.sort((a, b) => a.label.localeCompare(b.label)) + return options + }, [reportsIndex]) + + const selectedGroup = useMemo(() => ( + selectedBasePath + ? Object.values(reportsIndex) + .find(group => group.basePath === selectedBasePath) + : undefined + ), [reportsIndex, selectedBasePath]) + + const reportOptions = useMemo(() => { + if (!selectedGroup?.reports?.length) { + return [] + } + + const options = selectedGroup.reports.map(report => ({ + label: report.name, + value: report.path, + })) + + options.sort((a, b) => a.label.localeCompare(b.label)) + return options + }, [selectedGroup]) + + const selectedReport = useMemo(() => ( + selectedGroup?.reports?.find(report => report.path === selectedReportPath) + ), [selectedGroup, selectedReportPath]) + + const handleBasePathChange = useCallback((event: ChangeEvent) => { + setSelectedBasePath(event.target.value) + setSelectedReportPath('') + }, []) + + const handleReportChange = useCallback((event: ChangeEvent) => { + setSelectedReportPath(event.target.value) + }, []) + + const handleDownload = useCallback(async (format: 'json' | 'csv') => { + if (!selectedReport) { + return + } + + try { + setDownloadingFormat(format) + + const blob = format === 'json' + ? await downloadReportAsJson(selectedReport.path) + : await downloadReportAsCsv(selectedReport.path) + + const link = document.createElement('a') + const fileName = buildDownloadName(selectedReport.name, format) + const url = window.URL.createObjectURL(blob) + + link.href = url + link.setAttribute('download', fileName) + document.body.appendChild(link) + link.click() + link.parentNode?.removeChild(link) + window.URL.revokeObjectURL(url) + } catch (error) { + handleError(error) + } finally { + setDownloadingFormat(undefined) + } + }, [selectedReport]) + + const isDownloading = downloadingFormat !== undefined + + const handleJsonDownload = useCallback(() => { + handleDownload('json') + }, [handleDownload]) + + const handleCsvDownload = useCallback(() => { + handleDownload('csv') + }, [handleDownload]) + + return ( + <> + + {pageTitle} + + +
    +

    + Select a base path to view the available reports. After choosing a report, download + the data as JSON or CSV directly from the reports API. +

    + + {isLoading ? ( +
    + +
    + ) : ( + <> + {basePathOptions.length ? ( +
    + + + {selectedGroup && ( + + )} +
    + ) : ( +
    + No reports are currently available. +
    + )} + + {selectedReport && ( + <> +
    +
    {selectedReport.name}
    + {selectedReport.description && ( +
    + {selectedReport.description} +
    + )} +
    + {formatMethod(selectedReport.method)} + {' '} + {selectedReport.path} +
    +
    + +
    + + +
    + + )} + + )} +
    +
    + + ) +} + +export default ReportsPage diff --git a/src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx b/src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx index 4e963bc00..50edfbe4c 100644 --- a/src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx +++ b/src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx @@ -286,6 +286,31 @@ export const ChallengeDetailsContent: FC = (props: Props) => { ), [challengeInfo?.phases], ) + const screeningOutcome = useMemo( + () => { + const passingSubmissionIds = new Set() + const failingSubmissionIds = new Set() + + props.screening.forEach(entry => { + if (!entry?.submissionId) { + return + } + + const normalizedResult = (entry.result || '').toUpperCase() + if (normalizedResult === 'PASS') { + passingSubmissionIds.add(`${entry.submissionId}`) + } else if (normalizedResult === 'NO PASS') { + failingSubmissionIds.add(`${entry.submissionId}`) + } + }) + + return { + failingSubmissionIds, + passingSubmissionIds, + } + }, + [props.screening], + ) const passesReviewTabGuards: (submission: SubmissionInfo) => boolean = useMemo( () => (submission: SubmissionInfo): boolean => shouldIncludeInReviewPhase( submission, @@ -300,36 +325,22 @@ export const ChallengeDetailsContent: FC = (props: Props) => { reviews: SubmissionInfo[] submitterReviews: SubmissionInfo[] } = useMemo(() => { - const shouldFilter = props.isActiveChallenge && hasScreeningPhase - if (!shouldFilter) { - return { - reviews: props.review.filter(passesReviewTabGuards), - submitterReviews: props.submitterReviews.filter(passesReviewTabGuards), - } - } - - const passingSubmissionIds = new Set() - props.screening.forEach(entry => { - if (!entry?.submissionId) { - return - } - - const result = (entry.result || '').toUpperCase() - if (result === 'PASS') { - passingSubmissionIds.add(`${entry.submissionId}`) - } - }) - - if (passingSubmissionIds.size === 0) { - return { - reviews: props.review - .filter(passesReviewTabGuards), - submitterReviews: props.submitterReviews - .filter(passesReviewTabGuards), + const { + failingSubmissionIds, + passingSubmissionIds, + }: { + failingSubmissionIds: Set + passingSubmissionIds: Set + } = screeningOutcome + const shouldFilter = props.isActiveChallenge + && (hasScreeningPhase || props.screening.length > 0) + && (passingSubmissionIds.size > 0 || failingSubmissionIds.size > 0) + + const matchesScreeningOutcome = (submission: SubmissionInfo): boolean => { + if (!shouldFilter) { + return true } - } - const matchesPassingScreening = (submission: SubmissionInfo): boolean => { if (!submission) { return false } @@ -348,15 +359,23 @@ export const ChallengeDetailsContent: FC = (props: Props) => { return true } + if (passingSubmissionIds.size > 0) { + return candidateIds.some(id => passingSubmissionIds.has(id)) + } + + if (failingSubmissionIds.size > 0) { + return !candidateIds.some(id => failingSubmissionIds.has(id)) + } + return candidateIds.some(id => passingSubmissionIds.has(id)) } return { reviews: props.review - .filter(matchesPassingScreening) + .filter(matchesScreeningOutcome) .filter(passesReviewTabGuards), submitterReviews: props.submitterReviews - .filter(matchesPassingScreening) + .filter(matchesScreeningOutcome) .filter(passesReviewTabGuards), } }, [ @@ -365,7 +384,9 @@ export const ChallengeDetailsContent: FC = (props: Props) => { props.review, props.submitterReviews, props.screening, + props.screening.length, passesReviewTabGuards, + screeningOutcome, ]) const renderSelectedTab = (): JSX.Element => { diff --git a/src/apps/review/src/lib/hooks/index.ts b/src/apps/review/src/lib/hooks/index.ts index f984003c8..2e069dd47 100644 --- a/src/apps/review/src/lib/hooks/index.ts +++ b/src/apps/review/src/lib/hooks/index.ts @@ -20,3 +20,4 @@ export * from './useSubmissionHistory' export * from './useScorecardPassingScores' export * from './useFetchAiWorkflowRuns' export * from './useFetchSubmissionInfo' +export * from './useReviewEditAccess' diff --git a/src/apps/review/src/lib/hooks/useFetchChallengeSubmissions.ts b/src/apps/review/src/lib/hooks/useFetchChallengeSubmissions.ts index da4e7721f..4c28a8ac0 100644 --- a/src/apps/review/src/lib/hooks/useFetchChallengeSubmissions.ts +++ b/src/apps/review/src/lib/hooks/useFetchChallengeSubmissions.ts @@ -1,18 +1,27 @@ import { useEffect, + useMemo, } from 'react' import useSWR, { SWRResponse } from 'swr' import { handleError } from '~/libs/shared' -import { BackendSubmission } from '../models' +import { BackendSubmission, BackendSubmissionStatus } from '../models' import { fetchSubmissions } from '../services' export interface useFetchChallengeSubmissionsProps { challengeSubmissions: BackendSubmission[] + deletedLegacySubmissionIds: Set + deletedSubmissionIds: Set isLoading: boolean } +interface ChallengeSubmissionsMemoResult { + deletedLegacySubmissionIds: Set + deletedSubmissionIds: Set + filteredSubmissions: BackendSubmission[] +} + /** * Fetch challenge submissions * @param challengeId challenge id @@ -44,8 +53,69 @@ export function useFetchChallengeSubmissions( } }, [error]) + const { + filteredSubmissions, + deletedLegacySubmissionIds, + deletedSubmissionIds, + }: ChallengeSubmissionsMemoResult = useMemo(() => { + if (!challengeSubmissions?.length) { + return { + deletedLegacySubmissionIds: new Set(), + deletedSubmissionIds: new Set(), + filteredSubmissions: challengeSubmissions ?? [], + } + } + + const normalizedDeletedIds = new Set() + const normalizedDeletedLegacyIds = new Set() + const activeSubmissions: BackendSubmission[] = [] + + const normalizeStatus = (status: unknown): string => { + if (typeof status === 'string') { + return status.trim() + .toUpperCase() + } + + if (typeof status === 'number') { + const mapped = BackendSubmissionStatus[status as BackendSubmissionStatus] + if (mapped) { + return `${mapped}`.trim() + .toUpperCase() + } + } + + return `${status ?? ''}`.trim() + .toUpperCase() + } + + challengeSubmissions.forEach(submission => { + const status = normalizeStatus(submission?.status) + if (status === 'DELETED') { + if (submission?.id) { + normalizedDeletedIds.add(`${submission.id}`) + } + + if (submission?.legacySubmissionId) { + normalizedDeletedLegacyIds.add(`${submission.legacySubmissionId}`) + } + + return + } + + activeSubmissions.push(submission) + }) + + return { + deletedLegacySubmissionIds: normalizedDeletedLegacyIds, + deletedSubmissionIds: normalizedDeletedIds, + filteredSubmissions: activeSubmissions, + } + }, [challengeSubmissions]) + return { - challengeSubmissions: challengeSubmissions ?? [], + challengeSubmissions: filteredSubmissions, + deletedLegacySubmissionIds, + deletedSubmissionIds, isLoading, } } diff --git a/src/apps/review/src/lib/hooks/useFetchScreeningReview.ts b/src/apps/review/src/lib/hooks/useFetchScreeningReview.ts index 226a73b9f..370816480 100644 --- a/src/apps/review/src/lib/hooks/useFetchScreeningReview.ts +++ b/src/apps/review/src/lib/hooks/useFetchScreeningReview.ts @@ -447,13 +447,15 @@ export function useFetchScreeningReview(): useFetchScreeningReviewProps { // fetch challenge submissions const { - challengeSubmissions: allChallengeSubmissions, + challengeSubmissions, + deletedLegacySubmissionIds, + deletedSubmissionIds, isLoading, }: useFetchChallengeSubmissionsProps = useFetchChallengeSubmissions(challengeId) const visibleChallengeSubmissions = useMemo( - () => allChallengeSubmissions, - [allChallengeSubmissions], + () => challengeSubmissions, + [challengeSubmissions], ) const visibleSubmissionsById = useMemo( @@ -716,8 +718,35 @@ export function useFetchScreeningReview(): useFetchScreeningReviewProps { }, [challengeId, reviewerKey]) const challengeReviews = useMemo( - () => challengeReviewsData, - [challengeReviewsData], + () => { + if (!challengeReviewsData) { + return challengeReviewsData + } + + if (!deletedSubmissionIds.size && !deletedLegacySubmissionIds.size) { + return challengeReviewsData + } + + return challengeReviewsData.filter(reviewEntry => { + if (!reviewEntry) { + return false + } + + if (reviewEntry.submissionId && deletedSubmissionIds.has(`${reviewEntry.submissionId}`)) { + return false + } + + if ( + reviewEntry.legacySubmissionId + && deletedLegacySubmissionIds.has(`${reviewEntry.legacySubmissionId}`) + ) { + return false + } + + return true + }) + }, + [challengeReviewsData, deletedLegacySubmissionIds, deletedSubmissionIds], ) const challengeReviewById = useMemo(() => { diff --git a/src/apps/review/src/lib/hooks/useReviewEditAccess.ts b/src/apps/review/src/lib/hooks/useReviewEditAccess.ts new file mode 100644 index 000000000..e88f79452 --- /dev/null +++ b/src/apps/review/src/lib/hooks/useReviewEditAccess.ts @@ -0,0 +1,292 @@ +import { useMemo } from 'react' + +import { + BackendResource, + ChallengeInfo, + ReviewInfo, + ScorecardInfo, +} from '../models' + +type ReviewPhaseType = + | 'screening' + | 'checkpoint screening' + | 'checkpoint review' + | 'post-mortem' + | 'approval' + | 'review' + +type ReviewerConfig = { + phaseId?: unknown + scorecardId?: unknown + type?: unknown +} + +type ChallengePhaseSummary = { + id?: unknown + name?: unknown +} + +type RoleMatcher = (normalizedRoleName: string) => boolean + +const isNil = (value: unknown): value is null | undefined => value === null || value === undefined + +const normalizeRoleName = (value: unknown): string => { + if (typeof value !== 'string') { + return '' + } + + return value + .trim() + .toLowerCase() + .replace(/[^a-z]/g, '') +} + +const POST_MORTEM_KEYWORDS = ['post-mortem', 'post mortem', 'postmortem'] + +type PhaseStringMatcher = { + match: (source: string) => boolean + phase: ReviewPhaseType +} + +const PHASE_STRING_MATCHERS: PhaseStringMatcher[] = [ + { + match: source => source.includes('checkpoint screening'), + phase: 'checkpoint screening', + }, + { + match: source => source.includes('checkpoint review'), + phase: 'checkpoint review', + }, + { + match: source => POST_MORTEM_KEYWORDS.some(keyword => source.includes(keyword)), + phase: 'post-mortem', + }, + { + match: source => source.includes('screening') && !source.includes('checkpoint'), + phase: 'screening', + }, + { + match: source => source.includes('approval'), + phase: 'approval', + }, + { + match: source => source.includes('review'), + phase: 'review', + }, +] + +const detectPhaseTypeFromString = (value: string): ReviewPhaseType | undefined => { + const normalized = value.trim() + .toLowerCase() + if (!normalized) { + return undefined + } + + for (const matcher of PHASE_STRING_MATCHERS) { + if (matcher.match(normalized)) { + return matcher.phase + } + } + + return undefined +} + +const detectPhaseTypeFromObject = (value: Record): ReviewPhaseType | undefined => { + const candidateKeys: Array = [ + 'name', + 'phaseName', + 'phase', + 'type', + ].filter(key => key in value) as Array + + for (const key of candidateKeys) { + const detected = detectReviewPhaseType(value[key]) + if (detected) { + return detected + } + } + + return undefined +} + +function detectReviewPhaseType(value?: unknown): ReviewPhaseType | undefined { + if (isNil(value)) { + return undefined + } + + if (typeof value === 'object') { + return detectPhaseTypeFromObject(value as Record) + } + + return detectPhaseTypeFromString(`${value}`) +} + +const PHASE_ROLE_MATCHERS: Partial> = { + approval: normalizedRoleName => ( + normalizedRoleName.includes('approver') + || normalizedRoleName.includes('approval') + ), + 'checkpoint review': normalizedRoleName => normalizedRoleName === 'checkpointreviewer', + 'checkpoint screening': normalizedRoleName => normalizedRoleName === 'checkpointscreener', + 'post-mortem': normalizedRoleName => normalizedRoleName.includes('postmortem'), + review: normalizedRoleName => ( + normalizedRoleName.includes('reviewer') + && !normalizedRoleName.includes('checkpoint') + && !normalizedRoleName.includes('postmortem') + ), + screening: normalizedRoleName => ( + ( + normalizedRoleName.includes('screener') + || normalizedRoleName.includes('screening') + ) + && !normalizedRoleName.includes('checkpoint') + ), +} + +const detectReviewTypeFromReviewerConfig = ( + reviewerConfigs: ReviewerConfig[] | undefined, + normalizedPhaseId?: string, + normalizedScorecardId?: string, +): ReviewPhaseType | undefined => { + if (!reviewerConfigs?.length) { + return undefined + } + + const matchedConfig = reviewerConfigs.find(config => ( + (normalizedPhaseId && `${config.phaseId}` === normalizedPhaseId) + || (normalizedScorecardId && `${config.scorecardId}` === normalizedScorecardId) + )) + + return detectReviewPhaseType(matchedConfig?.type) +} + +const detectReviewTypeFromPhases = ( + phases: ChallengePhaseSummary[] | undefined, + targetPhaseId?: unknown, +): ReviewPhaseType | undefined => { + if (!phases?.length || targetPhaseId === undefined || targetPhaseId === null) { + return undefined + } + + const normalizedTargetPhaseId = `${targetPhaseId}` + const matchedPhase = phases.find(phase => `${phase.id}` === normalizedTargetPhaseId) + + return detectReviewPhaseType(matchedPhase?.name) +} + +const canRoleEditPhase = ( + reviewPhaseType: ReviewPhaseType | undefined, + currentPhaseReviewType: ReviewPhaseType | undefined, + normalizedRoleName: string, +): boolean => { + if (!reviewPhaseType) { + return false + } + + if (currentPhaseReviewType && currentPhaseReviewType !== reviewPhaseType) { + return false + } + + const matcher = PHASE_ROLE_MATCHERS[reviewPhaseType] + + return matcher ? matcher(normalizedRoleName) : false +} + +export interface UseReviewEditAccessArgs { + challengeInfo?: ChallengeInfo + reviewInfo?: ReviewInfo + scorecardInfo?: ScorecardInfo + myChallengeResources: BackendResource[] + isEditPhase: boolean + isReviewCompleted: boolean +} + +export interface UseReviewEditAccessResult { + isEdit: boolean + isPhaseEditAllowed: boolean + reviewPhaseType?: ReviewPhaseType +} + +/** + * Determine whether the current user can edit the review scorecard. + * Handles review, screening, checkpoint, approval, and post-mortem phase checks. + */ +export const useReviewEditAccess = ({ + challengeInfo, + reviewInfo, + scorecardInfo, + myChallengeResources, + isEditPhase, + isReviewCompleted, +}: UseReviewEditAccessArgs): UseReviewEditAccessResult => { + const reviewPhaseType = useMemo(() => { + const reviewerConfigs = challengeInfo?.reviewers ?? [] + const normalizedPhaseId = reviewInfo?.phaseId ? `${reviewInfo.phaseId}` : undefined + const normalizedScorecardId = reviewInfo?.scorecardId ? `${reviewInfo.scorecardId}` : undefined + + const metadataDerived = detectReviewPhaseType(reviewInfo?.metadata) + const phaseDerived = detectReviewTypeFromPhases( + challengeInfo?.phases as ChallengePhaseSummary[], + reviewInfo?.phaseId, + ) + const reviewerDerived = detectReviewTypeFromReviewerConfig( + reviewerConfigs as ReviewerConfig[], + normalizedPhaseId, + normalizedScorecardId, + ) + const scorecardDerived = detectReviewPhaseType(scorecardInfo?.name) + + return metadataDerived + || phaseDerived + || reviewerDerived + || scorecardDerived + || undefined + }, [ + challengeInfo?.phases, + challengeInfo?.reviewers, + reviewInfo?.metadata, + reviewInfo?.phaseId, + reviewInfo?.scorecardId, + scorecardInfo?.name, + ]) + + const currentPhaseReviewType = useMemo( + () => detectReviewPhaseType(challengeInfo?.currentPhase), + [challengeInfo?.currentPhase], + ) + + const isPhaseEditAllowed = useMemo(() => { + if (!reviewPhaseType || !reviewInfo?.resourceId) { + return false + } + + const myResource = myChallengeResources.find(resource => resource.id === reviewInfo.resourceId) + if (!myResource) { + return false + } + + const normalizedRoleName = normalizeRoleName(myResource.roleName) + + return canRoleEditPhase( + reviewPhaseType, + currentPhaseReviewType, + normalizedRoleName, + ) + }, [ + reviewPhaseType, + currentPhaseReviewType, + myChallengeResources, + reviewInfo?.resourceId, + ]) + + const isEdit = useMemo( + () => (isEditPhase || isPhaseEditAllowed) && !isReviewCompleted, + [isPhaseEditAllowed, isEditPhase, isReviewCompleted], + ) + + return { + isEdit, + isPhaseEditAllowed, + reviewPhaseType, + } +} diff --git a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx index ee77f50c5..785cc9749 100644 --- a/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx +++ b/src/apps/review/src/pages/active-review-assignements/ScorecardDetailsPage/ScorecardDetailsPage.tsx @@ -19,6 +19,8 @@ import { useFetchSubmissionInfo, useFetchSubmissionReviews, useFetchSubmissionReviewsProps, + useReviewEditAccess, + UseReviewEditAccessResult, useRole, useRoleProps, } from '../../../lib/hooks' @@ -38,214 +40,6 @@ import { useIsEditReview, useIsEditReviewProps } from '../../../lib/hooks/useIsE import styles from './ScorecardDetailsPage.module.scss' -type ReviewPhaseType = - | 'screening' - | 'checkpoint screening' - | 'checkpoint review' - | 'post-mortem' - | 'approval' - | 'review' - -const isNil = (value: unknown): value is null | undefined => value === null || value === undefined - -const POST_MORTEM_KEYWORDS = ['post-mortem', 'post mortem', 'postmortem'] - -type PhaseStringMatcher = { - match: (source: string) => boolean - phase: ReviewPhaseType -} - -const PHASE_STRING_MATCHERS: PhaseStringMatcher[] = [ - { - match: source => source.includes('checkpoint screening'), - phase: 'checkpoint screening', - }, - { - match: source => source.includes('checkpoint review'), - phase: 'checkpoint review', - }, - { - match: source => POST_MORTEM_KEYWORDS.some(keyword => source.includes(keyword)), - phase: 'post-mortem', - }, - { - match: source => source.includes('screening') && !source.includes('checkpoint'), - phase: 'screening', - }, - { - match: source => source.includes('approval'), - phase: 'approval', - }, - { - match: source => source.includes('review'), - phase: 'review', - }, -] - -const detectPhaseTypeFromString = (value: string): ReviewPhaseType | undefined => { - const normalized = value.trim() - .toLowerCase() - if (!normalized) { - return undefined - } - - for (const matcher of PHASE_STRING_MATCHERS) { - if (matcher.match(normalized)) { - return matcher.phase - } - } - - return undefined -} - -const detectPhaseTypeFromObject = (value: Record): ReviewPhaseType | undefined => { - const candidateKeys: Array = [ - 'name', - 'phaseName', - 'phase', - 'type', - ].filter(key => key in value) as Array - - for (const key of candidateKeys) { - const detected = detectReviewPhaseType(value[key]) - if (detected) { - return detected - } - } - - return undefined -} - -function detectReviewPhaseType(value?: unknown): ReviewPhaseType | undefined { - if (isNil(value)) { - return undefined - } - - if (typeof value === 'object') { - return detectPhaseTypeFromObject(value as Record) - } - - return detectPhaseTypeFromString(`${value}`) -} - -type ReviewerConfig = { - phaseId?: unknown - scorecardId?: unknown - type?: unknown -} - -type ChallengePhaseSummary = { - id?: unknown - name?: unknown -} - -type RoleMatcher = (normalizedRoleName: string) => boolean - -const normalizeRoleName = (value: unknown): string => { - if (typeof value !== 'string') { - return '' - } - - return value - .trim() - .toLowerCase() - .replace(/[^a-z]/g, '') -} - -const PHASE_ROLE_MATCHERS: Partial> = { - approval: normalizedRoleName => ( - normalizedRoleName.includes('approver') - || normalizedRoleName.includes('approval') - ), - 'checkpoint review': normalizedRoleName => normalizedRoleName === 'checkpointreviewer', - 'checkpoint screening': normalizedRoleName => normalizedRoleName === 'checkpointscreener', - 'post-mortem': normalizedRoleName => normalizedRoleName.includes('postmortem'), - review: normalizedRoleName => ( - normalizedRoleName.includes('reviewer') - && !normalizedRoleName.includes('checkpoint') - && !normalizedRoleName.includes('postmortem') - ), - screening: normalizedRoleName => ( - ( - normalizedRoleName.includes('screener') - || normalizedRoleName.includes('screening') - ) - && !normalizedRoleName.includes('checkpoint') - ), -} - -const detectReviewTypeFromReviewerConfig = ( - reviewerConfigs: ReviewerConfig[] | undefined, - normalizedPhaseId?: string, - normalizedScorecardId?: string, -): ReviewPhaseType | undefined => { - if (!reviewerConfigs?.length) { - return undefined - } - - const matchedConfig = reviewerConfigs.find(config => ( - (normalizedPhaseId && `${config.phaseId}` === normalizedPhaseId) - || (normalizedScorecardId && `${config.scorecardId}` === normalizedScorecardId) - )) - - return detectReviewPhaseType(matchedConfig?.type) -} - -const detectReviewTypeFromMetadata = ( - metadata: unknown, -): ReviewPhaseType | undefined => { - if (!metadata || typeof metadata !== 'object') { - return undefined - } - - const metadataRecord = metadata as Record - const metadataKeys = ['type', 'reviewType', 'scorecardType', 'phaseName', 'name'] - - for (const key of metadataKeys) { - const rawValue = metadataRecord[key] - if (typeof rawValue === 'string') { - const detected = detectReviewPhaseType(rawValue) - if (detected) { - return detected - } - } - } - - return undefined -} - -const detectReviewTypeFromPhases = ( - phases: ChallengePhaseSummary[] | undefined, - targetPhaseId?: unknown, -): ReviewPhaseType | undefined => { - if (!phases?.length || targetPhaseId === undefined || targetPhaseId === null) { - return undefined - } - - const normalizedTargetPhaseId = `${targetPhaseId}` - const matchedPhase = phases.find(phase => `${phase.id}` === normalizedTargetPhaseId) - - return detectReviewPhaseType(matchedPhase?.name) -} - -const canRoleEditPhase = ( - reviewPhaseType: ReviewPhaseType | undefined, - currentPhaseReviewType: ReviewPhaseType | undefined, - normalizedRoleName: string, -): boolean => { - if (!reviewPhaseType) { - return false - } - - if (currentPhaseReviewType && currentPhaseReviewType !== reviewPhaseType) { - return false - } - - const matcher = PHASE_ROLE_MATCHERS[reviewPhaseType] - - return matcher ? matcher(normalizedRoleName) : false -} - interface Props { className?: string } @@ -323,70 +117,14 @@ export const ScorecardDetailsPage: FC = (props: Props) => { [submitterLockedPhaseName], ) - const reviewPhaseType = useMemo(() => { - const reviewerConfigs = challengeInfo?.reviewers ?? [] - const normalizedPhaseId = reviewInfo?.phaseId ? `${reviewInfo.phaseId}` : undefined - const normalizedScorecardId = reviewInfo?.scorecardId ? `${reviewInfo.scorecardId}` : undefined - - const metadataDerived = detectReviewTypeFromMetadata(reviewInfo?.metadata) - const phaseDerived = detectReviewTypeFromPhases( - challengeInfo?.phases as ChallengePhaseSummary[], - reviewInfo?.phaseId, - ) - const reviewerDerived = detectReviewTypeFromReviewerConfig( - reviewerConfigs as ReviewerConfig[], - normalizedPhaseId, - normalizedScorecardId, - ) - const scorecardDerived = detectReviewPhaseType(scorecardInfo?.name) - - return metadataDerived - || phaseDerived - || reviewerDerived - || scorecardDerived - || undefined - }, [ - challengeInfo?.phases, - challengeInfo?.reviewers, - reviewInfo?.metadata, - reviewInfo?.phaseId, - reviewInfo?.scorecardId, - scorecardInfo?.name, - ]) - - const currentPhaseReviewType = useMemo( - () => detectReviewPhaseType(challengeInfo?.currentPhase), - [challengeInfo?.currentPhase], - ) - - const isPhaseEditAllowed = useMemo(() => { - if (!reviewPhaseType || !reviewInfo?.resourceId) { - return false - } - - const myResource = myChallengeResources.find(resource => resource.id === reviewInfo.resourceId) - if (!myResource) { - return false - } - - const normalizedRoleName = normalizeRoleName(myResource.roleName) - - return canRoleEditPhase( - reviewPhaseType, - currentPhaseReviewType, - normalizedRoleName, - ) - }, [ - reviewPhaseType, - currentPhaseReviewType, + const { isEdit }: UseReviewEditAccessResult = useReviewEditAccess({ + challengeInfo, + isEditPhase, + isReviewCompleted, myChallengeResources, - reviewInfo?.resourceId, - ]) - - const isEdit = useMemo( - () => (isEditPhase || isPhaseEditAllowed) && !isReviewCompleted, - [isPhaseEditAllowed, isEditPhase, isReviewCompleted], - ) + reviewInfo, + scorecardInfo, + }) const reviewBreadcrumbLabel = useMemo( () => submissionInfo?.id diff --git a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx index 0b6009b6c..b1d54e80c 100644 --- a/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx +++ b/src/apps/review/src/pages/reviews/components/ReviewViewer/ReviewViewer.tsx @@ -7,6 +7,8 @@ import { useAppNavigate, useFetchSubmissionReviews, useFetchSubmissionReviewsProps, + useReviewEditAccess, + UseReviewEditAccessResult, useRole, useRoleProps, } from '~/apps/review/src/lib/hooks' @@ -99,10 +101,14 @@ const ReviewViewer: FC = () => { [submitterLockedPhaseName], ) - const isEdit = useMemo( - () => isEditPhase && !isReviewCompleted, - [isEditPhase, isReviewCompleted], - ) + const { isEdit }: UseReviewEditAccessResult = useReviewEditAccess({ + challengeInfo, + isEditPhase, + isReviewCompleted, + myChallengeResources, + reviewInfo, + scorecardInfo, + }) /** * Cancel edit diff --git a/start.sh b/start.sh index b61274870..d05ae749c 100755 --- a/start.sh +++ b/start.sh @@ -4,9 +4,9 @@ export ESLINT_NO_DEV_ERRORS=true export HTTPS=true -export SSL_CRT_FILE=ssl-local/local.topcoder.com.pem -export SSL_KEY_FILE=ssl-local/local.topcoder.com-key.pem -export HOST=local.topcoder.com +export SSL_CRT_FILE=ssl-local/local.topcoder-dev.com+2.pem +export SSL_KEY_FILE=ssl-local/local.topcoder-dev.com+2-key.pem +export HOST=local.topcoder-dev.com export REACT_APP_HOST_ENV=${REACT_APP_HOST_ENV:-dev} export PORT=443 From a44b332598569239f8ce1c0e26a71881bbd20a1c Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Thu, 20 Nov 2025 21:54:58 +1100 Subject: [PATCH 082/125] Back out my local start.sh updates --- start.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/start.sh b/start.sh index d05ae749c..b61274870 100755 --- a/start.sh +++ b/start.sh @@ -4,9 +4,9 @@ export ESLINT_NO_DEV_ERRORS=true export HTTPS=true -export SSL_CRT_FILE=ssl-local/local.topcoder-dev.com+2.pem -export SSL_KEY_FILE=ssl-local/local.topcoder-dev.com+2-key.pem -export HOST=local.topcoder-dev.com +export SSL_CRT_FILE=ssl-local/local.topcoder.com.pem +export SSL_KEY_FILE=ssl-local/local.topcoder.com-key.pem +export HOST=local.topcoder.com export REACT_APP_HOST_ENV=${REACT_APP_HOST_ENV:-dev} export PORT=443 From 564381fe71e188a5121ba7998b6006d02113a8f3 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 21 Nov 2025 00:24:57 +0100 Subject: [PATCH 083/125] fix: moved setInitialVotesForFeedback into useEffect --- .../AiFeedbackActions/AiFeedbackActions.tsx | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx index d7e71567b..306338459 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx @@ -40,35 +40,35 @@ export const AiFeedbackActions: FC = props => { const votesArr: any[] = (props.actionType === 'runItem' ? (props.feedback?.votes) : (props.comment?.votes)) || [] - const setInitialVotesForFeedback = (): void => { - const initialUp = props.feedback?.upVotes ?? votesArr.filter(v => String(v.voteType) - .toLowerCase() - .includes('up')).length - const initialDown = props.feedback?.downVotes ?? votesArr.filter(v => String(v.voteType) - .toLowerCase() - .includes('down')).length - - const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) - setUpVotes(initialUp) - setDownVotes(initialDown) - setUserVote(myVote?.voteType ?? undefined) - } - - const setInitialVotesForComment = (): void => { - const initialUp = votesArr.filter(v => String(v.voteType) - .toLowerCase() - .includes('up')).length - const initialDown = votesArr.filter(v => String(v.voteType) - .toLowerCase() - .includes('down')).length - - const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) - setUpVotes(initialUp) - setDownVotes(initialDown) - setUserVote(myVote?.voteType ?? undefined) - } - useEffect(() => { + const setInitialVotesForFeedback = (): void => { + const initialUp = props.feedback?.upVotes ?? votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('up')).length + const initialDown = props.feedback?.downVotes ?? votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('down')).length + + const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) + setUpVotes(initialUp) + setDownVotes(initialDown) + setUserVote(myVote?.voteType ?? undefined) + } + + const setInitialVotesForComment = (): void => { + const initialUp = votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('up')).length + const initialDown = votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('down')).length + + const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) + setUpVotes(initialUp) + setDownVotes(initialDown) + setUserVote(myVote?.voteType ?? undefined) + } + if (props.actionType === 'runItem') { setInitialVotesForFeedback() } else { From 79091296716af3800e4a123c13ab29bb3e345072 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 21 Nov 2025 00:27:27 +0100 Subject: [PATCH 084/125] fix: moved setInitialVotesForFeedback into useEffect --- .../AiFeedbackActions/AiFeedbackActions.tsx | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx index 306338459..4601cbe91 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx @@ -40,35 +40,35 @@ export const AiFeedbackActions: FC = props => { const votesArr: any[] = (props.actionType === 'runItem' ? (props.feedback?.votes) : (props.comment?.votes)) || [] + const setInitialVotesForFeedback = useCallback((): void => { + const initialUp = props.feedback?.upVotes ?? votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('up')).length + const initialDown = props.feedback?.downVotes ?? votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('down')).length + + const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) + setUpVotes(initialUp) + setDownVotes(initialDown) + setUserVote(myVote?.voteType ?? undefined) + }, []) + + const setInitialVotesForComment = useCallback((): void => { + const initialUp = votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('up')).length + const initialDown = votesArr.filter(v => String(v.voteType) + .toLowerCase() + .includes('down')).length + + const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) + setUpVotes(initialUp) + setDownVotes(initialDown) + setUserVote(myVote?.voteType ?? undefined) + }, []) + useEffect(() => { - const setInitialVotesForFeedback = (): void => { - const initialUp = props.feedback?.upVotes ?? votesArr.filter(v => String(v.voteType) - .toLowerCase() - .includes('up')).length - const initialDown = props.feedback?.downVotes ?? votesArr.filter(v => String(v.voteType) - .toLowerCase() - .includes('down')).length - - const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) - setUpVotes(initialUp) - setDownVotes(initialDown) - setUserVote(myVote?.voteType ?? undefined) - } - - const setInitialVotesForComment = (): void => { - const initialUp = votesArr.filter(v => String(v.voteType) - .toLowerCase() - .includes('up')).length - const initialDown = votesArr.filter(v => String(v.voteType) - .toLowerCase() - .includes('down')).length - - const myVote = votesArr.find(v => String(v.createdBy) === String(loginUserInfo?.userId)) - setUpVotes(initialUp) - setDownVotes(initialDown) - setUserVote(myVote?.voteType ?? undefined) - } - if (props.actionType === 'runItem') { setInitialVotesForFeedback() } else { From f9c98adb0a551947a040d7ca1cd44be5bfd40001 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 21 Nov 2025 00:29:38 +0100 Subject: [PATCH 085/125] fix: review comments --- .../ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx index 4601cbe91..ab89c9263 100644 --- a/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx +++ b/src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackActions/AiFeedbackActions.tsx @@ -52,7 +52,7 @@ export const AiFeedbackActions: FC = props => { setUpVotes(initialUp) setDownVotes(initialDown) setUserVote(myVote?.voteType ?? undefined) - }, []) + }, [votesArr, props.feedback]) const setInitialVotesForComment = useCallback((): void => { const initialUp = votesArr.filter(v => String(v.voteType) @@ -66,7 +66,7 @@ export const AiFeedbackActions: FC = props => { setUpVotes(initialUp) setDownVotes(initialDown) setUserVote(myVote?.voteType ?? undefined) - }, []) + }, [votesArr]) useEffect(() => { if (props.actionType === 'runItem') { From 1cb6a4572979bf0db375c6e342e14bc6e62a0c68 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Fri, 21 Nov 2025 03:27:01 +0100 Subject: [PATCH 086/125] feat: comments implementation --- .../src/lib/assets/icons/icon-reply.svg | 3 + src/apps/review/src/lib/assets/icons/index.ts | 2 + .../FieldMarkdownEditor.tsx | 32 ++++++- .../AiFeedback/AiFeedback.tsx | 35 ++++++- .../AiFeedbackActions/AiFeedbackActions.tsx | 11 +++ .../AiFeedbackComments/AiFeedbackComment.tsx | 76 +++++++++++++++ .../AiFeedbackComments.module.scss | 5 + .../AiFeedbackComments/AiFeedbackComments.tsx | 34 ++----- .../AiFeedbackReply.module.scss | 19 ++++ .../AiFeedbackReply/AiFeedbackReply.tsx | 94 +++++++++++++++++++ .../src/lib/models/FormFeedbackReply.model.ts | 6 ++ .../src/lib/services/scorecards.service.ts | 13 +++ src/apps/review/src/lib/utils/validation.ts | 10 ++ 13 files changed, 306 insertions(+), 34 deletions(-) create mode 100644 src/apps/review/src/lib/assets/icons/icon-reply.svg create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackComments/AiFeedbackComment.tsx create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackReply/AiFeedbackReply.module.scss create mode 100644 src/apps/review/src/lib/components/Scorecard/ScorecardViewer/ScorecardQuestion/AiFeedbackReply/AiFeedbackReply.tsx create mode 100644 src/apps/review/src/lib/models/FormFeedbackReply.model.ts diff --git a/src/apps/review/src/lib/assets/icons/icon-reply.svg b/src/apps/review/src/lib/assets/icons/icon-reply.svg new file mode 100644 index 000000000..91ed863a2 --- /dev/null +++ b/src/apps/review/src/lib/assets/icons/icon-reply.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 de1e549c0..d1c6b61fd 100644 --- a/src/apps/review/src/lib/assets/icons/index.ts +++ b/src/apps/review/src/lib/assets/icons/index.ts @@ -4,6 +4,7 @@ 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 IconReply } from './icon-reply.svg' import { ReactComponent as IconRegistration } from './icon-phase-registration.svg' import { ReactComponent as IconPhaseReview } from './icon-phase-review.svg' import { ReactComponent as IconAppeal } from './icon-phase-appeal.svg' @@ -47,6 +48,7 @@ export { IconAppeal, IconAppealResponse, IconPhaseWinners, + IconReply, IconDeepseekAi, IconClock, IconPremium, diff --git a/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx b/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx index 966b321c2..ef258aef5 100644 --- a/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx +++ b/src/apps/review/src/lib/components/FieldMarkdownEditor/FieldMarkdownEditor.tsx @@ -1,9 +1,9 @@ /** * Field Markdown Editor. */ -import { FC, useCallback, useContext, useEffect, useRef } from 'react' +import { FC, useCallback, useContext, useEffect, useRef, useState } from 'react' import _ from 'lodash' -import CodeMirror from 'codemirror' +import CodeMirror, { EditorChange, EditorChangeCancellable } from 'codemirror' import EasyMDE from 'easymde' import classNames from 'classnames' import 'easymde/dist/easymde.min.css' @@ -44,6 +44,7 @@ interface Props { showBorder?: boolean disabled?: boolean uploadCategory?: string + maxCharactersAllowed?: number } const errorMessages = { fileTooLarge: @@ -149,6 +150,7 @@ type CodeMirrorType = keyof typeof stateStrategy | 'variable-2' export const FieldMarkdownEditor: FC = (props: Props) => { const elementRef = useRef(null) const easyMDE = useRef(null) + const [remainingCharacters, setRemainingCharacters] = useState((props.maxCharactersAllowed || 0) - (props.initialValue?.length || 0)) const { challengeId }: ChallengeDetailContextModel = useContext(ChallengeDetailContext) const uploadCategory: string = props.uploadCategory ?? 'general' @@ -825,8 +827,30 @@ export const FieldMarkdownEditor: FC = (props: Props) => { uploadImage: true, }) + easyMDE.current.codemirror.on("beforeChange", (cm: CodeMirror.Editor, change: EditorChangeCancellable) => { + if (change.update) { + const current = cm.getValue().length; + const incoming = change.text.join("\n").length; + const replaced = cm.indexFromPos(change.to) - cm.indexFromPos(change.from); + + const newLength = current + incoming - replaced; + + if (props.maxCharactersAllowed) { + if (newLength > props.maxCharactersAllowed) { + change.cancel(); + } + } + } + }); + easyMDE.current.codemirror.on('change', (cm: CodeMirror.Editor) => { - props.onChange?.(cm.getValue()) + if (props.maxCharactersAllowed) { + const remainingCharacters = (props.maxCharactersAllowed || 0) - cm.getValue().length + setRemainingCharacters(remainingCharacters) + props.onChange?.(cm.getValue()) + } else { + props.onChange?.(cm.getValue()) + } }) easyMDE.current.codemirror.on('blur', () => { @@ -856,7 +880,7 @@ export const FieldMarkdownEditor: FC = (props: Props) => { })} >
    {run.status === 'SUCCESS' ? ( run.workflow.id ? ( - {run.score} - + ) : run.score ) : '-'}