Conversation
…figuration PM-3851 ai review configuration
…t be editable in the UI
…nto PM-3813_ai-reviewer-configs
…nto PM-3813_ai-reviewer-configs
.circleci/config.yml
Outdated
| context: org-global | ||
| filters: &filters-dev | ||
| branches: | ||
| <<<<<<< HEAD |
There was a problem hiding this comment.
[❗❗ 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 && |
There was a problem hiding this comment.
[💡 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)) { |
There was a problem hiding this comment.
[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) |
There was a problem hiding this comment.
[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.
There was a problem hiding this comment.
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
reviewerslist at save-time; introduced AI reviewer config read-only behavior whentotalSubmissions > 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.
| 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) |
There was a problem hiding this comment.
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).
| 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) |
| 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) |
There was a problem hiding this comment.
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.
| 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) |
| 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) |
There was a problem hiding this comment.
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.
| 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) |
|
|
||
| const currentAssignedMembers = this.state.assignedMembers[reviewerIndex] || [] | ||
| const roleName = getRoleNameForReviewer(reviewer, challenge.phases) | ||
| const role = getResourceRoleByName(metadata.resourceRoles || [], roleName) |
There was a problem hiding this comment.
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.
| 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 | |
| } |
| 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) |
There was a problem hiding this comment.
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.
| const role = getResourceRoleByName(metadata.resourceRoles || [], roleName) | ||
| const resourceRoleId = role && role.id | ||
|
|
||
| if (!resourceRoleId) return [] |
There was a problem hiding this comment.
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 -.
| if (!resourceRoleId) return [] | |
| if (!resourceRoleId) return '' |
| <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'> |
There was a problem hiding this comment.
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.
| <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'> |
| 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) |
There was a problem hiding this comment.
🔴 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.
| 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) |
Was this helpful? React with 👍 or 👎 to provide feedback.
| const assignedPhaseIds = new Set( | ||
| (challenge.reviewers || []) | ||
| .filter((r, i) => i !== index && (r.isMemberReview !== false)) | ||
| .map(r => r.phaseId) | ||
| .filter(id => id !== undefined && id !== null) | ||
| ) |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
| <a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noref'> | ||
| {workflow.workflow.scorecard.name} | ||
| </a> |
There was a problem hiding this comment.
🔴 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.
| <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> | |
| ) : '-'} |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
@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) || {} |
There was a problem hiding this comment.
🔴 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
src/components/ChallengeEditor/ChallengeReviewer-Field/ReviewSummary/ReviewSummary.js
Show resolved
Hide resolved
| 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) | ||
| } |
There was a problem hiding this comment.
🚩 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
@vas3a shall we have the logic for calculation extracted to shared method?
| 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) |
| const assignedPhaseIds = new Set( | ||
| (challenge.reviewers || []) | ||
| .filter((r, i) => i !== index && (r.isMemberReview !== false)) | ||
| .map(r => r.phaseId) | ||
| .filter(id => id !== undefined && id !== null) | ||
| ) |
| <a href={`${REVIEW_APP_URL}/scorecard/${workflow.workflow.scorecard.id}`} target='_blank' rel='noopener noref'> | ||
| {workflow.workflow.scorecard.name} | ||
| </a> |
There was a problem hiding this comment.
@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) || {} |
| 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) | ||
| } |
There was a problem hiding this comment.
@vas3a shall we have the logic for calculation extracted to shared method?
| 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) |
This was never merged into dev. we tested & QAd it via feature branch.