Skip to content

Commit 2fe6451

Browse files
feat(automatedQA): integrate AI answer generation api DEV-739 (#6735)
### 📣 Summary This PR implements the flow for activating the AI response generation feature and calling the actual generation endpoint ### 💭 Notes - For this to work some changes were necessary in the current `TabAnalysis` flow, so we could have access to the automated feature as well. - Now the `manual_qual` feature is created whenever a question is set and it's still not there. In the common flow it will happen once the first question is added. I chose that path instead of automatically creating the feature when `TabAnalysis` was opened, which i considered another viable option. This way we won´t create the feature unless the user really adds a qaQuestion. - There's a verification logic to decide whether to create the advanced feature for the AI generation, to patch it or do nothing if all qaQuestions are already set for it. This is important in the case of new questions being added. - Tags and Notes questions don't have `Generate with AI` enabled since it's not supported for them - The actual API call to generate the answer is working, but the generated data is not being parsed and displayed in this PR. We will take care of this in a follow up work. ### 👀 Preview steps 1. ℹ️ have an account and a project with transcription 2. have QA questions 3. have `autoQAEnabled` feature flag enabled (`?ff_autoQAEnabled=true`) 4. Press Generate with AI button in the analysis tab 5. 🔴 [on main] notice that this won't trigger any action 6. 🟢 [on PR] notice that this triggers activation of the feature for the existing questions and the API call for generating the answer with AI. --------- Co-authored-by: Leszek Pietrzak <[email protected]> Co-authored-by: Leszek <[email protected]>
1 parent 6af5b81 commit 2fe6451

9 files changed

Lines changed: 225 additions & 56 deletions

File tree

jsapp/js/api/mutation-defaults/survey-data.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,13 @@ queryClient.setMutationDefaults(
324324
snapshots: [itemSnapshot],
325325
}
326326
}
327+
case ActionEnum.automatic_bedrock_qual: {
328+
// No optimistic update for automatic_bedrock_qual since
329+
// it only supports 'complete' or 'failed' status (no 'in_progress')
330+
return {
331+
snapshots: [],
332+
}
333+
}
327334
default:
328335
throw new Error(`Unknown action "${action}" is not handled.`)
329336
}

jsapp/js/components/processing/SingleProcessingContent/TabAnalysis/AnalysisContent/AnalysisQuestionsList/AnalysisQuestionListItem/ResponseForm.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react'
1+
import React, { useState } from 'react'
22

33
import { Group, Modal, Stack, Text } from '@mantine/core'
44
import { Box, ThemeIcon } from '@mantine/core'
@@ -19,14 +19,28 @@ interface Props {
1919
disabled: boolean
2020
onEdit: (qaQuestion: ResponseQualActionParams) => unknown
2121
onDelete: (qaQuestion: ResponseQualActionParams) => Promise<unknown>
22+
/**
23+
* Adds a Generate with AI button. API handling is being served by parent(s), as this component doesn't have all
24+
* the required data and it's easier to push this one function up than all the small pieces down.
25+
*/
26+
onGenerateWithAI?: () => Promise<unknown>
2227
}
2328

2429
/**
2530
* Displays question type icon, name, and an edit and delete buttons (if user
2631
* has sufficient permissions). Is being used in multiple other components.
2732
*/
28-
export default function ResponseForm({ qaQuestion, children, onClear, disabled, onEdit, onDelete }: Props) {
33+
export default function ResponseForm({
34+
qaQuestion,
35+
children,
36+
onClear,
37+
disabled,
38+
onEdit,
39+
onDelete,
40+
onGenerateWithAI,
41+
}: Props) {
2942
const [opened, { open, close }] = useDisclosure(false)
43+
const [isGenerating, setIsGenerating] = useState(false)
3044

3145
const ffAutoQAEnabled = useFeatureFlag(FeatureFlag.autoQAEnabled)
3246

@@ -45,6 +59,16 @@ export default function ResponseForm({ qaQuestion, children, onClear, disabled,
4559
close()
4660
}
4761

62+
const handleGenerateWithAI = async () => {
63+
if (!onGenerateWithAI) return
64+
setIsGenerating(true)
65+
try {
66+
await onGenerateWithAI()
67+
} finally {
68+
setIsGenerating(false)
69+
}
70+
}
71+
4872
return (
4973
<Stack gap='10px'>
5074
<Group align={'flex-start'} gap={'xs'}>
@@ -108,7 +132,7 @@ export default function ResponseForm({ qaQuestion, children, onClear, disabled,
108132

109133
{/* Hard coded left padding to account for the 32px icon size + 8px gap */}
110134
{children && <Box pl={'40px'}>{children}</Box>}
111-
{!disabled && ffAutoQAEnabled && (
135+
{!disabled && ffAutoQAEnabled && onGenerateWithAI && (
112136
<Group pl={'40px'}>
113137
<ButtonNew
114138
variant='transparent'
@@ -118,6 +142,9 @@ export default function ResponseForm({ qaQuestion, children, onClear, disabled,
118142
disabled={disabled}
119143
c='var(--mantine-color-blue-5)'
120144
leftIcon='sparkles'
145+
onClick={handleGenerateWithAI}
146+
loading={isGenerating}
147+
loaderProps={{ type: 'dots' }}
121148
>
122149
{t('Generate with AI')}
123150
</ButtonNew>

jsapp/js/components/processing/SingleProcessingContent/TabAnalysis/AnalysisContent/AnalysisQuestionsList/AnalysisQuestionListItem/index.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import styles from './index.module.scss'
3636

3737
export interface Props {
3838
asset: AssetResponse
39-
advancedFeature: AdvancedFeatureResponseManualQual
39+
advancedFeatureManual: AdvancedFeatureResponseManualQual
4040
questionXpath: string
4141
qaQuestion: ResponseQualActionParams
4242
setQaQuestion: (qualQuestion: ResponseQualActionParams | undefined) => void
@@ -45,6 +45,7 @@ export interface Props {
4545
moveRow: (uuid: string, oldIndex: number, newIndex: number) => void
4646
editMode: boolean
4747
isAnyQuestionBeingEdited: boolean
48+
onGenerateWithAI: (qaQuestion: ResponseQualActionParams) => Promise<void>
4849
}
4950

5051
interface DragItem {
@@ -61,7 +62,7 @@ interface DragItem {
6162
*/
6263
export default function AnalysisQuestionListItem({
6364
asset,
64-
advancedFeature,
65+
advancedFeatureManual,
6566
questionXpath,
6667
qaQuestion,
6768
setQaQuestion,
@@ -70,6 +71,7 @@ export default function AnalysisQuestionListItem({
7071
moveRow,
7172
editMode,
7273
isAnyQuestionBeingEdited,
74+
onGenerateWithAI,
7375
}: Props) {
7476
const rootUuid = removeDefaultUuidPrefix(submission['meta/rootUuid'])
7577

@@ -94,6 +96,7 @@ export default function AnalysisQuestionListItem({
9496
const mutationSaveAnswer = useAssetsDataSupplementPartialUpdate({ mutation: { scope: { id: 'qa-answer' } } })
9597
const mutationCreateQuestion = useAssetsAdvancedFeaturesCreate({ mutation: { scope: { id: 'qa-question' } } })
9698
const mutationPatchQuestion = useAssetsAdvancedFeaturesPartialUpdate({ mutation: { scope: { id: 'qa-question' } } })
99+
97100
const handleSaveAnswer = async (value: ManualQualValue) => {
98101
await mutationSaveAnswer.mutateAsync({
99102
uidAsset: asset.uid,
@@ -110,28 +113,27 @@ export default function AnalysisQuestionListItem({
110113
})
111114
}
112115

113-
const isCreate = advancedFeature.uid === LOCALLY_EDITED_PLACEHOLDER_UUID
116+
const isCreate = advancedFeatureManual.uid === LOCALLY_EDITED_PLACEHOLDER_UUID
114117

115118
const handleSaveQuestion = async (params: ResponseQualActionParams[]) => {
116119
if (isCreate) {
117120
await mutationCreateQuestion.mutateAsync({
118121
uidAsset: asset.uid,
119122
data: {
120123
action: ActionEnum.manual_qual,
121-
question_xpath: advancedFeature.question_xpath,
124+
question_xpath: advancedFeatureManual.question_xpath,
122125
params: params,
123126
},
124127
})
125128
} else {
126129
await mutationPatchQuestion.mutateAsync({
127130
uidAsset: asset.uid,
128-
uidAdvancedFeature: advancedFeature.uid,
131+
uidAdvancedFeature: advancedFeatureManual.uid,
129132
data: {
130133
params: params,
131134
},
132135
})
133136
}
134-
console.log('adf')
135137
setQaQuestion(undefined)
136138
}
137139

@@ -141,13 +143,13 @@ export default function AnalysisQuestionListItem({
141143

142144
const handleDeleteQuestion = async (qaQuestionToDelete: ResponseQualActionParams) => {
143145
// Mark the question as deleted by setting options.deleted to true
144-
const updatedParams = advancedFeature.params.map((param) =>
146+
const updatedParams = advancedFeatureManual.params.map((param: ResponseQualActionParams) =>
145147
param.uuid === qaQuestionToDelete.uuid ? { ...param, options: { ...param.options, deleted: true } } : param,
146148
)
147149

148150
await mutationPatchQuestion.mutateAsync({
149151
uidAsset: asset.uid,
150-
uidAdvancedFeature: advancedFeature.uid,
152+
uidAdvancedFeature: advancedFeatureManual.uid,
151153
data: {
152154
params: updatedParams,
153155
},
@@ -158,7 +160,7 @@ export default function AnalysisQuestionListItem({
158160
const handleReorderQuestions = (reorderedParams: ResponseQualActionParams[]) => {
159161
return mutationPatchQuestion.mutateAsync({
160162
uidAsset: asset.uid,
161-
uidAdvancedFeature: advancedFeature.uid,
163+
uidAdvancedFeature: advancedFeatureManual.uid,
162164
data: {
163165
params: reorderedParams,
164166
},
@@ -251,7 +253,7 @@ export default function AnalysisQuestionListItem({
251253
// Save the reordered questions to the backend
252254
// The moveRow callback has already updated the visual order in the parent component
253255
// Here we persist that change to the backend using the current params order
254-
await handleReorderQuestions(advancedFeature.params)
256+
await handleReorderQuestions(advancedFeatureManual.params)
255257
},
256258
})
257259

@@ -263,7 +265,7 @@ export default function AnalysisQuestionListItem({
263265
if (editMode) {
264266
return (
265267
<AnalysisQuestionEditor
266-
advancedFeature={advancedFeature}
268+
advancedFeature={advancedFeatureManual}
267269
qaQuestion={qaQuestion}
268270
onSaveQuestion={handleSaveQuestion}
269271
disabled={disabledAnswer}
@@ -295,6 +297,7 @@ export default function AnalysisQuestionListItem({
295297
disabled={disabledQuestion}
296298
onEdit={setQaQuestion}
297299
onDelete={handleDeleteQuestion}
300+
onGenerateWithAI={() => onGenerateWithAI(qaQuestion)}
298301
>
299302
<SelectMultipleResponseForm
300303
qaQuestion={qaQuestion}
@@ -330,6 +333,7 @@ export default function AnalysisQuestionListItem({
330333
onEdit={setQaQuestion}
331334
onDelete={handleDeleteQuestion}
332335
onClear={handleClearSelection}
336+
onGenerateWithAI={() => onGenerateWithAI(qaQuestion)}
333337
>
334338
<SelectOneResponseForm
335339
qaQuestion={qaQuestion}
@@ -359,6 +363,7 @@ export default function AnalysisQuestionListItem({
359363
disabled={disabledQuestion}
360364
onEdit={setQaQuestion}
361365
onDelete={handleDeleteQuestion}
366+
onGenerateWithAI={() => onGenerateWithAI(qaQuestion)}
362367
>
363368
<IntegerResponseForm qaAnswer={queryAnswer.data} disabled={disabledAnswer} onSave={handleSaveAnswer} />
364369
</ResponseForm>
@@ -371,6 +376,7 @@ export default function AnalysisQuestionListItem({
371376
disabled={disabledQuestion}
372377
onEdit={setQaQuestion}
373378
onDelete={handleDeleteQuestion}
379+
onGenerateWithAI={() => onGenerateWithAI(qaQuestion)}
374380
>
375381
<TextResponseForm qaAnswer={queryAnswer.data} disabled={disabledAnswer} onSave={handleSaveAnswer} />
376382
</ResponseForm>

0 commit comments

Comments
 (0)