Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,8 @@ export const FormUserAutocomplete: FC<FormUserAutocompleteProps> = (props: FormU
: ''

setSelectedOption(nextSelectedOption)
field.onChange(nextValue)
onValueChange?.(nextValue)
field.onChange(nextValue)
},
[field, onValueChange],
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ import {
import {
ReviewersField,
} from './ReviewersField'
import type { AiReviewConfigSaveController } from './ReviewersField/AiReviewTab'
import {
ReviewTypeField,
} from './ReviewTypeField'
Expand Down Expand Up @@ -1512,6 +1513,7 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
const challengeRef = useRef<Challenge | undefined>(props.challenge)
const pendingChallengeRefreshRef = useRef<Challenge | undefined>()
const defaultedDiscussionForumTypeIdRef = useRef<string | undefined>()
const aiReviewConfigSaveControllerRef = useRef<AiReviewConfigSaveController | undefined>(undefined)
const fallbackProjectId = useMemo(
() => normalizeProjectId(props.projectId) || normalizeProjectId(props.challenge?.projectId),
[
Expand Down Expand Up @@ -2927,8 +2929,14 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
throw createHandledLaunchBlockError(projectBillingAccountErrorMessage)
}

let formDataToSave = formData
if (aiReviewConfigSaveControllerRef.current) {
await aiReviewConfigSaveControllerRef.current.flushPendingSave()
formDataToSave = getValues()
}

const formDataWithProjectBilling = applyProjectBillingToChallengeFormData(
formData,
formDataToSave,
resolvedProjectBillingAccount,
)
const payload = transformFormDataToChallenge({
Expand Down Expand Up @@ -3402,12 +3410,18 @@ export const ChallengeEditorForm: FC<ChallengeEditorFormProps> = (
loginUserId,
rawPaymentCreator,
])

const reviewSection = usesManualReviewers
? (
<section className={styles.section}>
<h3 className={styles.sectionTitle}>Review</h3>
<div className={styles.block}>
<ReviewersField isReadOnly={isReadOnly} />
<ReviewersField
isReadOnly={isReadOnly}
onConfigSaveControllerReady={function (controller: AiReviewConfigSaveController | undefined) {
aiReviewConfigSaveControllerRef.current = controller
}}
/>
</div>
</section>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ import {
} from './reviewers-field.utils'
import styles from './AiReviewTab.module.scss'

export interface AiReviewConfigSaveController {
flushPendingSave: () => Promise<void>
}

interface AiReviewTabProps {
challengeId?: string
hasSubmissions?: boolean
Expand All @@ -55,6 +59,7 @@ interface AiReviewTabProps {
trackId?: string
typeId?: string
onConfigPersisted?: (config: AiReviewConfig) => void
onConfigSaveControllerReady?: (controller: AiReviewConfigSaveController | undefined) => void
}

type ConfigurationMode = 'manual' | 'template'
Expand Down Expand Up @@ -461,6 +466,7 @@ export const AiReviewTab: FC<AiReviewTabProps> = (
const onConfigRemoved = props.onConfigRemoved
const readOnly = props.hasSubmissions === true
const onConfigPersisted = props.onConfigPersisted
const onConfigSaveControllerReady = props.onConfigSaveControllerReady
const reviewers = props.reviewers
const trackId = props.trackId
const typeId = props.typeId
Expand All @@ -470,6 +476,7 @@ export const AiReviewTab: FC<AiReviewTabProps> = (
const initialConfigLookupChallengeIdRef = useRef<string | undefined>()
const onConfigPersistedRef = useRef<typeof onConfigPersisted>(onConfigPersisted)
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>()
const configSavePromiseRef = useRef<Promise<void> | undefined>(undefined)

const [availableWorkflows, setAvailableWorkflows] = useState<Workflow[]>([])
const [configuration, setConfiguration] = useState<AiReviewConfigurationDraft>(DEFAULT_CONFIGURATION)
Expand Down Expand Up @@ -615,6 +622,15 @@ export const AiReviewTab: FC<AiReviewTabProps> = (
templatesLoading,
],
)

const hasPendingConfigurationChanges = useMemo(
(): boolean => (
!!normalizedConfiguration
&& aiReviewConfigHasChanges(lastSavedConfigurationRef.current, normalizedConfiguration)
&& validationErrors.length === 0
),
[normalizedConfiguration, validationErrors],
)
const hasPersistedConfigForCurrentChallenge = useMemo(
() => (
!!normalizeReviewerText(configId)
Expand Down Expand Up @@ -999,6 +1015,72 @@ export const AiReviewTab: FC<AiReviewTabProps> = (
}
}, [configurationMode, selectedTrackName, selectedTypeName])

const persistConfiguration = useCallback(async (): Promise<void> => {
if (!normalizedConfiguration || readOnly || validationErrors.length > 0) {
return
}

setIsSaving(true)

const savePromise = (async (): Promise<void> => {
try {
const savedConfiguration = configId
? await updateAiReviewConfig(configId, normalizedConfiguration)
: await createAiReviewConfig(normalizedConfiguration)
const nextConfiguration = mapConfigToDraft(savedConfiguration)

setConfigId(savedConfiguration.id)
setConfiguration(nextConfiguration)
lastSavedConfigurationRef.current = savedConfiguration
onConfigPersistedRef.current?.(savedConfiguration)
} catch (error) {
showErrorToast(error instanceof Error
? error.message
: 'Failed to autosave AI review configuration')
} finally {
setIsSaving(false)
}
})()

configSavePromiseRef.current = savePromise
try {
await savePromise
} finally {
if (configSavePromiseRef.current === savePromise) {
configSavePromiseRef.current = undefined
}
}
}, [configId, normalizedConfiguration, readOnly, validationErrors])

const flushPendingSave = useCallback(async (): Promise<void> => {
if (configSavePromiseRef.current) {
await configSavePromiseRef.current.catch(() => undefined)
return
}

if (!hasPendingConfigurationChanges) {
return
}

if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current)
saveTimerRef.current = undefined
}

await persistConfiguration()
.catch(() => undefined)
}, [hasPendingConfigurationChanges, persistConfiguration])

useEffect(() => {
onConfigSaveControllerReady?.({
flushPendingSave,
})

return () => {
onConfigSaveControllerReady?.(undefined)
}
}, [flushPendingSave, onConfigSaveControllerReady])

useEffect(() => {
if (!normalizedChallengeId || !configurationMode || !normalizedConfiguration || readOnly) {
return undefined
Expand All @@ -1016,28 +1098,6 @@ export const AiReviewTab: FC<AiReviewTabProps> = (
}

saveTimerRef.current = setTimeout(() => {
setIsSaving(true)

const persistConfiguration = async (): Promise<void> => {
try {
const savedConfiguration = configId
? await updateAiReviewConfig(configId, normalizedConfiguration)
: await createAiReviewConfig(normalizedConfiguration)
const nextConfiguration = mapConfigToDraft(savedConfiguration)

setConfigId(savedConfiguration.id)
setConfiguration(nextConfiguration)
lastSavedConfigurationRef.current = savedConfiguration
onConfigPersistedRef.current?.(savedConfiguration)
} catch (error) {
showErrorToast(error instanceof Error
? error.message
: 'Failed to autosave AI review configuration')
} finally {
setIsSaving(false)
}
}

persistConfiguration()
.catch(() => undefined)
}, 1500)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
isAiReviewer,
syncAiConfigReviewers,
} from './reviewers-field.utils'
import AiReviewTab from './AiReviewTab'
import AiReviewTab, { AiReviewConfigSaveController } from './AiReviewTab'
import HumanReviewTab from './HumanReviewTab'
import ReviewConfigurationSummary from './ReviewConfigurationSummary'
import styles from './ReviewersField.module.scss'
Expand All @@ -41,6 +41,7 @@ type ReviewTab = 'ai' | 'human'

interface ReviewersFieldProps {
isReadOnly?: boolean
onConfigSaveControllerReady?: (controller: AiReviewConfigSaveController | undefined) => void
}

function hasReviewerChanges(
Expand Down Expand Up @@ -387,6 +388,7 @@ export const ReviewersField: FC<ReviewersFieldProps> = (props: ReviewersFieldPro
hasSubmissions={hasSubmissions}
onConfigPersisted={handleAiConfigPersisted}
onConfigRemoved={handleAiConfigRemoved}
onConfigSaveControllerReady={props.onConfigSaveControllerReady}
reviewers={reviewerRows}
trackId={trackId}
typeId={typeId}
Expand Down
Loading