Skip to content

PM-3813 ai reviewer configs#1737

Open
vas3a wants to merge 36 commits intodevelopfrom
PM-3813_ai-reviewer-configs
Open

PM-3813 ai reviewer configs#1737
vas3a wants to merge 36 commits intodevelopfrom
PM-3813_ai-reviewer-configs

Conversation

@vas3a
Copy link
Collaborator

@vas3a vas3a commented Mar 11, 2026

This was never merged into dev. we tested & QAd it via feature branch.


Open with Devin

vas3a and others added 30 commits February 17, 2026 13:38
…figuration

PM-3851 ai review configuration
@vas3a vas3a requested review from jmgasper and kkartunov March 11, 2026 11:47
context: org-global
filters: &filters-dev
branches:
<<<<<<< HEAD

Choose a reason for hiding this comment

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

[❗❗ correctness]
There is a merge conflict marker in the code. This needs to be resolved before merging. Ensure that the correct branches are specified in the only filter.

}

return scorecards.filter(scorecard => (
scorecard &&

Choose a reason for hiding this comment

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

[💡 style]
The check scorecard && is redundant here because scorecards is already defaulted to an empty array, and filter will skip undefined elements.

incrementalCoefficient: defaultReviewer.incrementalCoefficient
})

if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {

Choose a reason for hiding this comment

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

[⚠️ correctness]
The condition updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false) is potentially problematic if updatedReviewers[index] is undefined. Consider ensuring updatedReviewers[index] is always defined before this point or handle the case where it might be undefined.

))
const fallbackScorecardId = hasDefaultScorecard
? defaultScorecardId
: normalizeIdValue(scorecardsForPhase[0] && scorecardsForPhase[0].id)

Choose a reason for hiding this comment

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

[⚠️ correctness]
The fallback logic for fallbackScorecardId assumes that scorecardsForPhase always has at least one element. Consider adding a check to ensure scorecardsForPhase is not empty before accessing scorecardsForPhase[0].id.

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

This PR introduces AI Review configuration support in the Challenge Editor, including API service wrappers, a new tabbed reviewer configuration UI (Human vs AI), and a read-only AI reviewer config mode once submissions exist.

Changes:

  • Added services/helpers for AI review templates and AI review configs (CRUD + validation/diff helpers).
  • Refactored the reviewer configuration UI into Human Review + AI Review tabs, and added a read-only “Review Summary” view.
  • Synced AI review config workflows into the challenge reviewers list at save-time; introduced AI reviewer config read-only behavior when totalSubmissions > 0.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
