@truedat/ai 7.3.2 → 7.3.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/ai",
3
- "version": "7.3.2",
3
+ "version": "7.3.4",
4
4
  "description": "Truedat Web Artificial Intelligence package",
5
5
  "sideEffects": false,
6
6
  "jsnext:main": "src/index.js",
@@ -35,7 +35,7 @@
35
35
  "@testing-library/react": "^12.0.0",
36
36
  "@testing-library/react-hooks": "^8.0.1",
37
37
  "@testing-library/user-event": "^13.2.1",
38
- "@truedat/test": "7.3.2",
38
+ "@truedat/test": "7.3.4",
39
39
  "babel-jest": "^28.1.0",
40
40
  "babel-plugin-dynamic-import-node": "^2.3.3",
41
41
  "babel-plugin-lodash": "^3.3.4",
@@ -85,7 +85,7 @@
85
85
  ]
86
86
  },
87
87
  "dependencies": {
88
- "@truedat/core": "7.3.2",
88
+ "@truedat/core": "7.3.4",
89
89
  "prop-types": "^15.8.1",
90
90
  "react-hook-form": "^7.45.4",
91
91
  "react-intl": "^5.20.10",
@@ -97,5 +97,5 @@
97
97
  "react-dom": ">= 16.8.6 < 17",
98
98
  "semantic-ui-react": ">= 2.0.3 < 2.2"
99
99
  },
100
- "gitHead": "3a5d8fc5160ffabc1ef86251883070331045f738"
100
+ "gitHead": "681c0d026b5477fadf9b53fe5242cfb0ed11d973"
101
101
  }
package/src/api.js CHANGED
@@ -13,6 +13,9 @@ const API_PROVIDER_CHAT = "/api/providers/:id/chat_completion";
13
13
  const API_SUGGESTIONS_AVAILABILITY_CHECK =
14
14
  "/api/suggestions/availability_check";
15
15
  const API_SUGGESTIONS_REQUEST = "/api/suggestions/request";
16
+ const API_TRANSLATIONS_AVAILABILITY_CHECK =
17
+ "/api/translations/availability_check";
18
+ const API_TRANSLATIONS_REQUEST = "/api/translations/request";
16
19
 
