Skip to content

Commit 2102bc1

Browse files
Allow teachers to enable AI for their students
1 parent 0cd03bb commit 2102bc1

7 files changed

Lines changed: 190 additions & 10 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// @flow
2+
3+
import * as React from 'react';
4+
import { I18n } from '@lingui/react';
5+
import { t, Trans } from '@lingui/macro';
6+
import Dialog from '../../../../UI/Dialog';
7+
import FlatButton from '../../../../UI/FlatButton';
8+
import { CompactToggleField } from '../../../../UI/CompactToggleField';
9+
import { Line } from '../../../../UI/Grid';
10+
import TeamContext from '../../../../Profile/Team/TeamContext';
11+
12+
type Props = {|
13+
onClose: () => void,
14+
|};
15+
16+
const AdvancedStudentOptionsDialog = ({ onClose }: Props): React.Node => {
17+
const { team, onUpdateTeam } = React.useContext(TeamContext);
18+
const [isUpdatingTeam, setIsUpdatingTeam] = React.useState<boolean>(false);
19+
20+
const onToggleStudentsAskAi = React.useCallback(
21+
async (allowed: boolean) => {
22+
setIsUpdatingTeam(true);
23+
try {
24+
await onUpdateTeam({ classrooms: { hideAskAi: !allowed } });
25+
} catch (error) {
26+
console.error(
27+
'An error occurred while updating the team AI setting:',
28+
error
29+
);
30+
} finally {
31+
setIsUpdatingTeam(false);
32+
}
33+
},
34+
[onUpdateTeam]
35+
);
36+
37+
return (
38+
<I18n>
39+
{({ i18n }) => (
40+
<Dialog
41+
title={<Trans>Advanced student options</Trans>}
42+
actions={[
43+
<FlatButton
44+
label={<Trans>Close</Trans>}
45+
disabled={isUpdatingTeam}
46+
key="close"
47+
primary={false}
48+
onClick={onClose}
49+
/>,
50+
]}
51+
maxWidth="sm"
52+
cannotBeDismissed={isUpdatingTeam}
53+
onRequestClose={onClose}
54+
open
55+
>
56+
<Line noMargin>
57+
<CompactToggleField
58+
label={i18n._(
59+
t`Allow students to use GDevelop's AI (disabled by default)`
60+
)}
61+
checked={
62+
!!team &&
63+
!!team.classrooms &&
64+
team.classrooms.hideAskAi === false
65+
}
66+
onCheck={allowed => {
67+
onToggleStudentsAskAi(allowed);
68+
}}
69+
disabled={isUpdatingTeam}
70+
/>
71+
</Line>
72+
</Dialog>
73+
)}
74+
</I18n>
75+
);
76+
};
77+
78+
export default AdvancedStudentOptionsDialog;

newIDE/app/src/MainFrame/EditorContainers/HomePage/TeamSection/index.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ import { EducationCard } from '../LearnSection/EducationCard';
4646
import UserSVG from '../../../../UI/CustomSvgIcons/User';
4747
import { copyTextToClipboard } from '../../../../Utils/Clipboard';
4848
import ManageEducationAccountDialog from './ManageEducationAccountDialog';
49+
import AdvancedStudentOptionsDialog from './AdvancedStudentOptionsDialog';
4950
import TeamAvailableSeats from './TeamAvailableSeats';
51+
import IconButton from '../../../../UI/IconButton';
52+
import Settings from '../../../../UI/CustomSvgIcons/Settings';
5053

5154
const PADDING = 16;
5255

@@ -112,6 +115,10 @@ const TeamSection = React.forwardRef<Props, TeamSectionInterface>(
112115
manageSeatsDialogOpen,
113116
setManageSeatsDialogOpen,
114117
] = React.useState<boolean>(false);
118+
const [
119+
advancedStudentOptionsDialogOpen,
120+
setAdvancedStudentOptionsDialogOpen,
121+
] = React.useState<boolean>(false);
115122
const forceUpdate = useForceUpdate();
116123
const { isMobile } = useResponsiveWindowSize();
117124
const contextMenu = React.useRef<?ContextMenuInterface>(null);
@@ -296,14 +303,23 @@ const TeamSection = React.forwardRef<Props, TeamSectionInterface>(
296303
justifyContent="space-between"
297304
>
298305
<TeamAvailableSeats />
299-
<RaisedButton
300-
primary
301-
label={
302-
isMobile ? <Trans>Manage</Trans> : <Trans>Manage seats</Trans>
303-
}
304-
icon={<UserSVG fontSize="small" />}
305-
onClick={() => setManageSeatsDialogOpen(true)}
306-
/>
306+
<LineStackLayout noMargin alignItems="center">
307+
<RaisedButton
308+
primary
309+
label={
310+
isMobile ? <Trans>Manage</Trans> : <Trans>Manage seats</Trans>
311+
}
312+
icon={<UserSVG fontSize="small" />}
313+
onClick={() => setManageSeatsDialogOpen(true)}
314+
/>
315+
<IconButton
316+
size="small"
317+
tooltip={t`Advanced student options`}
318+
onClick={() => setAdvancedStudentOptionsDialogOpen(true)}
319+
>
320+
<Settings />
321+
</IconButton>
322+
</LineStackLayout>
307323
</LineStackLayout>
308324
</div>
309325
);
@@ -540,6 +556,11 @@ const TeamSection = React.forwardRef<Props, TeamSectionInterface>(
540556
onClose={() => setManageSeatsDialogOpen(false)}
541557
/>
542558
)}
559+
{advancedStudentOptionsDialogOpen && (
560+
<AdvancedStudentOptionsDialog
561+
onClose={() => setAdvancedStudentOptionsDialogOpen(false)}
562+
/>
563+
)}
543564
</>
544565
);
545566
}

newIDE/app/src/Profile/Team/TeamContext.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export type TeamState = {|
3737
newPassword: string
3838
) => Promise<void>,
3939
onEditUser: (editedUserId: string, changes: EditUserChanges) => Promise<void>,
40+
onUpdateTeam: (attributes: {|
41+
classrooms: {| hideAskAi: boolean |},
42+
|}) => Promise<void>,
4043
|};
4144

4245
export const initialTeamState = {
@@ -61,6 +64,7 @@ export const initialTeamState = {
6164
onSetMember: async () => {},
6265
onChangeMemberPassword: async () => {},
6366
onEditUser: async () => {},
67+
onUpdateTeam: async () => {},
6468
};
6569

6670
const TeamContext: React.Context<TeamState> = React.createContext<TeamState>(

newIDE/app/src/Profile/Team/TeamProvider.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
setUserAsMember,
2525
listTeamInvitations,
2626
editUser,
27+
updateTeam,
2728
type EditUserChanges,
2829
} from '../../Utils/GDevelopServices/User';
2930
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
@@ -399,6 +400,20 @@ const TeamProvider = ({ children }: Props): React.Node => {
399400
[team, getAuthorizationHeader, adminUserId]
400401
);
401402

403+
const onUpdateTeam = React.useCallback(
404+
async (attributes: {| classrooms: {| hideAskAi: boolean |} |}) => {
405+
if (!adminUserId || !team) return;
406+
const updatedTeam = await updateTeam(
407+
getAuthorizationHeader,
408+
adminUserId,
409+
team.id,
410+
attributes
411+
);
412+
setTeam(updatedTeam);
413+
},
414+
[team, getAuthorizationHeader, adminUserId]
415+
);
416+
402417
const getAvailableSeats = React.useCallback(
403418
() =>
404419
team && members && admins && invitations
@@ -434,6 +449,7 @@ const TeamProvider = ({ children }: Props): React.Node => {
434449
onActivateMembers,
435450
onSetAdmin,
436451
onSetMember,
452+
onUpdateTeam,
437453
}}
438454
>
439455
{children}

newIDE/app/src/Utils/GDevelopServices/User.js

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,16 @@ export type User = {|
181181
+password?: ?string,
182182
|};
183183

184-
export type Team = {| id: string, createdAt: number, seats: number |};
184+
export type Team = {|
185+
id: string,
186+
createdAt: number,
187+
seats: number,
188+
/**
189+
* Overrides applied to the `classrooms` capability of the students of this
190+
* team. Keys mirror the `classrooms` capability of the limits.
191+
*/
192+
classrooms?: ?{| hideAskAi: boolean |},
193+
|};
185194
export type TeamGroup = {| id: string, name: string |};
186195
export type TeamInvitation = {|
187196
teamId: string,
@@ -345,6 +354,24 @@ export const updateGroup = async (
345354
});
346355
};
347356

