Skip to content

Commit

Permalink
[FEATURE] [MER-4040] Initial activity trigger authoring implementation (
Browse files Browse the repository at this point in the history
#5370)

* initial activity trigger authoring implementation

* delint

* delint

* revert inadvertently included local mod

---------

Co-authored-by: Anders Weinstein <[email protected]>
  • Loading branch information
andersweinstein and Anders Weinstein authored Jan 24, 2025
1 parent f5be5a5 commit c16d4f4
Show file tree
Hide file tree
Showing 13 changed files with 334 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AuthoringElement, AuthoringElementProps } from '../AuthoringElement';
import { AuthoringElementProvider, useAuthoringElementContext } from '../AuthoringElementProvider';
import { Explanation } from '../common/explanation/ExplanationAuthoring';
import { ActivityScoring } from '../common/responses/ActivityScoring';
import { TriggerAuthoring, TriggerLabel } from '../common/triggers/TriggerAuthoring';
import { VariableEditorOrNot } from '../common/variables/VariableEditorOrNot';
import { VariableActions } from '../common/variables/variableActions';
import * as ActivityTypes from '../types';
Expand Down Expand Up @@ -97,6 +98,10 @@ const CheckAllThatApply = () => {
onEdit={(t) => dispatch(VariableActions.onUpdateTransformations(t))}
/>
</TabbedNavigation.Tab>
<TabbedNavigation.Tab label={TriggerLabel()}>
<TriggerAuthoring partId={model.authoring.parts[0].id} />
</TabbedNavigation.Tab>

<ActivitySettings settings={[shuffleAnswerChoiceSetting(model, dispatch)]} />
</TabbedNavigation.Tabs>
);
Expand Down
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 assets/src/components/activities/common/triggers/TriggerAuthoring.tsx
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>&quot;Give the students another worked example of this question type&quot;</li>
<li>
&quot;Ask the student if they need further assistance answering this
question&quot;
</li>
<li>
&quot;Point students towards more practice regarding this question&apos;s learning
objectives&quot;
</li>
<li>&quot;Give students another question of this type&quot;</li>
<li>&quot;Give students an expert response to this question&quot;</li>
<li>&quot;Evaluate the student&apos;s answer to this question&quot;</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 assets/src/components/activities/common/triggers/TriggerUtils.tsx
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]';
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import guid from 'utils/guid';
import { AuthoringElement, AuthoringElementProps } from '../AuthoringElement';
import { AuthoringElementProvider, useAuthoringElementContext } from '../AuthoringElementProvider';
import { Explanation } from '../common/explanation/ExplanationAuthoring';
import { TriggerAuthoring, TriggerLabel } from '../common/triggers/TriggerAuthoring';
import * as ActivityTypes from '../types';
import { MediaItemRequest } from '../types';
import { ICActions } from './actions';
Expand Down Expand Up @@ -212,6 +213,9 @@ const ImageCoding = (props: AuthoringElementProps<ImageCodingModelSchema>) => {
<TabbedNavigation.Tab label="Explanation">
<Explanation partId={model.authoring.parts[0].id} />
</TabbedNavigation.Tab>
<TabbedNavigation.Tab label={TriggerLabel()}>
<TriggerAuthoring partId={model.authoring.parts[0].id} />
</TabbedNavigation.Tab>
</TabbedNavigation.Tabs>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { ChoicesDelivery } from '../common/choices/delivery/ChoicesDelivery';
import { Explanation } from '../common/explanation/ExplanationAuthoring';
import { SimpleFeedback } from '../common/responses/SimpleFeedback';
import { TargetedFeedback } from '../common/responses/TargetedFeedback';
import { TriggerAuthoring, TriggerLabel } from '../common/triggers/TriggerAuthoring';
import * as ActivityTypes from '../types';
import { MediaItemRequest, makeChoice } from '../types';
import { CircleEditor } from './Sections/CircleEditor';
Expand Down Expand Up @@ -289,6 +290,9 @@ const ImageHotspot = (props: AuthoringElementProps<ImageHotspotModelSchema>) =>
<TabbedNavigation.Tab label="Explanation">
<Explanation partId={selectedPartId} />
</TabbedNavigation.Tab>
<TabbedNavigation.Tab label={TriggerLabel()}>
<TriggerAuthoring partId={model.authoring.parts[0].id} />
</TabbedNavigation.Tab>
</TabbedNavigation.Tabs>
</React.Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { InputRef } from 'data/content/model/elements/types';
import { configureStore } from 'state/store';
import { AuthoringElement, AuthoringElementProps } from '../AuthoringElement';
import { AuthoringElementProvider, useAuthoringElementContext } from '../AuthoringElementProvider';
import { TriggerAuthoring, TriggerLabel } from '../common/triggers/TriggerAuthoring';
import { VariableEditorOrNot } from '../common/variables/VariableEditorOrNot';
import { VariableActions } from '../common/variables/variableActions';
import { ExplanationTab } from './sections/ExplanationTab';
Expand Down Expand Up @@ -81,6 +82,10 @@ export const MultiInputComponent = () => {
onEdit={(t) => dispatch(VariableActions.onUpdateTransformations(t))}
/>
</TabbedNavigation.Tab>
<TabbedNavigation.Tab label={TriggerLabel()}>
<TriggerAuthoring partId={model.authoring.parts[0].id} />
</TabbedNavigation.Tab>

<ActivitySettings settings={settings} />
</TabbedNavigation.Tabs>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AuthoringElementProvider, useAuthoringElementContext } from '../Authori
import { MCActions as Actions } from '../common/authoring/actions/multipleChoiceActions';
import { Explanation } from '../common/explanation/ExplanationAuthoring';
import { ActivityScoring } from '../common/responses/ActivityScoring';
import { TriggerAuthoring, TriggerLabel } from '../common/triggers/TriggerAuthoring';
import { VariableEditorOrNot } from '../common/variables/VariableEditorOrNot';
import { VariableActions } from '../common/variables/variableActions';
import * as ActivityTypes from '../types';
Expand Down Expand Up @@ -102,6 +103,10 @@ const MultipleChoice: React.FC = () => {
/>
</TabbedNavigation.Tab>

<TabbedNavigation.Tab label={TriggerLabel()}>
<TriggerAuthoring partId={model.authoring.parts[0].id} />
</TabbedNavigation.Tab>

<ActivitySettings settings={[shuffleAnswerChoiceSetting(model, dispatch)]} />
</TabbedNavigation.Tabs>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { AuthoringElement, AuthoringElementProps } from '../AuthoringElement';
import { AuthoringElementProvider, useAuthoringElementContext } from '../AuthoringElementProvider';
import { Explanation } from '../common/explanation/ExplanationAuthoring';
import { ActivityScoring } from '../common/responses/ActivityScoring';
import { TriggerAuthoring, TriggerLabel } from '../common/triggers/TriggerAuthoring';
import { VariableEditorOrNot } from '../common/variables/VariableEditorOrNot';
import { VariableActions } from '../common/variables/variableActions';
import * as ActivityTypes from '../types';
Expand Down Expand Up @@ -86,6 +87,9 @@ export const Ordering: React.FC = () => {
onEdit={(t) => dispatch(VariableActions.onUpdateTransformations(t))}
/>
</TabbedNavigation.Tab>
<TabbedNavigation.Tab label={TriggerLabel()}>
<TriggerAuthoring partId={model.authoring.parts[0].id} />
</TabbedNavigation.Tab>

<ActivitySettings settings={[shuffleAnswerChoiceSetting(model, dispatch)]} />
</TabbedNavigation.Tabs>
Expand Down
Loading

0 comments on commit c16d4f4

Please sign in to comment.