17
20
  export {
18
21
  API_ACTION,
@@ -29,4 +32,6 @@ export {
29
32
  API_PROVIDER_CHAT,
30
33
  API_SUGGESTIONS_AVAILABILITY_CHECK,
31
34
  API_SUGGESTIONS_REQUEST,
35
+ API_TRANSLATIONS_AVAILABILITY_CHECK,
36
+ API_TRANSLATIONS_REQUEST,
32
37
  };
@@ -0,0 +1,613 @@
1
+ import _ from "lodash/fp";
2
+ import React, { useEffect, useState } from "react";
3
+ import PropTypes from "prop-types";
4
+ import {
5
+ Button,
6
+ Checkbox,
7
+ Divider,
8
+ Form,
9
+ Grid,
10
+ Icon,
11
+ Modal,
12
+ Popup,
13
+ Segment,
14
+ Table,
15
+ TableBody,
16
+ TableCell,
17
+ TableHeader,
18
+ TableHeaderCell,
19
+ TableRow,
20
+ } from "semantic-ui-react";
21
+ import { useIntl } from "react-intl";
22
+ import FieldViewerValue from "@truedat/df/components/FieldViewerValue";
23
+ import { splitTranslatableFields } from "@truedat/core/services/i18nContent";
24
+ import { useRequestTranslation } from "../hooks/useTranslations";
25
+
26
+ const TranslationModal = ({
27
+ open,
28
+ onClose,
29
+ i18nContent,
30
+ template,
31
+ resourceType,
32
+ domainId,
33
+ setTranslations,
34
+ }) => {
35
+ const { formatMessage } = useIntl();
36
+ const [translateName, setTranslateName] = useState(true);
37
+ const [translateTemplate, settranslateTemplate] = useState();
38
+ const [translateContent, setTranslateContent] = useState();
39
+ const [translateResult, setTranslateResult] = useState();
40
+
41
+ const { trigger, isMutating } = useRequestTranslation();
42
+
43
+ const { translatable } = splitTranslatableFields(template);
44
+ const translatableFields = _.keysIn(translatable);
45
+
46
+ const defaultContent = _.find({ is_default: true })(i18nContent);
47
+
48
+ const initializeTemplate = () => {
49
+ const templateContent = _.flow(
50
+ _.map((group) => {
51
+ const newFields = _.flow(
52
+ _.map((field) =>
53
+ _.includes(field.name)(translatableFields) &&
54
+ (defaultContent.content[field.name] ||
55
+ (field.default?.origin === "default" &&
56
+ !_.isEmpty(field.default?.value)))
57
+ ? {
58
+ ...field,
59
+ checked: true,
60
+ }
61
+ : null
62
+ ),
63
+ _.filter(_.isObject)
64
+ )(group.fields);
65
+
66
+ return {
67
+ ...group,
68
+ fields: newFields,
69
+ checked: true,
70
+ };
71
+ }),
72
+ _.filter((group) => !_.isEmpty(group.fields))
73
+ )(template?.content);
74
+ settranslateTemplate(templateContent);
75
+ };
76
+
77
+ const initializeContent = () => {
78
+ const locales = _.flow(
79
+ _.map((locale) => ({
80
+ ...locale,
81
+ checked: !locale.is_default,
82
+ })),
83
+ _.keyBy("lang")
84
+ )(i18nContent);
85
+ setTranslateContent(locales);
86
+ };
87
+
88
+ const initializeTranslation = () => {
89
+ initializeTemplate();
90
+ initializeContent();
91
+ setTranslateName(true);
92
+ setTranslateResult(null);
93
+ };
94
+
95
+ useEffect(() => {
96
+ if (i18nContent && template?.content) {
97
+ initializeTemplate();
98
+ initializeContent();
99
+ setTranslateName(!_.isEmpty(defaultContent.name));
100
+ }
101
+ }, [i18nContent, template]);
102
+
103
+ const handleGroupChecked = (groupName) => {
104
+ const updatedObject = _.map((group) => {
105
+ if (group.name === groupName) {
106
+ const groupCheck =
107
+ group.checked === "indeterminate" ? true : !group.checked;
108
+ return {
109
+ ...group,
110
+ checked: groupCheck,
111
+ fields: _.map((field) => ({
112
+ ...field,
113
+ indeterminate: false,
114
+ checked: groupCheck,
115
+ }))(group.fields),
116
+ };
117
+ }
118
+ return group;
119
+ })(translateTemplate);
120
+ settranslateTemplate(updatedObject);
121
+ };
122
+
123
+ const handleFieldChecked = (groupName, fieldName) => {
124
+ const updatedObject = _.map((group) => {
125
+ if (group.name === groupName) {
126
+ const updatedFields = _.map((field) => {
127
+ if (field.name === fieldName) {
128
+ return {
129
+ ...field,
130
+ checked: !field.checked,
131
+ };
132
+ }
133
+ return field;
134
+ })(group.fields);
135
+ const groupCheck = _.cond([
136
+ [_.every(["checked", true]), _.stubTrue],
137
+ [_.every(["checked", false]), _.stubFalse],
138
+ [_.stubTrue, () => "indeterminate"],
139
+ ])(updatedFields);
140
+ return {
141
+ ...group,
142
+ checked: groupCheck,
143
+ fields: updatedFields,
144
+ };
145
+ }
146
+ return group;
147
+ })(translateTemplate);
148
+ settranslateTemplate(updatedObject);
149
+ };
150
+
151
+ const handleLocaleChecked = (lang, checked) => {
152
+ setTranslateContent({
153
+ ...translateContent,
154
+ [lang]: {
155
+ ...translateContent[lang],
156
+ checked,
157
+ },
158
+ });
159
+ };
160
+
161
+ const requestTranslation = () => {
162
+ const translation_body = _.flow(
163
+ _.flatMap(({ fields }) => fields),
164
+ _.filter("checked"),
165
+ _.keyBy("name"),
166
+ _.mapValues(
167
+ ({ name, default: defaultValue }) =>
168
+ defaultContent?.content[name]?.value || defaultValue?.value
169
+ ),
170
+ (values) =>
171
+ translateName
172
+ ? {
173
+ ...values,
174
+ name: defaultContent?.name,
175
+ }
176
+ : values
177
+ )(translateTemplate);
178
+
179
+ const payload = {
180
+ resource_type: resourceType,
181
+ locales: _.flow(_.filter("checked"), _.map("lang"))(translateContent),
182
+ domain_ids: [domainId],
183
+ translation_body,
184
+ };
185
+ trigger(payload)
186
+ .then(({ data: { data: data } }) => {
187
+ const translatedContent = _.flow(
188
+ _.map(({ lang }) => {
189
+ if (data[lang]) {
190
+ const { name, ...content } = data[lang];
191
+ return {
192
+ ...translateContent[lang],
193
+ name: name || "",
194
+ content: { ...translateContent[lang].content, ...content },
195
+ };
196
+ } else {
197
+ return translateContent[lang];
198
+ }
199
+ }),
200
+ _.keyBy("lang")
201
+ )(translateContent);
202
+ setTranslateContent(translatedContent);
203
+ setTranslateResult("success");
204
+ })
205
+ .catch(() => {
206
+ setTranslateResult("error");
207
+ });
208
+ };
209
+
210
+ const applyTranslation = () => {
211
+ const updatedI18nContent = _.mapValues((locale) => {
212
+ return translateContent[locale.lang].content
213
+ ? _.flow(
214
+ _.mapValues((value) =>
215
+ value.value && value.origin ? value : { value, origin: "ai" }
216
+ ),
217
+ (content) => ({
218
+ ...i18nContent[locale.lang],
219
+ name:
220
+ translateContent[locale.lang].name ||
221
+ i18nContent[locale.lang].name,
222
+ content: {
223
+ ...i18nContent[locale.lang].content,
224
+ ...content,
225
+ },
226
+ })
227
+ )(translateContent[locale.lang].content)
228
+ : locale;
229
+ })(i18nContent);
230
+
231
+ setTranslations(updatedI18nContent);
232
+ onClose();
233
+ initializeTranslation();
234
+ };
235
+
236
+ const renderRequest = () => (
237
+ <Grid>
238
+ <Grid.Row>
239
+ <Grid.Column
240
+ width={12}
241
+ style={{ display: "flex", flexDirection: "column" }}
242
+ >
243
+ <Segment style={{ flex: 1 }}>
244
+ <h2>
245
+ {formatMessage({
246
+ id: "ai.translation.fields_to_translate",
247
+ defaultMessage: "Fields to translate",
248
+ })}
249
+ </h2>
250
+ <h3>
251
+ {formatMessage({
252
+ id: "ai.translation.concept_name",
253
+ defaultMessage: "Concept name",
254
+ })}
255
+ </h3>
256
+ {!_.isEmpty(defaultContent.name) ? (
257
+ <dl>
258
+ <dt>
259
+ <h4>
260
+ <Checkbox
261
+ label={formatMessage({
262
+ id: "ai.translation.name",
263
+ defaultMessage: "Name",
264
+ })}
265
+ checked={translateName}
266
+ onChange={() => setTranslateName(!translateName)}
267
+ />
268
+ </h4>
269
+ </dt>
270
+ <dd>{defaultContent?.name}</dd>
271
+ </dl>
272
+ ) : null}
273
+ {_.map(({ name: groupName, fields, checked }) => (
274
+ <React.Fragment key={groupName}>
275
+ <Divider />
276
+ <h3>
277
+ <Checkbox
278
+ label={groupName}
279
+ checked={!!checked}
280
+ indeterminate={checked === "indeterminate"}
281
+ onChange={() => handleGroupChecked(groupName, null)}
282
+ />
283
+ </h3>
284
+ <dl>
285
+ {_.map(
286
+ ({
287
+ name: fieldName,
288
+ label,
289
+ checked,
290
+ default: defaultValue,
291
+ type,
292
+ widget,
293
+ }) => (
294
+ <React.Fragment key={fieldName}>
295
+ <dt>
296
+ <h4>
297
+ <Checkbox
298
+ label={label}
299
+ checked={checked}
300
+ onChange={() =>
301
+ handleFieldChecked(groupName, fieldName)
302
+ }
303
+ />
304
+ {!defaultContent?.content[fieldName] &&
305
+ !_.isEmpty(defaultValue?.value) ? (
306
+ <Popup
307
+ content={formatMessage({
308
+ id: "ai.translation.default_value",
309
+ defaultMessage:
310
+ "This value is set by default",
311
+ })}
312
+ trigger={
313
+ <Icon
314
+ circular
315
+ inverted
316
+ color="orange"
317
+ name="exclamation"
318
+ size="tiny"
319
+ />
320
+ }
321
+ />
322
+ ) : null}
323
+ {defaultContent?.content[fieldName]?.origin ===
324
+ "ai" ? (
325
+ <Popup
326
+ content={formatMessage({
327
+ id: "ai.translation.ai_value",
328
+ defaultMessage: "This value is set by AI",
329
+ })}
330
+ trigger={
331
+ <Icon
332
+ circular
333
+ inverted
334
+ color="teal"
335
+ name="idea"
336
+ size="tiny"
337
+ />
338
+ }
339
+ />
340
+ ) : null}
341
+ </h4>
342
+ </dt>
343
+ <dd>
344
+ <FieldViewerValue
345
+ type={type}
346
+ value={
347
+ defaultContent?.content[fieldName]?.value ||
348
+ defaultValue.value
349
+ }
350
+ widget={widget}
351
+ />
352
+ </dd>
353
+ </React.Fragment>
354
+ )
355
+ )(fields)}
356
+ </dl>
357
+ </React.Fragment>
358
+ ))(translateTemplate)}
359
+ </Segment>
360
+ </Grid.Column>
361
+ <Grid.Column
362
+ width={4}
363
+ style={{ display: "flex", flexDirection: "column" }}
364
+ >
365
+ <Segment style={{ flex: 1 }}>
366
+ <h2>
367
+ {formatMessage({
368
+ id: "ai.translation.available_languages",
369
+ defaultMessage: "Available languages",
370
+ })}
371
+ </h2>
372
+ <Form>
373
+ {_.map(({ lang, is_required, checked }) => (
374
+ <Form.Field key={lang}>
375
+ <Checkbox
376
+ key={lang}
377
+ value={lang}
378
+ label={`${formatMessage({
379
+ id: `i18n.messages.locale.${lang}`,
380
+ defaultMessage: lang,
381
+ })} ${is_required ? "*" : ""}`}
382
+ checked={checked}
383
+ onChange={(_e, { checked }) =>
384
+ handleLocaleChecked(lang, checked)
385
+ }
386
+ />
387
+ </Form.Field>
388
+ ))(_.omitBy("is_default")(translateContent))}
389
+ </Form>
390
+ </Segment>
391
+ </Grid.Column>
392
+ </Grid.Row>
393
+ <Grid.Row textAlign="right">
394
+ <Grid.Column>
395
+ <Button
396
+ disabled={isMutating}
397
+ secondary
398
+ onClick={() => {
399
+ initializeTranslation(), onClose();
400
+ }}
401
+ >
402
+ {formatMessage({ id: "actions.cancel", defaultMessage: "Cancel" })}
403
+ </Button>
404
+ <Button
405
+ disabled={
406
+ isMutating ||
407
+ (!_.some("checked")(translateTemplate) && !translateName) ||
408
+ !_.some("checked")(translateContent)
409
+ }
410
+ primary
411
+ onClick={() => requestTranslation()}
412
+ >
413
+ {formatMessage({
414
+ id: "actions.request_translation",
415
+ defaultMessage: "Request translation",
416
+ })}
417
+ </Button>
418
+ </Grid.Column>
419
+ </Grid.Row>
420
+ </Grid>
421
+ );
422
+
423
+ const renderResult = () => (
424
+ <Grid>
425
+ <Grid.Row>
426
+ <Grid.Column width={16}>
427
+ <h2>
428
+ {formatMessage({
429
+ id: "ai.translation.preview",
430
+ defaultMessage: "Translation preview",
431
+ })}
432
+ </h2>
433
+ <Table definition>
434
+ <TableHeader>
435
+ <TableRow>
436
+ <TableHeaderCell />
437
+ {_.map(({ lang, is_default, is_required, checked }) =>
438
+ checked || is_default ? (
439
+ <TableHeaderCell key={lang}>
440
+ {`${formatMessage({
441
+ id: `i18n.messages.locale.${lang}`,
442
+ defaultMessage: lang,
443
+ })} `}
444
+ {is_default ? (
445
+ <Icon name="star" color="yellow" />
446
+ ) : is_required ? (
447
+ "*"
448
+ ) : null}
449
+ </TableHeaderCell>
450
+ ) : null
451
+ )(translateContent)}
452
+ </TableRow>
453
+ </TableHeader>
454
+ <TableBody>
455
+ {translateName ? (
456
+ <TableRow>
457
+ <TableCell key={0}>
458
+ {formatMessage({
459
+ id: "ai.translation.concept_term_name",
460
+ defaultMessage: "Concept term name",
461
+ })}
462
+ </TableCell>
463
+ {_.map(({ lang, name, is_default, checked }) =>
464
+ is_default || checked ? (
465
+ <TableCell key={lang}>{name}</TableCell>
466
+ ) : null
467
+ )(translateContent)}
468
+ </TableRow>
469
+ ) : null}
470
+
471
+ {_.map(({ fields }) =>
472
+ _.map(
473
+ ({
474
+ name: fieldName,
475
+ label,
476
+ checked,
477
+ default: defaultValue,
478
+ type,
479
+ widget,
480
+ }) =>
481
+ checked ? (
482
+ <TableRow key={fieldName}>
483
+ <TableCell>{label}</TableCell>
484
+ {_.map(({ lang, content, is_default, checked }) =>
485
+ is_default || checked ? (
486
+ <TableCell key={lang}>
487
+ <FieldViewerValue
488
+ type={type}
489
+ value={
490
+ content[fieldName]?.value ||
491
+ content[fieldName] ||
492
+ defaultValue?.value
493
+ }
494
+ widget={widget}
495
+ />
496
+ </TableCell>
497
+ ) : null
498
+ )(translateContent)}
499
+ </TableRow>
500
+ ) : null
501
+ )(fields)
502
+ )(translateTemplate)}
503
+ </TableBody>
504
+ </Table>
505
+ </Grid.Column>
506
+ </Grid.Row>
507
+ <Grid.Row>
508
+ <Grid.Column textAlign="left" width={8}>
509
+ <Button
510
+ secondary
511
+ onClick={() => {
512
+ setTranslateResult(null);
513
+ }}
514
+ >
515
+ {formatMessage({
516
+ id: "actions.go_back",
517
+ defaultMessage: "Go back",
518
+ })}
519
+ </Button>
520
+ </Grid.Column>
521
+ <Grid.Column textAlign="right" width={8}>
522
+ <Button
523
+ secondary
524
+ onClick={() => {
525
+ initializeTranslation();
526
+ onClose();
527
+ }}
528
+ >
529
+ {formatMessage({ id: "actions.cancel", defaultMessage: "Cancel" })}
530
+ </Button>
531
+ <Button primary onClick={applyTranslation}>
532
+ {formatMessage({ id: "actions.apply", defaultMessage: "Apply" })}
533
+ </Button>
534
+ </Grid.Column>
535
+ </Grid.Row>
536
+ </Grid>
537
+ );
538
+
539
+ const renderError = () => (
540
+ <Grid>
541
+ <Grid.Row>
542
+ <Grid.Column width={16}>
543
+ <h2>
544
+ {formatMessage({
545
+ id: "ai.translation.error",
546
+ defaultMessage: "Translation error",
547
+ })}
548
+ </h2>
549
+ <p>
550
+ {formatMessage({
551
+ id: "ai.translation.error_message",
552
+ defaultMessage:
553
+ "There was an error while translating the content",
554
+ })}
555
+ </p>
556
+ </Grid.Column>
557
+ </Grid.Row>
558
+ <Grid.Row>
559
+ <Grid.Column width={16} textAlign="right">
560
+ <Button
561
+ secondary
562
+ onClick={() => {
563
+ initializeTranslation();
564
+ onClose();
565
+ }}
566
+ >
567
+ {formatMessage({ id: "actions.cancel", defaultMessage: "Cancel" })}
568
+ </Button>
569
+ </Grid.Column>
570
+ </Grid.Row>
571
+ </Grid>
572
+ );
573
+
574
+ return (
575
+ <Modal
576
+ size="fullscreen"
577
+ open={open}
578
+ onClose={onClose}
579
+ closeIcon
580
+ className="translation-modal"
581
+ >
582
+ <Modal.Header>
583
+ <h1>
584
+ {formatMessage({
585
+ id: "ai.translation.header",
586
+ defaultMessage: "Concept translation",
587
+ })}
588
+ </h1>
589
+ </Modal.Header>
590
+ <Modal.Content>
591
+ <Modal.Description>
592
+ {translateResult === "success"
593
+ ? renderResult()
594
+ : translateResult === "error"
595
+ ? renderError()
596
+ : renderRequest()}
597
+ </Modal.Description>
598
+ </Modal.Content>
599
+ </Modal>
600
+ );
601
+ };
602
+
603
+ TranslationModal.propTypes = {
604
+ open: PropTypes.bool,
605
+ onClose: PropTypes.func,
606
+ i18nContent: PropTypes.object,
607
+ template: PropTypes.object,
608
+ resourceType: PropTypes.string,
609
+ domainId: PropTypes.number,
610
+ setTranslations: PropTypes.func,
611
+ };
612
+
613
+ export default TranslationModal;