@nualang/nualang-ui-components 0.1.1352 → 0.1.1354

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/Dialogs/CreatePhrase/CreatePhrase.js +43 -0
  2. package/dist/Dialogs/GenerateDefinitions/GenerateDefinitions.js +1 -0
  3. package/dist/Dialogs/GeneratePhrases/GeneratePhrases.js +33 -3
  4. package/dist/Dialogs/GroupedFeedbackDialog/GroupedFeedbackDialog.js +146 -47
  5. package/dist/Dialogs/GroupedFeedbackDialog/package.json +6 -0
  6. package/dist/Dialogs/UploadPhrases/UploadPhrases.js +75 -14
  7. package/dist/Dialogs/UploadPhrases/upload-phrases-csv.png +0 -0
  8. package/dist/Editors/PhraseList/PhraseList.js +38 -34
  9. package/dist/Editors/Phrases/Phrases.js +37 -13
  10. package/dist/Exercises/Meaning/Meaning.js +788 -0
  11. package/dist/Exercises/PhraseGroup/Games/MeaningGame/MeaningGame.js +86 -0
  12. package/dist/Exercises/PhraseGroup/Games/index.js +2 -1
  13. package/dist/Exercises/PhraseGroup/PhraseGroup.js +4 -3
  14. package/dist/Exercises/Roleplay/Roleplay.js +2 -1
  15. package/dist/Exercises/Roleplay/RoleplayGameOptions/RoleplayGames.js +11 -4
  16. package/dist/Forms/CreateRoleplay/Steps/RoleplaySettings/RoleplaySettings.js +5 -2
  17. package/dist/Forms/UpdatePhraseList/UpdatePhraseList.js +8 -0
  18. package/dist/Forms/UpdateRoleplay/UpdateRoleplay.js +2 -1
  19. package/dist/Lists/Phrases/Phrases.js +9 -5
  20. package/dist/Misc/ChatBubble/ChatBubble.js +7 -2
  21. package/dist/Misc/HoverText/HoverText.js +18 -7
  22. package/dist/Misc/RubyText/RubyText.js +27 -0
  23. package/dist/Screens/Classrooms/ViewClassroom/ViewClassroom.js +2 -2
  24. package/dist/Screens/Courses/ViewCourse/ViewTopic/ViewTopic.js +94 -193
  25. package/dist/Tables/Progress/Header.js +557 -0
  26. package/dist/Tables/Progress/Progress.js +54 -349
  27. package/dist/Tables/Progress/ProgressList.js +21 -43
  28. package/dist/Tables/Progress/ProgressList.test.js +48 -0
  29. package/dist/Tables/Progress/ProgressTable.js +58 -1027
  30. package/dist/Tables/Progress/ProgressTable.test.js +258 -0
  31. package/dist/Tables/Progress/ProgressTableRow.js +270 -0
  32. package/dist/Tables/Progress/README.md +53 -0
  33. package/dist/Tables/Progress/TableToolbar.js +130 -0
  34. package/dist/Tables/Progress/TableToolbar.test.js +69 -0
  35. package/dist/Tables/Progress/cells/index.js +446 -0
  36. package/dist/Tables/Progress/cells/index.test.js +345 -0
  37. package/dist/Tables/Progress/constants.js +6 -0
  38. package/dist/Tables/Progress/exportToExcel.js +224 -0
  39. package/dist/Tables/Progress/exportToExcel.test.js +135 -0
  40. package/dist/Tables/Progress/helpers.js +42 -0
  41. package/dist/Tables/Progress/helpers.test.js +23 -0
  42. package/dist/Tables/Progress/sorting.js +78 -0
  43. package/dist/Tables/Progress/sorting.test.js +69 -0
  44. package/dist/Tables/Progress/styledComponents.js +156 -0
  45. package/dist/Tables/Progress/useProgressUrlParams.js +147 -0
  46. package/dist/Tables/Progress/useProgressUrlParams.test.js +183 -0
  47. package/dist/Tables/Progress/utils.js +2 -2
  48. package/dist/Tables/Progress/utils.test.js +129 -0
  49. package/dist/Tables/Progress/viewState.js +25 -0
  50. package/dist/hooks/useExerciseState.js +24 -4
  51. package/dist/hooks/usePhonetics.js +42 -0
  52. package/dist/utils/japaneseTokenizer.js +55 -0
  53. package/package.json +4 -1