357+
export const updateTeam = async (
358+
getAuthorizationHeader: () => Promise<string>,
359+
userId: string,
360+
teamId: string,
361+
attributes: {| classrooms: {| hideAskAi: boolean |} |}
362+
): Promise<Team> => {
363+
const authorizationHeader = await getAuthorizationHeader();
364+
const response = await client.patch(`/team/${teamId}`, attributes, {
365+
headers: { Authorization: authorizationHeader },
366+
params: { userId },
367+
});
368+
return ensureObjectHasProperty({
369+
data: response.data,
370+
propertyName: 'id',
371+
endpointName: '/team/{id} of User API',
372+
});
373+
};
374+
348375
export const createGroup = async (
349376
getAuthorizationHeader: () => Promise<string>,
350377
userId: string,

newIDE/app/src/stories/MockTeamProvider.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,10 @@ export const MockTeamProvider = ({
195195
noActiveMembers?: boolean,
196196
teamSize?: number,
197197
|}): React.Node => {
198-
const team = { ...initialTeam, seats: teamSize || initialTeam.seats };
198+
const [team, setTeam] = React.useState<Team>({
199+
...initialTeam,
200+
seats: teamSize || initialTeam.seats,
201+
});
199202
const [nameChangeTryCount, setNameChangeTryCount] = React.useState<number>(0);
200203
const [userChangeTryCount, setUserChangeTryCount] = React.useState<number>(0);
201204
const [members, setMembers] = React.useState<?(User[])>(
@@ -507,6 +510,13 @@ export const MockTeamProvider = ({
507510
onEditUser: editUser,
508511
invitations: null,
509512
onRefreshInvitations: async () => {},
513+
onUpdateTeam: async attributes => {
514+
action('updateTeam')(attributes);
515+
setTeam(currentTeam => ({
516+
...currentTeam,
517+
classrooms: attributes.classrooms,
518+
}));
519+
},
510520
}}
511521
>
512522
<Text allowSelection>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// @flow
2+
import * as React from 'react';
3+
import { action } from '@storybook/addon-actions';
4+
5+
import paperDecorator from '../../../PaperDecorator';
6+
import FixedHeightFlexContainer from '../../../FixedHeightFlexContainer';
7+
import AdvancedStudentOptionsDialog from '../../../../MainFrame/EditorContainers/HomePage/TeamSection/AdvancedStudentOptionsDialog';
8+
import { MockTeamProvider } from '../../../MockTeamProvider';
9+
10+
export default {
11+
title: 'HomePage/TeamSection/AdvancedStudentOptionsDialog',
12+
component: AdvancedStudentOptionsDialog,
13+
decorators: [paperDecorator],
14+
};
15+
16+
export const Default = (): React.Node => {
17+
return (
18+
<MockTeamProvider loading={false}>
19+
<FixedHeightFlexContainer height={400}>
20+
<AdvancedStudentOptionsDialog onClose={action('onClose')} />
21+
</FixedHeightFlexContainer>
22+
</MockTeamProvider>
23+
);
24+
};

0 commit comments

Comments
 (0)