src/util/tc.js Adds getRoleNameForReviewer helper for mapping phases to resource role names.
src/services/aiReviewTemplates.js Adds API call to fetch AI review templates with filters.
src/services/aiReviewConfigs.js Adds CRUD API calls for AI review configs and basic validation.
src/services/aiReviewConfigHelpers.js Adds client-side config validation + diff utilities for autosave/switching modes.
src/containers/ChallengeEditor/index.js Passes totalSubmissions down to ChallengeEditor component routes.
src/config/constants.js Adds TC_REVIEWS_API_BASE_URL env constant.
src/components/ChallengeEditor/TextEditor-Field/index.js Plumbs aiReviewerReadOnly prop down to reviewer field.
src/components/ChallengeEditor/index.js Fetches AI config during save to sync config workflows into challenge.reviewers; enforces AI read-only when submissions exist.
src/components/ChallengeEditor/ChallengeReviewer-Field/shared.module.scss Introduces shared styles used across the reviewer tabs.
src/components/ChallengeEditor/ChallengeReviewer-Field/ReviewSummary/ReviewSummary.module.scss Styles for the new read-only review summary UI.
src/components/ChallengeEditor/ChallengeReviewer-Field/ReviewSummary/ReviewSummary.js Adds read-only summary view combining human reviewers, AI config, and a flow/cost summary.
src/components/ChallengeEditor/ChallengeReviewer-Field/ReviewSummary/index.js Barrel export for ReviewSummary.
src/components/ChallengeEditor/ChallengeReviewer-Field/index.js Refactors reviewer configuration into tabbed Human vs AI UI + summary view in read-only.
src/components/ChallengeEditor/ChallengeReviewer-Field/HumanReviewTab.js New Human Review tab with reviewer editing and member assignment.
src/components/ChallengeEditor/ChallengeReviewer-Field/ChallengeReviewer-Field.module.scss Adds tab styling and imports shared styles.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiWorkflowCard.module.scss Styles for AI workflow cards shown in initial AI state.
src/components/ChallengeEditor/ChallengeReviewer-Field/AIWorkflowCard.js New AI workflow card component for displaying assigned workflows.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/views/TemplateConfigurationView.js Template-based AI config view (selection + summary).
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/views/ManualConfigurationView.js Manual AI config view with editable workflows/settings and change confirmation.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/views/InitialStateView.js Initial AI view when AI reviewers exist but no AI config exists.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/utils.js Adds isAIReviewer helper used across AI UI.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/index.js Barrel export for AiReviewTab.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/hooks/useTemplateManager.js Hook to load/manage AI templates and current selection.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/hooks/useConfigurationState.js Hook managing AI config state + debounced autosave to backend.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/components/WeightValidationCard.js UI validation for workflow weights totaling 100%.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/components/SummarySection.js Displays AI config summary cards.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/components/ReviewSettingsSection.js UI for AI mode/threshold/autofinalize settings.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/components/ManualWorkflowCard.js Editable workflow card for manual mode.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/components/ConfigurationSourceSelector.js UI to switch between template/manual (with confirmation flows).
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/components/AiWorkflowsTableListing.js Table view of workflows in read-only/template view.
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/AiReviewTab.module.scss Styles for AI review tab UI (initial/template/manual).
src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/AiReviewTab.js Main AI tab orchestrating views + config removal and reviewer removal.
src/components/Buttons/OutlineButton/Outline.module.scss Adds .minWidth style for OutlineButton.
src/components/Buttons/OutlineButton/index.js Adds minWidth prop support.
.circleci/config.yml Adds a feature branch name to dev build branch filters.

💡 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 +203 to +209
const { deleteResource } = this.props
const removedMembers = currentAssigned.slice(newCount)
const newAssigned = currentAssigned.slice(0, newCount)

