Skip to content

PM-3906 ai review escalation#1535

Open
vas3a wants to merge 8 commits intodevfrom
PM-3906_ai-review-escalation
Open

PM-3906 ai review escalation#1535
vas3a wants to merge 8 commits intodevfrom
PM-3906_ai-review-escalation

Conversation

@vas3a
Copy link
Collaborator

@vas3a vas3a commented Mar 14, 2026

Related JIRA Ticket:

https://topcoder.atlassian.net/browse/PM-3906

What's in this PR?

image image image image image image image image

Related PR: topcoder-platform/review-api-v6#226


Open with Devin

}
}

:global(.TableCell) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Using :global(.TableCell) for styling might lead to unintended side effects if .TableCell is used elsewhere in the application. Consider scoping this style more narrowly if possible.


.statusLocked {
@extend .resultPill;
background-color: $black-5;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ accessibility]
Ensure that $black-5 and $red-100 provide sufficient contrast for accessibility. Consider using a tool to verify color contrast ratios.

.statusLocked {
@extend .resultPill;
background-color: $black-5;
color: $red-100;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ accessibility]
Ensure that $red-100 provides sufficient contrast against $black-5 for accessibility. Consider using a tool to verify color contrast ratios.

}

.resultPending {
@extend .resultPill;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ accessibility]
Ensure that $yellow-25 and $yellow-140 provide sufficient contrast for accessibility. Consider using a tool to verify color contrast ratios.


.resultPending {
@extend .resultPill;
background-color: $yellow-25;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ accessibility]
Ensure that $yellow-140 provides sufficient contrast against $yellow-25 for accessibility. Consider using a tool to verify color contrast ratios.

} from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { useSWRConfig } from 'swr'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Ensure that useSWRConfig is used correctly and that the mutate function is called with valid cache keys. Improper usage can lead to cache inconsistencies.

[aggregatedRows],
)

const escalationDecisionBySubmissionId = useMemo<Map<string, AiReviewEscalationDecision>>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The use of useMemo with a Map can lead to unexpected behavior if the map is mutated after creation. Consider using an immutable data structure or ensure the map is not modified.

[aggregatedRows, aiReviewDecisionsBySubmissionId, challengeInfo?.id],
)

const isSubmissionAiLocked = useCallback((

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[❗❗ correctness]
The isSubmissionAiLocked function relies on the status property being exactly 'AI_FAILED_REVIEW'. Ensure that this status is consistently set and checked throughout the application.

)
}

const buildEscalateAction = (): JSX.Element | undefined => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ readability]
The buildEscalateAction function contains nested conditions that could be refactored for clarity. Consider breaking it into smaller functions or using early returns to simplify the logic.

submissionId: args.submissionId,
submissionLocked: args.submissionLocked,
}),
isPaused: () => !args.challengeId && !args.submissionId && !args.aiReviewDecisionId,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The isPaused function checks if challengeId, submissionId, and aiReviewDecisionId are all undefined, which pauses the fetch. This could lead to unexpected behavior if the user intends to fetch data with only one of these parameters. Consider revising this logic to ensure it aligns with the intended use cases.

finalizedAt: string | null
createdAt: string
updatedAt: string
escalations?: AiReviewDecisionEscalation[]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ maintainability]
Consider using a non-nullable array for escalations if it is always expected to be an array, even if empty. This can simplify handling of the property by avoiding null checks.

* @param status - The status value from backend (can be number or string)
* @returns Normalized status string
*/
function normalizeSubmissionStatus(status: BackendSubmissionStatus | string | undefined): string | undefined {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
The function normalizeSubmissionStatus returns undefined if the input status is neither a number nor a string. Consider whether this behavior is intentional and if it should be handled explicitly in the calling code to avoid potential issues with unexpected undefined values.

if (params.submissionId) query.set('submissionId', params.submissionId)
if (params.aiReviewDecisionId) query.set('aiReviewDecisionId', params.aiReviewDecisionId)
if (params.status) query.set('status', params.status)
if (params.submissionLocked !== undefined) query.set('submissionLocked', String(params.submissionLocked))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[⚠️ correctness]
Using String(params.submissionLocked) to convert a boolean to a string might lead to unexpected results if params.submissionLocked is null or undefined. Consider explicitly checking for true or false values to ensure the query parameter is correctly set.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds UI + client-side wiring in the Review app to support AI review escalation/unlock flows for AI-failed submissions (PM-3906), including new API service helpers, models, and table actions/modals.

Changes:

  • Introduce AI review escalation service + hook and export them from the library.
  • Extend AI review/submission models to include escalation data and the new AI_FAILED_REVIEW status.
  • Update TableReview to display an “AI Locked” state and provide Escalate/Verify/Unlock actions via new modals.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/apps/review/src/lib/services/index.ts Exposes the new escalation service from the services barrel.
src/apps/review/src/lib/services/aiReviewEscalation.service.ts Adds list/create/update escalation API calls and cache key builder.
src/apps/review/src/lib/models/SubmissionInfo.model.ts Adds normalized submission status onto the frontend submission model.
src/apps/review/src/lib/models/BackendSubmissionStatus.enum.ts Adds AI_FAILED_REVIEW backend submission status.
src/apps/review/src/lib/models/AiReview.model.ts Adds escalation types and escalations on AiReviewDecision.
src/apps/review/src/lib/hooks/useFetchAiReviewEscalations.ts Adds SWR hook to fetch escalation decisions.
src/apps/review/src/lib/hooks/index.ts Exposes the new hook from the hooks barrel.
src/apps/review/src/lib/components/TableReview/TableReview.tsx Adds AI lock detection + new actions and renders escalation modals.
src/apps/review/src/lib/components/TableReview/TableReview.module.scss Adds styles for AI Locked pill and escalation modals; tweaks spacing.
src/apps/review/src/lib/components/TableReview/EscalationModals.tsx Implements Escalate/Verify/Unlock modal flows and API calls.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +3 to +26

export type AiReviewEscalationStatus = 'PENDING_APPROVAL' | 'APPROVED' | 'REJECTED'

export interface AiReviewDecisionEscalation {
id: string
aiReviewDecisionId: string
escalationNotes: string | null
approverNotes: string | null
status: AiReviewEscalationStatus
createdAt: string
createdBy: string | null
updatedAt: string
updatedBy: string | null
}

export interface AiReviewEscalationDecision {
aiReviewDecisionId: string
submissionId: string
challengeId: string | undefined
decisionStatus: string
submissionLocked: boolean
escalations: AiReviewDecisionEscalation[]
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vas3a please let's use only one location definintion for those

Comment on lines +98 to +104
closeEscalateDialog()
if (props.challengeId) {
await refreshChallengeReviewData(props.challengeId)
}

await props.revalidateEscalationData()
} catch (error) {
Comment on lines +373 to +374
map.set(String(resource.memberId), {
color: (resource as any).handleColor ?? undefined,
Comment on lines +3 to +26

export type AiReviewEscalationStatus = 'PENDING_APPROVAL' | 'APPROVED' | 'REJECTED'

export interface AiReviewDecisionEscalation {
id: string
aiReviewDecisionId: string
escalationNotes: string | null
approverNotes: string | null
status: AiReviewEscalationStatus
createdAt: string
createdBy: string | null
updatedAt: string
updatedBy: string | null
}

export interface AiReviewEscalationDecision {
aiReviewDecisionId: string
submissionId: string
challengeId: string | undefined
decisionStatus: string
submissionLocked: boolean
escalations: AiReviewDecisionEscalation[]
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vas3a please let's use only one location definintion for those

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants