-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FEATURE] [MER-4040] Initial activity trigger authoring implementation (
#5370) * initial activity trigger authoring implementation * delint * delint * revert inadvertently included local mod --------- Co-authored-by: Anders Weinstein <[email protected]>
- Loading branch information
1 parent
f5be5a5
commit c16d4f4
Showing
13 changed files
with
334 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
30 changes: 30 additions & 0 deletions
30
assets/src/components/activities/common/triggers/TriggerActions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { HasParts } from 'components/activities/types'; | ||
import { getPartById } from 'data/activities/model/utils'; | ||
import { ActivityTrigger, sameTrigger } from 'data/triggers'; | ||
|
||
export const TriggerActions = { | ||
addTrigger(trigger: ActivityTrigger, partId: string) { | ||
return (model: HasParts) => { | ||
const part = getPartById(model, partId); | ||
if (part.triggers) part.triggers.push(trigger); | ||
else part.triggers = [trigger]; | ||
}; | ||
}, | ||
|
||
removeTrigger(trigger: ActivityTrigger, partId: string) { | ||
return (model: HasParts) => { | ||
const part = getPartById(model, partId); | ||
if (part.triggers) part.triggers = part.triggers.filter((t) => !sameTrigger(t, trigger)); | ||
}; | ||
}, | ||
|
||
setTriggerPrompt(trigger: ActivityTrigger, partId: string, prompt: string) { | ||
return (model: HasParts) => { | ||
const part = getPartById(model, partId); | ||
if (part.triggers) { | ||
const target = part.triggers.find((t) => sameTrigger(t, trigger)); | ||
if (target) target.prompt = prompt; | ||
} | ||
}; | ||
}, | ||
}; |
166 changes: 166 additions & 0 deletions
166
assets/src/components/activities/common/triggers/TriggerAuthoring.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import React, { useState } from 'react'; | ||
import { Button } from 'react-bootstrap'; | ||
import { useAuthoringElementContext } from 'components/activities/AuthoringElementProvider'; | ||
import { HasParts } from 'components/activities/types'; | ||
import { Card } from 'components/misc/Card'; | ||
import { getPartById } from 'data/activities/model/utils'; | ||
import { ActivityTrigger } from 'data/triggers'; | ||
import { RemoveButtonConnected } from '../authoring/RemoveButton'; | ||
import { TriggerActions } from './TriggerActions'; | ||
import { describeTrigger, getAvailableTriggers } from './TriggerUtils'; | ||
|
||
interface Props { | ||
partId: string; | ||
} | ||
|
||
export const TriggerAuthoring: React.FC<Props> = ({ partId }) => { | ||
const { model, dispatch } = useAuthoringElementContext<HasParts>(); | ||
const part = getPartById(model, partId); | ||
|
||
// Add trigger is a mode of the UI | ||
const [addMode, setAddMode] = useState<boolean>(false); | ||
const [showPromptHelp, setShowPromptHelp] = useState<boolean>(false); | ||
const [currentTrigger, setCurrentTrigger] = useState<ActivityTrigger | null>(null); | ||
const [currentPrompt, setCurrentPrompt] = useState<string>(''); | ||
|
||
const canAddTrigger = () => { | ||
const result = currentTrigger != null && currentPrompt != ''; | ||
console.log('canAddTrigger() => ' + result); | ||
return result; | ||
}; | ||
|
||
const addTrigger = () => { | ||
if (!currentTrigger) return; | ||
currentTrigger.prompt = currentPrompt; | ||
dispatch(TriggerActions.addTrigger(currentTrigger, partId)); | ||
endAddMode(); | ||
}; | ||
|
||
const endAddMode = () => { | ||
setAddMode(false); | ||
setCurrentTrigger(null); | ||
}; | ||
|
||
const available_triggers = getAvailableTriggers(model, partId); | ||
const existing_triggers = part.triggers || []; | ||
|
||
const onTriggerChange = (e: React.ChangeEvent<HTMLSelectElement>) => { | ||
const index = +e.target.value; | ||
setCurrentTrigger(available_triggers[index]); | ||
}; | ||
|
||
return ( | ||
<> | ||
<h4> DOT AI Activity Trigger Point</h4> | ||
<p> | ||
{' '} | ||
Customize a prompt for our AI assistant, DOT, to follow based on learner actions within this | ||
activity. | ||
</p> | ||
|
||
{!addMode && ( | ||
<div className="flex justify-center"> | ||
<Button onClick={(_e) => setAddMode(true)}>+ Create New Trigger</Button> | ||
</div> | ||
)} | ||
|
||
{/* modal area for extended prompt mode editing */} | ||
{addMode && ( | ||
<div> | ||
<p> | ||
<img src="/images/icons/icon-ai.svg" className="inline" /> | ||
<b>Trigger</b> | ||
</p> | ||
<p> | ||
An AI trigger is when our AI assistant, DOT, responds to something a learner does, like | ||
giving feedback or extra help based on their actions. | ||
</p> | ||
|
||
<select defaultValue="" onChange={onTriggerChange}> | ||
<option key="instructions" value="" disabled> | ||
Choose student action... | ||
</option> | ||
{available_triggers.map((t, i) => ( | ||
<option key={i} value={i}> | ||
{describeTrigger(t, part)} | ||
</option> | ||
))} | ||
</select> | ||
|
||
<p> | ||
<b>Prompt</b> | ||
</p> | ||
<p> | ||
An AI prompt is a question or instruction given to our AI assistant, DOT, to guide its | ||
response, helping it generate useful feedback, explanations, or support for learners. | ||
</p> | ||
<div> | ||
<Button onClick={(e) => setShowPromptHelp(!showPromptHelp)}> | ||
{showPromptHelp ? 'Hide Example Prompts' : 'Show Examples of Helpful Prompts'} | ||
</Button> | ||
{showPromptHelp && ( | ||
<ul> | ||
<li>"Give the students another worked example of this question type"</li> | ||
<li> | ||
"Ask the student if they need further assistance answering this | ||
question" | ||
</li> | ||
<li> | ||
"Point students towards more practice regarding this question's learning | ||
objectives" | ||
</li> | ||
<li>"Give students another question of this type"</li> | ||
<li>"Give students an expert response to this question"</li> | ||
<li>"Evaluate the student's answer to this question"</li> | ||
</ul> | ||
)} | ||
</div> | ||
<p>When triggered, DOT will:</p> | ||
<textarea className="w-full" onChange={(ev) => setCurrentPrompt(ev.target.value)} /> | ||
|
||
<div className=""> | ||
<Button onClick={addTrigger} disabled={!canAddTrigger()}> | ||
Save | ||
</Button> | ||
<Button onClick={endAddMode}>Cancel</Button> | ||
</div> | ||
</div> | ||
)} | ||
|
||
{/* Existing triggers */} | ||
{!addMode && | ||
existing_triggers.map((t, i) => ( | ||
<Card.Card key={i}> | ||
<Card.Title> | ||
{i + 1}. {describeTrigger(t, part)} | ||
<RemoveButtonConnected | ||
onClick={() => dispatch(TriggerActions.removeTrigger(t, partId))} | ||
/> | ||
</Card.Title> | ||
<Card.Content> | ||
<div className="flex"> | ||
Prompt: | ||
<input | ||
type="text" | ||
className="grow" | ||
value={t.prompt} | ||
onChange={(e) => | ||
dispatch(TriggerActions.setTriggerPrompt(t, partId, e.target.value)) | ||
} | ||
/> | ||
</div> | ||
</Card.Content> | ||
</Card.Card> | ||
))} | ||
</> | ||
); | ||
}; | ||
|
||
export const TriggerLabel = () => { | ||
return ( | ||
<span> | ||
<img src="/images/icons/icon-ai.svg" className="inline" /> | ||
DOT AI | ||
</span> | ||
); | ||
}; |
75 changes: 75 additions & 0 deletions
75
assets/src/components/activities/common/triggers/TriggerUtils.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { HasParts, Part, RichText } from 'components/activities/types'; | ||
import { toSimpleText } from 'components/editing/slateUtils'; | ||
import { findTargetedResponses } from 'data/activities/model/responses'; | ||
import { getPartById } from 'data/activities/model/utils'; | ||
import { | ||
ActivityTrigger, | ||
makeHintTrigger, | ||
makeTargetedTrigger, | ||
makeTrigger, | ||
sameTrigger, | ||
} from 'data/triggers'; | ||
|
||
export const getPossibleTriggers = (model: HasParts, partId: string): ActivityTrigger[] => { | ||
const part = getPartById(model, partId); | ||
|
||
const triggers = [makeTrigger('correct_answer'), makeTrigger('incorrect_answer')]; | ||
const hint_triggers = part.hints.map((_h, i) => makeHintTrigger(i + 1)); | ||
const targeted_triggers = findTargetedResponses(model, partId).map((r: any) => | ||
makeTargetedTrigger(r.id), | ||
); | ||
const explanation_triggers = part.explanation ? [makeTrigger('explanation')] : []; | ||
|
||
return triggers.concat( | ||
hint_triggers, | ||
targeted_triggers, | ||
explanation_triggers, | ||
) as ActivityTrigger[]; | ||
}; | ||
|
||
export const getAvailableTriggers = (model: HasParts, partId: string) => { | ||
const all_triggers = getPossibleTriggers(model, partId); | ||
const has_trigger = (t: ActivityTrigger) => | ||
getPartById(model, partId).triggers?.some((existing) => sameTrigger(t, existing)); | ||
|
||
return all_triggers.filter((t: ActivityTrigger) => !has_trigger(t)); | ||
}; | ||
|
||
export const describeTrigger = (t: ActivityTrigger, part: Part) => { | ||
const nth = [ | ||
'zeroth', | ||
'first', | ||
'second', | ||
'third', | ||
'fourth', | ||
'fifth', | ||
'sixth', | ||
'seventh', | ||
'eighth', | ||
]; | ||
|
||
const shortText = (content: RichText) => { | ||
const MAX = 30; | ||
const full = toSimpleText(content); | ||
return full.length < MAX ? full : full.slice(0, MAX - 3) + '...'; | ||
}; | ||
|
||
switch (t.trigger_type) { | ||
case 'correct_answer': | ||
return 'Student submits correct answer'; | ||
case 'incorrect_answer': | ||
return 'Student submits incorrect answer'; | ||
case 'explanation': | ||
return `Student triggers explanation (${shortText(part.explanation!.content)})`; | ||
case 'hint': | ||
const hint = shortText(part.hints[t.hint_number - 1].content); | ||
return `Student requests ${nth[t.hint_number]} hint (${hint})`; | ||
case 'targeted_feedback': | ||
const response = part.responses.find((r) => r.id == t.response_id); | ||
const feedback = response ? shortText(response.feedback.content) : 'not found'; | ||
return `Student triggers targeted feedback (${feedback})`; | ||
default: | ||
console.error('unrecognized activity trigger type'); | ||
return '[unknown]'; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.