for (const member of removedMembers) {
if (member && member.resourceId) {
await deleteResource(member.resourceId)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

deleteResource is the Redux action from src/actions/challenges.js and expects (challengeId, roleId, memberHandle). Calling it with member.resourceId will dispatch an invalid delete and likely fail (and leave removed assignments in backend). Track the reviewer roleId/handle and call deleteResource(challenge.id, roleId, handle) for each removed member instead (or add a dedicated action for deleting by resource id if that’s what you intend).

Suggested change
const { deleteResource } = this.props
const removedMembers = currentAssigned.slice(newCount)
const newAssigned = currentAssigned.slice(0, newCount)
for (const member of removedMembers) {
if (member && member.resourceId) {
await deleteResource(member.resourceId)
const { deleteResource, challenge } = this.props
const removedMembers = currentAssigned.slice(newCount)
const newAssigned = currentAssigned.slice(0, newCount)
for (const member of removedMembers) {
if (member && member.roleId && member.handle) {
await deleteResource(challenge.id, member.roleId, member.handle)

Copilot uses AI. Check for mistakes.
Comment on lines +346 to +353
if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[index].scorecardId)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

This block uses updatedReviewers[index] / updatedReviewers[index].scorecardId, but index is the filtered human-reviewer index, not the index into updatedReviewers (which is the full reviewers array). This will read/update the wrong reviewer when AI reviewers exist. Use actualIndex consistently in these checks/reads.

Suggested change
if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[index].scorecardId)
if (updatedReviewers[actualIndex] && (updatedReviewers[actualIndex].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[actualIndex].scorecardId)

Copilot uses AI. Check for mistakes.
Comment on lines +346 to +353
if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[index].scorecardId)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

updatedReviewers[index].scorecardId uses the filtered index (human reviewers) against the full reviewers array, which can point at the wrong reviewer when AI reviewers are present. Use updatedReviewers[actualIndex] here (and anywhere else in this method) to avoid mis-validating scorecards and overwriting the wrong reviewer.

Suggested change
if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[index].scorecardId)
if (updatedReviewers[actualIndex] && (updatedReviewers[actualIndex].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[actualIndex].scorecardId)

Copilot uses AI. Check for mistakes.

const currentAssignedMembers = this.state.assignedMembers[reviewerIndex] || []
const roleName = getRoleNameForReviewer(reviewer, challenge.phases)
const role = getResourceRoleByName(metadata.resourceRoles || [], roleName)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

getResourceRoleByName can return null/undefined when roles metadata isn’t loaded or the role name doesn’t exist. The current code uses role.id unconditionally, which will throw and break assignment changes. Add a guard and surface a user-facing error when the role can’t be resolved.

Suggested change
const role = getResourceRoleByName(metadata.resourceRoles || [], roleName)
const role = getResourceRoleByName(metadata.resourceRoles || [], roleName)
// If a new option is being assigned but the corresponding role cannot be resolved,
// show a user-facing error and abort to avoid runtime errors from accessing role.id.
if (option && !role) {
if (typeof window !== 'undefined' && window.alert) {
window.alert('Unable to assign reviewer because the required role information is not available. Please reload the page and try again.');
}
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +41
const removeAIReviewer = useCallback((index) => {
const allChallengeReviewers = challenge.reviewers || []
// Map the AI reviewer index to the actual index in the full reviewers array
const reviewerToRemove = aiReviewers[index]
const actualIndex = allChallengeReviewers.indexOf(reviewerToRemove)
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

removeAIReviewer closes over aiReviewers but it is not included in the useCallback dependency array, so it can remove the wrong reviewer after the reviewers list changes. Add aiReviewers (or derive reviewerToRemove from challenge.reviewers inside the callback) to keep it in sync.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

needs verification

const role = getResourceRoleByName(metadata.resourceRoles || [], roleName)
const resourceRoleId = role && role.id

if (!resourceRoleId) return []
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

Returning an empty array here prevents the UI fallback (|| '-') from working because [] is truthy in JS/React. Return an empty string (or null) when no role/resources are found so the caller can reliably render -.

Suggested change
if (!resourceRoleId) return []
if (!resourceRoleId) return ''

Copilot uses AI. Check for mistakes.
<td>{workflow.workflowName || (workflow.workflow && workflow.workflow.name) || workflow.workflowId || '-'}</td>
<td>{workflow.weightPercent === '-' ? '-' : `${workflow.weightPercent}%`}</td>
<td>
<a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noref'>
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

target='_blank' links should include rel='noopener noreferrer' (currently noopener noref), otherwise the noreferrer protection won’t apply and this is a known reverse-tabnabbing vector.

Suggested change
<a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noref'>
<a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noreferrer'>

Copilot uses AI. Check for mistakes.
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 6 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +346 to +353
if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[index].scorecardId)

Choose a reason for hiding this comment

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

🔴 HumanReviewTab.updateReviewer uses filtered index instead of actualIndex to access full reviewers array

In updateReviewer, the code correctly computes actualIndex (line 334) to map the human-reviewer-filtered index to the full challenge.reviewers array. However, on lines 346 and 353, it uses the filtered index instead of actualIndex to access updatedReviewers (which is a copy of the full reviewers array including AI reviewers). When AI reviewers exist in the array before the target human reviewer, index and actualIndex will differ, causing the code to check and read the scorecard from the wrong reviewer. For example, with reviewers [AI, Human0, Human1], editing Human1 (index=1) would check updatedReviewers[1] (Human0) instead of updatedReviewers[2] (Human1), leading to incorrect scorecard auto-selection on phase change.

Suggested change
if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[index].scorecardId)
if (updatedReviewers[actualIndex] && (updatedReviewers[actualIndex].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[actualIndex].scorecardId)
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +471 to +476
const assignedPhaseIds = new Set(
(challenge.reviewers || [])
.filter((r, i) => i !== index && (r.isMemberReview !== false))
.map(r => r.phaseId)
.filter(id => id !== undefined && id !== null)
)

Choose a reason for hiding this comment

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

🔴 HumanReviewTab phase filter excludes wrong reviewer due to index mismatch with full reviewers array

In renderReviewerForm, the assignedPhaseIds filter at line 472-473 iterates over challenge.reviewers (the full array including AI reviewers) and excludes items where i !== index. But index is the human-reviewer-filtered index, while i is the index in the full challenge.reviewers array. When AI reviewers are present, this excludes the wrong reviewer from the phase-assignment check, potentially hiding valid phase options or showing phases that should be excluded.

Prompt for agents
In src/components/ChallengeEditor/ChallengeReviewer-Field/HumanReviewTab.js, in renderReviewerForm (around line 471-476), the assignedPhaseIds computation filters challenge.reviewers using i !== index, but index is the human-filtered index while i iterates over the full reviewers array (including AI reviewers). You need to compute the actual index of the current reviewer in the full challenge.reviewers array first (similar to how updateReviewer does it with indexOf), then use that actualIndex for the exclusion filter. Change the filter from (r, i) => i !== index to exclude the actual reviewer object, for example by comparing against the reviewer object reference or using the actual index in the full array.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +245 to +247
<a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noref'>
{workflow.workflow.scorecard.name}
</a>

Choose a reason for hiding this comment

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

🔴 ReviewSummary crashes accessing workflow.workflow.scorecard on API config workflows that lack nested workflow property

At line 245, the code accesses workflow.workflow.scorecard.id and workflow.workflow.scorecard.name without null checks. This code runs for both aiConfiguration.workflows (from the API) and legacyAIWorkflows. API config workflow objects have the shape {workflowId, weightPercent, isGating} — they do not have a nested workflow property. This causes a crash: Cannot read properties of undefined (reading 'scorecard'). For legacy workflows (lines 122-125), if workflow is found but workflow.scorecard is undefined, workflow.scorecard.name also crashes.

Suggested change
<a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noref'>
{workflow.workflow.scorecard.name}
</a>
{workflow.workflow && workflow.workflow.scorecard && workflow.workflow.scorecard.id ? (
<a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noreferrer'>
{workflow.workflow.scorecard.name}
</a>
) : '-'}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

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

@vas3a this is why there is no title on the AI Workflows in the UI, probably. Please check...

<tbody>
{workflows.map((workflow, index) => {
const isAssigned = (challenge.reviewers || []).some(r => r.aiWorkflowId === workflow.workflowId)
const workflowDetails = workflows.find(w => w.id === workflow.workflowId) || {}

Choose a reason for hiding this comment

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

🔴 AiWorkflowsTableListing looks up workflow names from itself, always returning empty object

At line 26, the component searches workflows.find(w => w.id === workflow.workflowId) — but workflows is the configuration's workflow array with items shaped {workflowId, weightPercent, isGating} (per PropTypes on lines 71-77). These items have no id property, only workflowId. So the find always returns undefined, workflowDetails is always {}, and workflowDetails.name on line 32 renders nothing. The component needs a separate prop with the full available workflow metadata to resolve names.

Prompt for agents
In src/components/ChallengeEditor/ChallengeReviewer-Field/AiReviewerTab/components/AiWorkflowsTableListing.js, the component needs access to the full available workflows metadata (from metadata.workflows) to look up workflow names. Currently it only receives configuration.workflows which contain {workflowId, weightPercent, isGating} but no id or name fields. Add a new prop (e.g. availableWorkflows) that receives the full workflow objects from metadata, and change line 26 from workflows.find(w => w.id === workflow.workflowId) to availableWorkflows.find(w => w.id === workflow.workflowId). Update both call sites in TemplateConfigurationView.js (line 121) and ManualConfigurationView.js (line 101) to pass the availableWorkflows prop.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

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

@vas3a this too - no titles

Comment on lines +44 to +67
const calculateReviewCost = () => {
return humanReviewers
.reduce((sum, r) => {
const memberCount = parseInt(r.memberReviewerCount) || 1
const baseAmount = parseFloat(r.fixedAmount) || 0
const prizeSet = challenge.prizeSets && challenge.prizeSets[0]
const prizeValue = prizeSet && prizeSet.prizes && prizeSet.prizes[0] && prizeSet.prizes[0].value
const prizeAmount = prizeSet
? parseFloat(prizeValue) || 0
: 0

const estimatedSubmissions = 2
const baseCoefficient = parseFloat(r.baseCoefficient) || 0.13
const incrementalCoefficient = parseFloat(r.incrementalCoefficient) || 0.05

const calculatedCost = memberCount * (
baseAmount + (prizeAmount * baseCoefficient) +
(prizeAmount * estimatedSubmissions * incrementalCoefficient)
)

return sum + calculatedCost
}, 0)
.toFixed(2)
}

Choose a reason for hiding this comment

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

🚩 Review cost formula differs between HumanReviewTab and ReviewSummary

HumanReviewTab (HumanReviewTab.js:648) computes cost as fixedAmount + (baseCoefficient + incrementalCoefficient * submissions) * prize, while ReviewSummary (ReviewSummary.js:59-62) computes memberCount * (baseAmount + (prize * baseCoefficient) + (prize * submissions * incrementalCoefficient)). These are algebraically different formulas for the same concept. For example, with fixedAmount=10, base=0.13, incr=0.05, prize=100, subs=2, count=2: HumanReviewTab gives (10 + (0.13 + 0.05*2)*100) * 2 = 66, ReviewSummary gives 2 * (10 + 100*0.13 + 100*2*0.05) = 72. Also, ReviewSummary uses challenge.prizeSets[0] (first prize set) rather than finding the PLACEMENT prize set specifically, which could pick the wrong prize set.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

Choose a reason for hiding this comment

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

@vas3a shall we have the logic for calculation extracted to shared method?

Copy link
Contributor

@kkartunov kkartunov left a comment

Choose a reason for hiding this comment

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

@vas3a few things to check. Other than that great work!

Comment on lines +346 to +353
if (updatedReviewers[index] && (updatedReviewers[index].isMemberReview !== false)) {
const { metadata = {} } = this.props
const scorecardsForPhase = getScorecardsForPhase(
metadata.scorecards || [],
challenge.phases || [],
value
)
const currentScorecardId = normalizeIdValue(updatedReviewers[index].scorecardId)
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +471 to +476
const assignedPhaseIds = new Set(
(challenge.reviewers || [])
.filter((r, i) => i !== index && (r.isMemberReview !== false))
.map(r => r.phaseId)
.filter(id => id !== undefined && id !== null)
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Comment on lines +245 to +247
<a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noref'>
{workflow.workflow.scorecard.name}
</a>
Copy link
Contributor

Choose a reason for hiding this comment

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

@vas3a this is why there is no title on the AI Workflows in the UI, probably. Please check...

<tbody>
{workflows.map((workflow, index) => {
const isAssigned = (challenge.reviewers || []).some(r => r.aiWorkflowId === workflow.workflowId)
const workflowDetails = workflows.find(w => w.id === workflow.workflowId) || {}
Copy link
Contributor

Choose a reason for hiding this comment

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

@vas3a this too - no titles

Comment on lines +44 to +67
const calculateReviewCost = () => {
return humanReviewers
.reduce((sum, r) => {
const memberCount = parseInt(r.memberReviewerCount) || 1
const baseAmount = parseFloat(r.fixedAmount) || 0
const prizeSet = challenge.prizeSets && challenge.prizeSets[0]
const prizeValue = prizeSet && prizeSet.prizes && prizeSet.prizes[0] && prizeSet.prizes[0].value
const prizeAmount = prizeSet
? parseFloat(prizeValue) || 0
: 0

const estimatedSubmissions = 2
const baseCoefficient = parseFloat(r.baseCoefficient) || 0.13
const incrementalCoefficient = parseFloat(r.incrementalCoefficient) || 0.05

const calculatedCost = memberCount * (
baseAmount + (prizeAmount * baseCoefficient) +
(prizeAmount * estimatedSubmissions * incrementalCoefficient)
)

return sum + calculatedCost
}, 0)
.toFixed(2)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

@vas3a shall we have the logic for calculation extracted to shared method?

Comment on lines +37 to +41
const removeAIReviewer = useCallback((index) => {
const allChallengeReviewers = challenge.reviewers || []
// Map the AI reviewer index to the actual index in the full reviewers array
const reviewerToRemove = aiReviewers[index]
const actualIndex = allChallengeReviewers.indexOf(reviewerToRemove)
Copy link
Contributor

Choose a reason for hiding this comment

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

needs verification

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