Skip to content

Commit 5060ce1

Browse files
authored
Merge pull request #1277 from topcoder-platform/PM-1905_review-details-page
PM-1905 - ai reviews
2 parents 5f34220 + 9c25b0c commit 5060ce1

File tree

15 files changed

+649
-46
lines changed

15 files changed

+649
-46
lines changed
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
@import '@libs/ui/styles/includes';
2+
3+
.wrap {
4+
font-family: "Nunito Sans", sans-serif;
5+
max-width: 100%;
6+
overflow: hidden;
7+
}
8+
9+
.reviewsTable {
10+
width: 100%;
11+
border-collapse: collapse;
12+
13+
th {
14+
border-top: 1px solid #A8A8A8;
15+
font-weight: bold;
16+
background: #E0E4E8;
17+
}
18+
19+
th, td {
20+
text-align: left;
21+
font-size: 14px;
22+
padding: $sp-2 $sp-4;
23+
border-bottom: 1px solid #A8A8A8;
24+
}
25+
26+
.scoreCol {
27+
text-align: right;
28+
}
29+
}
30+
31+
.aiReviewer {
32+
display: flex;
33+
align-items: center;
34+
gap: $sp-2;
35+
36+
.icon {
37+
display: flex;
38+
align-items: center;
39+
flex: 0 0;
40+
}
41+
42+
.workflowName {
43+
> div:first-child {
44+
max-width: 200px;
45+
white-space: nowrap;
46+
overflow: hidden;
47+
text-overflow: ellipsis;
48+
}
49+
}
50+
}
51+
52+
.result {
53+
display: flex;
54+
align-items: center;
55+
gap: $sp-2;
56+
57+
:global(.icon) {
58+
color: #C1294F;
59+
&:global(.passed) {
60+
color: $teal-160;
61+
}
62+
&:global(.pending) {
63+
color: $black-20;
64+
display: flex;
65+
width: 16px;
66+
height: 16px;
67+
border-radius: 15px;
68+
border: 1px solid;
69+
align-items: center;
70+
justify-content: center;
71+
}
72+
}
73+
}
74+
75+
.mobileCard {
76+
border-top: 1px solid #A8A8A8;
77+
margin-top: $sp-2;
78+
}
79+
80+
.mobileRow {
81+
display: flex;
82+
padding-top: $sp-2;
83+
padding-left: $sp-4;
84+
padding-right: $sp-4;
85+
> * {
86+
flex: 1 1 50%;
87+
}
88+
}
89+
.label {
90+
font-weight: bold;
91+
}
92+
.value {
93+
94+
svg {
95+
display: inline;
96+
vertical-align: middle;
97+
margin-right: $sp-1;
98+
}
99+
100+
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { FC, useMemo } from 'react'
2+
import moment from 'moment'
3+
4+
import { CheckIcon, MinusCircleIcon } from '@heroicons/react/outline'
5+
import { useWindowSize, WindowSize } from '~/libs/shared'
6+
import { IconOutline, Tooltip } from '~/libs/ui'
7+
8+
import {
9+
AiWorkflowRun,
10+
AiWorkflowRunsResponse,
11+
AiWorkflowRunStatus,
12+
useFetchAiWorkflowsRuns,
13+
useRolePermissions,
14+
UseRolePermissionsResult,
15+
} from '../../hooks'
16+
import { IconAiReview } from '../../assets/icons'
17+
import { TABLE_DATE_FORMAT } from '../../../config/index.config'
18+
import { BackendSubmission } from '../../models'
19+
20+
import styles from './AiReviewsTable.module.scss'
21+
22+
interface AiReviewsTableProps {
23+
submission: Pick<BackendSubmission, 'id'|'virusScan'>
24+
reviewers: { aiWorkflowId: string }[]
25+
}
26+
27+
const aiRunInProgress = (aiRun: Pick<AiWorkflowRun, 'status'>): boolean => [
28+
AiWorkflowRunStatus.INIT,
29+
AiWorkflowRunStatus.QUEUED,
30+
AiWorkflowRunStatus.DISPATCHED,
31+
AiWorkflowRunStatus.IN_PROGRESS,
32+
].includes(aiRun.status)
33+
34+
const aiRunFailed = (aiRun: Pick<AiWorkflowRun, 'status'>): boolean => [
35+
AiWorkflowRunStatus.FAILURE,
36+
AiWorkflowRunStatus.CANCELLED,
37+
].includes(aiRun.status)
38+
39+
const AiReviewsTable: FC<AiReviewsTableProps> = props => {
40+
const aiWorkflowIds = useMemo(() => props.reviewers.map(r => r.aiWorkflowId), [props.reviewers])
41+
const { runs, isLoading }: AiWorkflowRunsResponse = useFetchAiWorkflowsRuns(props.submission.id, aiWorkflowIds)
42+
43+
const windowSize: WindowSize = useWindowSize()
44+
const isTablet = useMemo(
45+
() => (windowSize.width ?? 0) <= 984,
46+
[windowSize.width],
47+
)
48+
const { isAdmin }: UseRolePermissionsResult = useRolePermissions()
49+
50+
const aiRuns = useMemo(() => [
51+
...runs,
52+
{
53+
completedAt: (props.submission as BackendSubmission).submittedDate,
54+
id: '-1',
55+
score: props.submission.virusScan === true ? 100 : 0,
56+
status: AiWorkflowRunStatus.SUCCESS,
57+
workflow: {
58+
description: '',
59+
name: 'Virus Scan',
60+
},
61+
} as AiWorkflowRun,
62+
].filter(r => isAdmin || !aiRunFailed(r)), [runs, props.submission])
63+
64+
if (isTablet) {
65+
return (
66+
<div className={styles.wrap}>
67+
{!runs.length && isLoading && (
68+
<div className={styles.mobileLoading}>Loading...</div>
69+
)}
70+
71+
{aiRuns.map(run => (
72+
<div key={run.id} className={styles.mobileCard}>
73+
<div className={styles.mobileRow}>
74+
<div className={styles.label}>Reviewer</div>
75+
<div className={styles.value}>
76+
<span className={styles.icon}>
77+
<IconAiReview />
78+
</span>
79+
<span className={styles.workflowName} title={run.workflow.name}>
80+
{run.workflow.name}
81+
</span>
82+
</div>
83+
</div>
84+
85+
<div className={styles.mobileRow}>
86+
<div className={styles.label}>Review Date</div>
87+
<div className={styles.value}>
88+
{run.status === 'SUCCESS'
89+
? moment(run.completedAt)
90+
.local()
91+
.format(TABLE_DATE_FORMAT)
92+
: '-'}
93+
</div>
94+
</div>
95+
96+
<div className={styles.mobileRow}>
97+
<div className={styles.label}>Score</div>
98+
<div className={styles.value}>
99+
{run.status === 'SUCCESS' ? (
100+
run.workflow.scorecard ? (
101+
<a href={`/scorecard/${run.workflow.scorecard.id}`}>{run.score}</a>
102+
) : run.score
103+
) : '-'}
104+
</div>
105+
</div>
106+
107+
<div className={styles.mobileRow}>
108+
<div className={styles.label}>Result</div>
109+
<div className={`${styles.value} ${styles.resultCol}`}>
110+
{run.status === 'SUCCESS' && (
111+
<div className={styles.result}>
112+
{run.score >= (run.workflow.scorecard?.minimumPassingScore ?? 0) ? (
113+
<>
114+
<CheckIcon className='icon icon-xl passed' />
115+
{' '}
116+
Passed
117+
</>
118+
) : (
119+
<>
120+
<MinusCircleIcon className='icon icon-xl' />
121+
{' '}
122+
Failed
123+
</>
124+
)}
125+
</div>
126+
)}
127+
{aiRunInProgress(run) && (
128+
<div className={styles.result}>
129+
<span className='icon pending'>
130+
<IconOutline.MinusIcon className='icon-sm' />
131+
</span>
132+
{' '}
133+
To be filled
134+
</div>
135+
)}
136+
{aiRunFailed(run) && (
137+
<div className={styles.result}>
138+
<span className='icon'>
139+
<IconOutline.XCircleIcon className='icon-xl' />
140+
</span>
141+
</div>
142+
)}
143+
</div>
144+
</div>
145+
</div>
146+
))}
147+
</div>
148+
)
149+
}
150+
151+
return (
152+
<div className={styles.wrap}>
153+
<table className={styles.reviewsTable}>
154+
<thead>
155+
<tr>
156+
<th>AI Reviewer</th>
157+
<th>Review Date</th>
158+
<th>Score</th>
159+
<th>Result</th>
160+
</tr>
161+
</thead>
162+
163+
<tbody>
164+
{!runs.length && isLoading && (
165+
<tr>
166+
<td colSpan={4}>Loading...</td>
167+
</tr>
168+
)}
169+
170+
{aiRuns.map(run => (
171+
<tr key={run.id}>
172+
<td>
173+
<div className={styles.aiReviewer}>
174+
<span className={styles.icon}>
175+
<IconAiReview />
176+
</span>
177+
<span className={styles.workflowName} title={run.workflow.name}>
178+
<Tooltip content={run.workflow.name} triggerOn='hover'>
179+
{run.workflow.name}
180+
</Tooltip>
181+
</span>
182+
</div>
183+
</td>
184+
<td>
185+
{run.status === 'SUCCESS' && (
186+
moment(run.completedAt)
187+
.local()
188+
.format(TABLE_DATE_FORMAT)
189+
)}
190+
</td>
191+
<td className={styles.scoreCol}>
192+
{run.status === 'SUCCESS' ? (
193+
run.workflow.scorecard ? (
194+
<a href={`/scorecard/${run.workflow.scorecard.id}`}>{run.score}</a>
195+
) : run.score
196+
) : '-'}
197+
</td>
198+
<td className={styles.resultCol}>
199+
{run.status === 'SUCCESS' && (
200+
<div className={styles.result}>
201+
{run.score >= (run.workflow.scorecard?.minimumPassingScore ?? 0)
202+
? (
203+
<>
204+
<CheckIcon className='icon icon-xl passed' />
205+
{' '}
206+
Passed
207+
</>
208+
)
209+
: (
210+
<>
211+
<MinusCircleIcon className='icon icon-xl' />
212+
{' '}
213+
Failed
214+
</>
215+
)}
216+
</div>
217+
)}
218+
{aiRunInProgress(run) && (
219+
<div className={styles.result}>
220+
<span className='icon pending'>
221+
<IconOutline.MinusIcon className='icon-sm' />
222+
</span>
223+
{' '}
224+
To be filled
225+
</div>
226+
)}
227+
{aiRunFailed(run) && (
228+
<div className={styles.result}>
229+
<span className='icon'>
230+
<IconOutline.XCircleIcon className='icon-xl' />
231+
</span>
232+
</div>
233+
)}
234+
</td>
235+
</tr>
236+
))}
237+
</tbody>
238+
</table>
239+
</div>
240+
)
241+
}
242+
243+
export default AiReviewsTable
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default as AiReviewsTable } from './AiReviewsTable'

