From c6244676d9305842281609df7d005165bf82be42 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 28 Oct 2025 17:26:10 +0200 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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 )}