@@ -56,6 +56,7 @@ function CreatePhrase({
56
56
  validatePhraseRecognition,
57
57
  verificationStatus,
58
58
  makeChatGptApiRequest,
59
+ difficulty,
59
60
  ...otherProps
60
61
  }) {
61
62
  const translationInputRef = useRef();
@@ -78,6 +79,7 @@ function CreatePhrase({
78
79
  const [definition, setDefinition] = useState("");
79
80
  const [definitionList, setDefinitionList] = useState(otherProps.definitions || []);
80
81
  const [autoTranslatedText, setAutoTranslatedText] = useState(false);
82
+ const [autoGeneratedDefinitions, setAutoGeneratedDefinitions] = useState(false);
81
83
  const [checkingRecogniton, setCheckingRecogniton] = useState(false);
82
84
  const [recognised, setRecognised] = useState(false);
83
85
  const [notRecognised, setNotRecognised] = useState(false);
@@ -287,6 +289,39 @@ function CreatePhrase({
287
289
  setIsSubmitting(false);
288
290
  }
289
291
  };
292
+ const generateDefinitions = async () => {
293
+ try {
294
+ setIsSubmitting(true);
295
+ let chatGptResponse = await makeChatGptApiRequest({
296
+ model: "gpt-4o",
297
+ promptKey: "generateDefinitions",
298
+ promptVariables: {
299
+ learnLang,
300
+ forLang,
301
+ siteLanguage,
302
+ difficulty,
303
+ phrases: [{
304
+ phrase,
305
+ translations: translationList
306
+ }],
307
+ generateDefinitions: true
308
+ }
309
+ });
310
+ const definitions = chatGptResponse?.result?.[0]?.definitions;
311
+ if (!validateResponse(definitions)) {
312
+ throw new Error("Invalid AI response");
313
+ }
314
+ setAutoGeneratedDefinitions(definitions[0]);
315
+ setDefinitionList(prevDefinitionList => {
316
+ const newDefinitions = definitions.filter(text => !prevDefinitionList.includes(text));
317
+ return [...prevDefinitionList, ...newDefinitions];
318
+ });
319
+ } catch (error) {
320
+ console.error("Error fetching definitions from OpenAI:", error.message);
321
+ } finally {
322
+ setIsSubmitting(false);
323
+ }
324
+ };
290
325
  const handleEditTranslation = (event, index) => {
291
326
  const {
292
327
  target: {
@@ -320,6 +355,7 @@ function CreatePhrase({
320
355
  setDefinitionList([]);
321
356
  setSelectedImage(initialSelectedImagestate);
322
357
  setAutoTranslatedText("");
358
+ setAutoGeneratedDefinitions("");
323
359
  };
324
360
  const removePhraseImage = () => {
325
361
  setSelectedImage({
@@ -495,6 +531,13 @@ function CreatePhrase({
495
531
  className: classes.alternativeVersionButton,
496
532
  startIcon: /*#__PURE__*/_jsx(AutoFixHighIcon, {}),
497
533
  children: t("auto_generate_phrase_translation")
534
+ }), learnLang !== forLang && /*#__PURE__*/_jsx(DefaultButton, {
535
+ onClick: generateDefinitions,
536
+ disabled: !phrase || Array.isArray(definitionList) && definitionList.includes(autoGeneratedDefinitions),
537
+ size: "sm",
538
+ className: classes.alternativeVersionButton,
539
+ startIcon: /*#__PURE__*/_jsx(AutoFixHighIcon, {}),
540
+ children: t("auto_generate_definitions")
498
541
  }), learnLang !== forLang && (import.meta.env.REACT_APP_STAGE === "dev" || window.location.host === "localhost:9009") && /*#__PURE__*/_jsx(Box, {
499
542
  sx: {
500
543
  display: "inline-flex",
@@ -101,6 +101,7 @@ function GenerateDefinitions({
101
101
  }
102
102
  };
103
103
  const handleCancel = () => {
104
+ handleClose();
104
105
  setGeneratedDefinitions([]);
105
106
  setErrorMessage(null);
106
107
  };
@@ -1,7 +1,7 @@
1
- import { useState } from "react";
1
+ import { useState, useEffect } from "react";
2
2
  import ResponsiveDialog from "../ResponsiveDialog/ResponsiveDialog";
3
3
  import { AutoFixHigh } from "@mui/icons-material";
4
- import { Alert, Grid, InputAdornment, LinearProgress, MenuItem, Slider, TextField, Tooltip, Typography, ListItem, ListItemText, useMediaQuery } from "@mui/material";
4
+ import { Alert, Grid, InputAdornment, LinearProgress, MenuItem, Slider, TextField, Tooltip, Typography, ListItem, ListItemText, useMediaQuery, FormControlLabel, Switch, Box } from "@mui/material";
5
5
  import { useTheme } from "@mui/material/styles";
6
6
  import { Link } from "react-router";
7
7
  import InfoIcon from "@mui/icons-material/Info";
@@ -115,6 +115,7 @@ function GeneratePhrases({
115
115
  const [errorMessage, setErrorMessage] = useState(null);
116
116
  const [phraseType, setPhraseType] = useState("words");
117
117
  const [generatedPhrases, setGeneratedPhrases] = useState([]);
118
+ const [generateDefinitions, setGenerateDefinitions] = useState(true);
118
119
  const isPhrases = currentPhrases && currentPhrases.length > 0;
119
120
  const phrases = isPhrases ? currentPhrases.map(({
120
121
  phrase,
@@ -188,7 +189,8 @@ function GeneratePhrases({
188
189
  difficulty,
189
190
  phraseType,
190
191
  isPhrases,
191
- phrases
192
+ phrases,
193
+ generateDefinitions
192
194
  }
193
195
  });
194
196
  const phrasesArray = chatGptResponse.result;
@@ -217,6 +219,9 @@ function GeneratePhrases({
217
219
  const handleCancel = () => {
218
220
  setGeneratedPhrases([]);
219
221
  };
222
+ useEffect(() => {
223
+ setGenerateDefinitions(phraseType === 'words');
224
+ }, [phraseType]);
220
225
  return /*#__PURE__*/_jsxs(ResponsiveDialog, {
221
226
  open: open,
222
227
  closeText: generatedPhrases.length > 0 ? t("cancel") : t("close"),
@@ -353,6 +358,31 @@ function GeneratePhrases({
353
358
  min: 4,
354
359
  max: 16
355
360
  })]
361
+ }), /*#__PURE__*/_jsxs(Grid, {
362
+ size: 12,
363
+ children: [/*#__PURE__*/_jsx(Typography, {
364
+ children: /*#__PURE__*/_jsx(Box, {
365
+ children: t("generate_definitions")
366
+ })
367
+ }), /*#__PURE__*/_jsx(FormControlLabel, {
368
+ label: "",
369
+ control: /*#__PURE__*/_jsx(_Fragment, {
370
+ children: /*#__PURE__*/_jsx("span", {
371
+ children: /*#__PURE__*/_jsx(Switch, {
372
+ checked: generateDefinitions,
373
+ onChange: e => {
374
+ setGenerateDefinitions(e.target.checked);
375
+ },
376
+ inputProps: {
377
+ "aria-label": "controlled"
378
+ },
379
+ sx: {
380
+ marginLeft: 0.5
381
+ }
382
+ })
383
+ })
384
+ })
385
+ })]
356
386
  })]
357
387
  }), generatedPhrases.length > 0 && /*#__PURE__*/_jsxs(_Fragment, {
358
388
  children: [/*#__PURE__*/_jsx(Typography, {
@@ -1,13 +1,22 @@
1
1
  import { useMemo, useEffect, useState, useRef, useCallback } from "react";
2
- import { Dialog, DialogContent, IconButton, Box, Typography, AppBar, Toolbar, CircularProgress, Button, Checkbox, Paper, Divider, Tooltip, Skeleton, Grid } from "@mui/material";
2
+ import { Dialog, DialogContent, IconButton, Box, Typography, AppBar, Toolbar, CircularProgress, Button, Checkbox, Paper, Divider, Tooltip, Skeleton, Grid, TextField, LinearProgress, Alert } from "@mui/material";
3
+ import { Link } from "react-router";
4
+ import MuiLink from "@mui/material/Link";
3
5
  import CloseIcon from "@mui/icons-material/Close";
4
6
  import { red, orange, green, grey } from "@mui/material/colors";
5
7
  import FeedbackCard from "../../Cards/FeedbackCard/FeedbackCard";
6
8
  import AutoFixHighIcon from "@mui/icons-material/AutoFixHigh";
7
9
  import NualaHeadphonesBackground from "../../img/NualaHeadphonesBackground.svg";
8
10
  import CheckCircleIcon from "@mui/icons-material/CheckCircle";
11
+ import { AutoFixHigh } from "@mui/icons-material";
9
12
  import useConfirm from "../../hooks/useConfirm";
10
13
  import NualaCreating from "../../Misc/NualaCreating/NualaCreating";
14
+ import ResponsiveDialog from "../ResponsiveDialog/ResponsiveDialog";
15
+ import PDFViewer from "../PDFViewer/PDFViewer";
16
+ import level1Rubric from "../PDFViewer/rubrics/level1Rubric.pdf";
17
+ import level2Rubric from "../PDFViewer/rubrics/level2Rubric.pdf";
18
+ import level3Rubric from "../PDFViewer/rubrics/level3Rubric.pdf";
19
+ import level4Rubric from "../PDFViewer/rubrics/level4Rubric.pdf";
11
20
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
12
21
  export default function GroupFeedbackDialog({
13
22
  open = false,
@@ -44,7 +53,20 @@ export default function GroupFeedbackDialog({
44
53
  const [hasReachedBottom, setHasReachedBottom] = useState(false);
45
54
  const [isGeneratingAll, setIsGeneratingAll] = useState(false);
46
55
  const [isGeneratingIndividual, setIsGeneratingIndividual] = useState(false);
56
+ const [customFeedbackOpen, setCustomFeedbackOpen] = useState(false);
57
+ const [customFeedbackText, setCustomFeedbackText] = useState("");
58
+ const [openPDF, setOpenPDF] = useState(false);
59
+ const [pageNumber, setPageNumber] = useState(1);
60
+ const [scale, setScale] = useState(1);
47
61
  const isAnyGenerating = isGeneratingAll || isGeneratingIndividual;
62
+ const pendingGenerateRef = useRef(null);
63
+ const rubricMap = {
64
+ 1: level1Rubric,
65
+ 2: level2Rubric,
66
+ 3: level3Rubric,
67
+ 4: level4Rubric
68
+ };
69
+ const pdfUrl = rubricMap[selectedRecording?.level] || level1Rubric;
48
70
  const [confirm] = useConfirm(t);
49
71
  const bottomRefCallback = useCallback(node => {
50
72
  if (node) {
@@ -59,6 +81,12 @@ export default function GroupFeedbackDialog({
59
81
  node._observer = observer;
60
82
  }
61
83
  }, []);
84
+ const handleOpenPDF = () => {
85
+ setOpenPDF(true);
86
+ };
87
+ const handleClosePDF = () => {
88
+ setOpenPDF(false);
89
+ };
62
90
  useEffect(() => {
63
91
  // dialog is shared between discussions, so reset when teacher selects another discussion
64
92
  setGenerateAllUsed(false);
@@ -69,7 +97,42 @@ export default function GroupFeedbackDialog({
69
97
  setCanSubmit({});
70
98
  setHasReachedBottom(false);
71
99
  setIsGeneratingAll(false);
100
+ pendingGenerateRef.current = null;
72
101
  }, [groupedId]);
102
+ const openCustomPromptForAll = () => {
103
+ pendingGenerateRef.current = {
104
+ type: "all"
105
+ };
106
+ setCustomFeedbackText("");
107
+ setCustomFeedbackOpen(true);
108
+ };
109
+ const openCustomPromptForSingle = discussionData => {
110
+ pendingGenerateRef.current = {
111
+ type: "single",
112
+ payload: discussionData
113
+ };
114
+ setCustomFeedbackText("");
115
+ setCustomFeedbackOpen(true);
116
+ };
117
+ const handleCustomPromptCancel = () => {
118
+ setCustomFeedbackOpen(false);
119
+ pendingGenerateRef.current = null;
120
+ setCustomFeedbackText("");
121
+ };
122
+ const handleCustomPromptContinue = async () => {
123
+ try {
124
+ const pending = pendingGenerateRef.current;
125
+ pendingGenerateRef.current = null;
126
+ if (!pending) return;
127
+ if (pending.type === "all") {
128
+ await handleGenerateFeedbackForAll();
129
+ } else if (pending.type === "single") {
130
+ await handleGenerateFeedback(pending.payload);
131
+ }
132
+ } finally {
133
+ setCustomFeedbackOpen(false);
134
+ }
135
+ };
73
136
  useEffect(() => {
74
137
  if (pendingSubmissions === 0 && submitTrigger !== null) {
75
138
  setCheckedSubmissions({});
@@ -137,7 +200,9 @@ export default function GroupFeedbackDialog({
137
200
  }).filter(member => member.name !== "Unknown");
138
201
  };
139
202
  const handleGenerateFeedback = async discussionData => {
140
- if (handleBatchGradeConversations) {
203
+ if (!handleBatchGradeConversations) return;
204
+ setIsGeneratingIndividual(true);
205
+ try {
141
206
  const joinedMembersWithNames = getJoinedMembersNames(discussionData.joinedMembers || [], userData);
142
207
  await handleBatchGradeConversations([{
143
208
  meetingId: discussionData.meetingId || discussionData.meetingID,
@@ -148,8 +213,11 @@ export default function GroupFeedbackDialog({
148
213
  meetingPrompt: discussionData.meetingPrompt,
149
214
  learnLang: learnLang,
150
215
  joinedMembers: discussionData.joinedMembers || [],
151
- joinedMembersNames: joinedMembersWithNames
216
+ joinedMembersNames: joinedMembersWithNames,
217
+ customFeedback: customFeedbackText
152
218
  }]);
219
+ } finally {
220
+ setIsGeneratingIndividual(false);
153
221
  }
154
222
  };
155
223
  const handleGenerateFeedbackForAll = async () => {
@@ -171,7 +239,8 @@ export default function GroupFeedbackDialog({
171
239
  meetingPrompt: discussion.meetingPrompt || submissionForThisDiscussion.meetingPrompt,
172
240
  learnLang: learnLang,
173
241
  joinedMembers: submissionForThisDiscussion.joinedMembers || [],
174
- joinedMembersNames: joinedMembersWithNames
242
+ joinedMembersNames: joinedMembersWithNames,
243
+ customFeedback: customFeedbackText
175
244
  };
176
245
  });
177
246
  if (conversationsToGrade.length > 0 && handleBatchGradeConversations) {
@@ -478,7 +547,7 @@ export default function GroupFeedbackDialog({
478
547
  color: "secondary",
479
548
  endIcon: /*#__PURE__*/_jsx(AutoFixHighIcon, {}),
480
549
  disabled: selectedCount === 0 || isGeneratingAll,
481
- onClick: handleGenerateFeedbackForAll,
550
+ onClick: openCustomPromptForAll,
482
551
  sx: {
483
552
  fontWeight: 600
484
553
  },
@@ -528,7 +597,7 @@ export default function GroupFeedbackDialog({
528
597
  hasSubmission: hasSubmission,
529
598
  aiGrade: aiGrade[discussionData.meetingId || discussionData.meetingID] || {},
530
599
  existingFeedback: feedbackByMeetingId[feedbackMeetingId] || {},
531
- onGenerateFeedback: () => handleGenerateFeedback(discussionData),
600
+ onGenerateFeedback: () => openCustomPromptForSingle(discussionData),
532
601
  onSubmitFeedback: handleSubmitFeedback,
533
602
  setSelectedRecording: setSelectedRecording,
534
603
  selectedRecording: selectedRecording,
@@ -556,53 +625,83 @@ export default function GroupFeedbackDialog({
556
625
  })]
557
626
  })
558
627
  })]
559
- }), /*#__PURE__*/_jsx(Dialog, {
560
- open: isAnyGenerating,
628
+ }), /*#__PURE__*/_jsx(ResponsiveDialog, {
629
+ open: customFeedbackOpen,
630
+ handleClose: handleCustomPromptCancel,
631
+ handleSubmit: handleCustomPromptContinue,
632
+ submitText: t("generate_feedback"),
561
633
  maxWidth: "sm",
562
- fullWidth: true,
563
- PaperProps: {
564
- sx: {
565
- borderRadius: 3,
566
- overflow: "visible"
567
- }
568
- },
569
- children: /*#__PURE__*/_jsx(DialogContent, {
570
- sx: {
571
- p: 4
572
- },
573
- children: /*#__PURE__*/_jsx(Grid, {
574
- container: true,
634
+ dialogTitle: isAnyGenerating ? t("generating_feedback") : t("generate_feedback"),
635
+ isSubmitDisabled: isAnyGenerating,
636
+ isExperimentalFeature: true,
637
+ startIcon: /*#__PURE__*/_jsx(AutoFixHigh, {}),
638
+ children: /*#__PURE__*/_jsxs(_Fragment, {
639
+ children: [/*#__PURE__*/_jsx(Grid, {
640
+ textAlign: "center",
641
+ sx: {
642
+ ...(!isAnyGenerating && {
643
+ display: "none"
644
+ })
645
+ },
646
+ mx: 2,
647
+ size: 12,
648
+ children: /*#__PURE__*/_jsx(NualaCreating, {})
649
+ }), /*#__PURE__*/_jsx(Grid, {
575
650
  sx: {
576
- width: "100%"
651
+ ...(!isAnyGenerating && {
652
+ display: "none"
653
+ })
577
654
  },
578
- children: /*#__PURE__*/_jsxs(Grid, {
655
+ mt: 2,
656
+ size: 12,
657
+ children: /*#__PURE__*/_jsx(LinearProgress, {})
658
+ }), !isAnyGenerating && /*#__PURE__*/_jsxs(Grid, {
659
+ container: true,
660
+ spacing: 2,
661
+ children: [/*#__PURE__*/_jsx(Grid, {
579
662
  size: 12,
580
- sx: {
581
- display: "flex",
582
- flexDirection: "column",
583
- alignItems: "center",
584
- justifyContent: "center",
585
- textAlign: "center",
586
- gap: 3,
587
- py: 2
663
+ children: /*#__PURE__*/_jsxs(Alert, {
664
+ severity: "info",
665
+ children: [t("experimental_feature_desc"), " - ", " ", /*#__PURE__*/_jsxs(Link, {
666
+ to: "/contact",
667
+ children: [t("contact_form"), "."]
668
+ })]
669
+ })
670
+ }), /*#__PURE__*/_jsx(Grid, {
671
+ size: {
672
+ xs: 12
588
673
  },
589
- children: [/*#__PURE__*/_jsx(Typography, {
590
- variant: "h5",
591
- sx: {
592
- fontWeight: 600,
593
- color: "text.primary"
594
- },
595
- children: t("generating_feedback")
596
- }), /*#__PURE__*/_jsx(Box, {
597
- sx: {
598
- maxWidth: 500,
599
- width: "100%"
600
- },
601
- children: /*#__PURE__*/_jsx(NualaCreating, {})
602
- })]
603
- })
604
- })
674
+ children: /*#__PURE__*/_jsxs(Typography, {
675
+ variant: "body2",
676
+ children: [t("generate_feedback_view_the"), " ", /*#__PURE__*/_jsx(MuiLink, {
677
+ component: "button",
678
+ variant: "body2",
679
+ onClick: handleOpenPDF,
680
+ children: t("generate_feedback_rubric_name")
681
+ }), ", ", t("generate_feedback_rubric_desc")]
682
+ })
683
+ }), /*#__PURE__*/_jsx(Grid, {
684
+ size: 12,
685
+ children: /*#__PURE__*/_jsx(TextField, {
686
+ value: customFeedbackText,
687
+ onChange: e => setCustomFeedbackText(e.target.value),
688
+ placeholder: t("custom_feedback_placeholder"),
689
+ multiline: true,
690
+ minRows: 4,
691
+ fullWidth: true
692
+ })
693
+ })]
694
+ })]
605
695
  })
696
+ }), /*#__PURE__*/_jsx(PDFViewer, {
697
+ open: openPDF,
698
+ handleClose: handleClosePDF,
699
+ pdfUrl: pdfUrl,
700
+ pageNumber: pageNumber,
701
+ setPageNumber: setPageNumber,
702
+ scale: scale,
703
+ setScale: setScale,
704
+ t: t
606
705
  })]
607
706
  });
608
707
  }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "grouped-feedback-dialog",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "main": "./GroupedFeedbackDialog.js"
6
+ }
@@ -1,11 +1,13 @@
1
1
  import { useState, useRef, useEffect } from "react";
2
2
  import { CSVLink } from "react-csv";
3
- import { Box, DialogContentText, Typography, Alert, TextField, Divider } from "@mui/material";
3
+ import { Box, DialogContentText, Typography, Alert, TextField, Divider, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper } from "@mui/material";
4
4
  import { makeStyles } from "tss-react/mui";
5
5
  import FileCopyIcon from "@mui/icons-material/FileCopy";
6
6
  import DownloadIcon from "@mui/icons-material/Download";
7
+ import FrontHandIcon from '@mui/icons-material/FrontHand';
8
+ import WavingHandIcon from '@mui/icons-material/WavingHand';
9
+ import BadgeIcon from '@mui/icons-material/Badge';
7
10
  import PhraseList from "../../Lists/Phrases/Phrases";
8
- import ExampleCSVImage from "./upload-phrases-csv.png";
9
11
  import ParseScriptErrors from "../../Lists/ParseScriptErrors/ParseScriptErrors";
10
12
  import DefaultButton from "../../Misc/DefaultColourButton/DefaultColourButton";
11
13
  import CSVUploader from "../../Misc/CSVUploader/CSVUploader";
@@ -19,7 +21,6 @@ import DialogContent from "@mui/material/DialogContent";
19
21
  import useMediaQuery from "@mui/material/useMediaQuery";
20
22
  import { useTheme } from "@mui/material/styles";
21
23
  import Popper from "@mui/material/Popper";
22
- import Paper from "@mui/material/Paper";
23
24
  import Grow from "@mui/material/Grow";
24
25
  import ClickAwayListener from "@mui/material/ClickAwayListener";
25
26
  import MenuItem from "@mui/material/MenuItem";
@@ -29,14 +30,17 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
29
30
  const phrasesData = [{
30
31
  phrase: "Hello",
31
32
  translation: "hola",
33
+ definitions: "a greeting",
32
34
  image: "https://images.nualang.com/eyJidWNrZXQiOiJudWFsYW5nLWltYWdlcy1wcm9kIiwia2V5IjoicGhyYXNlcy9EYTNXcThzUUktMTYwMzg4Nzg3MjU2NS53ZWJwIn0="
33
35
  }, {
34
36
  phrase: "Goodbye",
35
37
  translation: "adios",
38
+ definitions: "a farewell",
36
39
  image: "https://images.nualang.com/eyJidWNrZXQiOiJudWFsYW5nLWltYWdlcy1wcm9kIiwia2V5IjoicGhyYXNlcy9RbXBFcWZYdHItMTYwMzg4Nzk2MjAzMC53ZWJwIn0="
37
40
  }, {
38
41
  phrase: "My name is",
39
42
  translation: "mi nombre es | me llamo",
43
+ definitions: "used to introduce yourself",
40
44
  image: "https://images.nualang.com/eyJidWNrZXQiOiJudWFsYW5nLWltYWdlcy1wcm9kIiwia2V5IjoicGhyYXNlcy9hREFmczhFeTEtMTYwMzg5MDM2MjAxNy53ZWJwIn0="
41
45
  }];
42
46
  const phrasesHeaders = [{
@@ -45,6 +49,9 @@ const phrasesHeaders = [{
45
49
  }, {
46
50
  label: "translations",
47
51
  key: "translation"
52
+ }, {
53
+ label: "definitions",
54
+ key: "definitions"
48
55
  }, {
49
56
  label: "image",
50
57
  key: "image"
@@ -54,6 +61,22 @@ const PhrasesCsvLink = {
54
61
  data: phrasesData,
55
62
  filename: "phrases_template.csv"
56
63
  };
64
+ const csvStructureExampleRows = [{
65
+ phrase: "hello",
66
+ translations: "hola",
67
+ definitions: "A word used to greet someone",
68
+ image: FrontHandIcon
69
+ }, {
70
+ phrase: "goodbye",
71
+ translations: "adios",
72
+ definitions: "A word used when leaving someone",
73
+ image: WavingHandIcon
74
+ }, {
75
+ phrase: "my name is",
76
+ translations: "mi nombre es | me llamo",
77
+ definitions: "A phrase used to tell someone your name",
78
+ image: BadgeIcon
79
+ }];
57
80
  const useStyles = makeStyles()(theme => ({
58
81
  parseErrorsBox: {
59
82
  marginTop: theme.spacing()
@@ -65,10 +88,6 @@ const useStyles = makeStyles()(theme => ({
65
88
  dialog: {
66
89
  marginBottom: "15px"
67
90
  },
68
- image: {
69
- height: "auto",
70
- width: "100%"
71
- },
72
91
  csvButton: {
73
92
  textDecoration: "none"
74
93
  },
@@ -92,6 +111,7 @@ function parsePhraseWithAlternatives(phraseString) {
92
111
  * - phrase: Primary phrase with optional alternatives separated by '|'
93
112
  * Example: "Hello|Hi|Hey"
94
113
  * - translations: Multiple translations separated by '|'
114
+ * - definitions: Multiple definitions separated by '|'
95
115
  * - image: Optional image URL
96
116
  */
97
117
 
@@ -135,10 +155,12 @@ function UploadPhrases({
135
155
  alternatives
136
156
  } = parsePhraseWithAlternatives(data.phrase);
137
157
  const formattedTranslations = data.translations.split("|").map(t => t.trim().replace(/^"|"$/g, "").trim());
158
+ const formattedDefinitions = data.definitions ? data.definitions.split("|").map(d => d.trim().replace(/^"|"$/g, "").trim()).filter(Boolean) : [];
138
159
  return {
139
160
  phrase: primary,
140
161
  alternativeVersions: alternatives,
141
162
  translations: formattedTranslations,
163
+ definitions: formattedDefinitions,
142
164
  image: data.image ? data.image.trim() : "",
143
165
  voices: [],
144
166
  idx: index
@@ -175,8 +197,6 @@ function UploadPhrases({
175
197
  setErrorMessage("File must be in the .csv format");
176
198
  return;
177
199
  } else if (results.errors && results.errors.length) {
178
- // formats errors coming from CSVReader into common error format
179
- // used for validating rows
180
200
  const formattedErrors = results.errors.reduce((acc, cur) => {
181
201
  acc.push({
182
202
  row: cur.row,
@@ -212,10 +232,51 @@ function UploadPhrases({
212
232
  children: t("upload_phrases_desc")
213
233
  }), /*#__PURE__*/_jsx(Box, {
214
234
  py: 1,
215
- children: /*#__PURE__*/_jsx("img", {
216
- className: classes.image,
217
- src: ExampleCSVImage,
218
- alt: ""
235
+ children: /*#__PURE__*/_jsx(TableContainer, {
236
+ component: Paper,
237
+ variant: "outlined",
238
+ children: /*#__PURE__*/_jsxs(Table, {
239
+ size: "small",
240
+ "aria-label": "CSV structure example",
241
+ children: [/*#__PURE__*/_jsx(TableHead, {
242
+ children: /*#__PURE__*/_jsxs(TableRow, {
243
+ children: [/*#__PURE__*/_jsx(TableCell, {
244
+ children: /*#__PURE__*/_jsx("strong", {
245
+ children: t("phrase")
246
+ })
247
+ }), /*#__PURE__*/_jsx(TableCell, {
248
+ children: /*#__PURE__*/_jsx("strong", {
249
+ children: t("translations")
250
+ })
251
+ }), /*#__PURE__*/_jsx(TableCell, {
252
+ children: /*#__PURE__*/_jsx("strong", {
253
+ children: t("definitions")
254
+ })
255
+ }), /*#__PURE__*/_jsx(TableCell, {
256
+ children: /*#__PURE__*/_jsx("strong", {
257
+ children: t("image")
258
+ })
259
+ })]
260
+ })
261
+ }), /*#__PURE__*/_jsx(TableBody, {
262
+ children: csvStructureExampleRows.map((row, index) => {
263
+ const ImageIcon = row.image;
264
+ return /*#__PURE__*/_jsxs(TableRow, {
265
+ children: [/*#__PURE__*/_jsx(TableCell, {
266
+ children: row.phrase
267
+ }), /*#__PURE__*/_jsx(TableCell, {
268
+ children: row.translations
269
+ }), /*#__PURE__*/_jsx(TableCell, {
270
+ children: row.definitions
271
+ }), /*#__PURE__*/_jsx(TableCell, {
272
+ children: /*#__PURE__*/_jsx(ImageIcon, {
273
+ fontSize: "small"
274
+ })
275
+ })]
276
+ }, `${row.phrase}-${index}`);
277
+ })
278
+ })]
279
+ })
219
280
  })
220
281
  }), /*#__PURE__*/_jsxs(Box, {
221
282
  pb: 2,
@@ -257,7 +318,7 @@ function UploadPhrases({
257
318
  id: "csvDataInput",
258
319
  name: "csvDataInput",
259
320
  label: t("paste_your_csv_data"),
260
- placeholder: `phrase, translations\n"Hola", "Hello | Hi"\n"Adios", "Goodbye"`,
321
+ placeholder: `phrase, translations, definitions\n"Hola", "Hello | Hi", "a greeting"\n"Adios", "Goodbye", "a farewell"`,
261
322
  value: csvDataInput,
262
323
  fullWidth: true,
263
324
  multiline: true,