src/apps/review/src/lib/components/ChallengeDetailsContent/ChallengeDetailsContent.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ interface SubmissionTabParams {
126126
isDownloadingSubmission: useDownloadSubmissionProps['isLoading']
127127
downloadSubmission: useDownloadSubmissionProps['downloadSubmission']
128128
isActiveChallenge: boolean
129+
aiReviewers: { aiWorkflowId: string }[]
129130
}
130131

131132
const renderSubmissionTab = ({
@@ -137,6 +138,7 @@ const renderSubmissionTab = ({
137138
isDownloadingSubmission,
138139
downloadSubmission,
139140
isActiveChallenge,
141+
aiReviewers,
140142
}: SubmissionTabParams): JSX.Element => {
141143
const isSubmissionTab = selectedTabNormalized === 'submission'
142144
const isTopgearSubmissionTab = selectedTabNormalized === 'topgearsubmission'
@@ -155,6 +157,7 @@ const renderSubmissionTab = ({
155157
if (canShowSubmissionList) {
156158
return (
157159
<TabContentSubmissions
160+
aiReviewers={aiReviewers}
158161
submissions={visibleSubmissions}
159162
isLoading={isLoadingSubmission}
160163
isDownloading={isDownloadingSubmission}
@@ -335,6 +338,9 @@ export const ChallengeDetailsContent: FC<Props> = (props: Props) => {
335338

336339
if (SUBMISSION_TAB_KEYS.has(selectedTabNormalized)) {
337340
return renderSubmissionTab({
341+
aiReviewers: (
342+
challengeInfo?.reviewers?.filter(r => !!r.aiWorkflowId) as { aiWorkflowId: string }[]
343+
) ?? [],
338344
downloadSubmission,
339345
isActiveChallenge: props.isActiveChallenge,
340346
isDownloadingSubmission,

src/apps/review/src/lib/components/ChallengeDetailsContent/TabContentSubmissions.module.scss

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,13 @@
126126
pointer-events: none;
127127
}
128128
}
129+
130+
.aiReviewerRow {
131+
@include ltelg {
132+
tr:has(&) {
133+
td:first-child {
134+
display: none;
135+
}
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)