@truedat/ai 7.3.3 → 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.
@@ -0,0 +1,351 @@
1
+ import React from "react";
2
+ import { act, fireEvent, waitFor } from "@testing-library/react";
3
+ import { render } from "@truedat/test/render";
4
+ import TranslationModal from "../TranslationModal";
5
+
6
+ jest.mock("@truedat/ai/hooks/useTranslations", () => ({
7
+ useRequestTranslation: jest.fn(() => ({
8
+ trigger: jest.fn(() =>
9
+ Promise.resolve({
10
+ data: {
11
+ data: {
12
+ de: {
13
+ "concept-field-1": "Ein Messer in der Dunkelheit",
14
+ "concept-field-2": "Das große Abenteuer von Heron dem Krieger",
15
+ "concept-field-default": "Dieser Wert muss bestätigt werden",
16
+ concept_term_name: "Konzeptentwurf",
17
+ "secondary-field-4":
18
+ "Eine große Macht bringt große Verantwortung mit sich",
19
+ },
20
+ es: {
21
+ "concept-field-1": "Un cuchillo en la oscuridad",
22
+ "concept-field-2": "La gran aventura de Heron El Guerrero",
23
+ "concept-field-default": "Este valor debe confirmarse",
24
+ concept_term_name: "borrador-de-concepto",
25
+ "secondary-field-4":
26
+ "Un gran poder conlleva una gran responsabilidad",
27
+ },
28
+ fr: {
29
+ "concept-field-1": "Un couteau dans le noir",
30
+ "concept-field-2": "La grande aventure d'Heron le Guerrier",
31
+ "concept-field-default": "Cette valeur doit être confirmée",
32
+ concept_term_name: "ébauche de concept",
33
+ "secondary-field-4":
34
+ "Un grand pouvoir implique une grande responsabilité",
35
+ },
36
+ },
37
+ },
38
+ })
39
+ ),
40
+ })),
41
+ }));
42
+
43
+ const props = {
44
+ open: true,
45
+ onClose: jest.fn(),
46
+ setTranslations: jest.fn(),
47
+ domainId: 1,
48
+ resourceType: "business_concept",
49
+ i18nContent: {
50
+ en: {
51
+ id: 1,
52
+ lang: "en",
53
+ is_default: true,
54
+ is_required: true,
55
+ name: "concept-draft",
56
+ content: {
57
+ "concept-field-1": {
58
+ origin: "user",
59
+ value: "Un cuchillo en la oscuridad",
60
+ },
61
+ "concept-field-2": {
62
+ origin: "ai",
63
+ value: "La gran aventura de Heron El Guerrero",
64
+ },
65
+ "secondary-field-2": {
66
+ origin: "user",
67
+ value: "B",
68
+ },
69
+ "secondary-field-4": {
70
+ origin: "user",
71
+ value: "Un gran poder conlleva una gran responsabilidad",
72
+ },
73
+ },
74
+ },
75
+ es: {
76
+ id: 2,
77
+ lang: "es",
78
+ is_default: false,
79
+ is_required: true,
80
+ name: "",
81
+ content: {},
82
+ },
83
+ de: {
84
+ id: 3,
85
+ lang: "de",
86
+ is_default: false,
87
+ is_required: false,
88
+ name: "",
89
+ content: {},
90
+ },
91
+ fr: {
92
+ id: 4,
93
+ lang: "fr",
94
+ is_default: false,
95
+ is_required: false,
96
+ name: "",
97
+ content: {},
98
+ },
99
+ },
100
+ template: {
101
+ id: "135",
102
+ name: "concept",
103
+ label: "concept",
104
+ scope: "bg",
105
+ content: [
106
+ {
107
+ fields: [
108
+ {
109
+ cardinality: "?",
110
+ default: {
111
+ origin: "default",
112
+ value: "",
113
+ },
114
+ label: "concept-field-1",
115
+ name: "concept-field-1",
116
+ type: "string",
117
+ values: null,
118
+ widget: "string",
119
+ },
120
+ {
121
+ cardinality: "?",
122
+ default: {
123
+ origin: "default",
124
+ value: "",
125
+ },
126
+ label: "concept-field-2",
127
+ name: "concept-field-2",
128
+ type: "string",
129
+ values: null,
130
+ widget: "string",
131
+ },
132
+ {
133
+ cardinality: "?",
134
+ default: {
135
+ origin: "default",
136
+ value: "This value is to be confirmed",
137
+ },
138
+ label: "concept-field-default",
139
+ name: "concept-field-default",
140
+ type: "string",
141
+ values: null,
142
+ widget: "string",
143
+ },
144
+ ],
145
+ name: "Primary Properties",
146
+ },
147
+ {
148
+ fields: [
149
+ {
150
+ cardinality: "?",
151
+ default: {
152
+ origin: "default",
153
+ value: "",
154
+ },
155
+ label: "secondary-field-1",
156
+ name: "secondary-field-1",
157
+ type: "string",
158
+ values: null,
159
+ widget: "string",
160
+ },
161
+ {
162
+ cardinality: "?",
163
+ default: {
164
+ origin: "default",
165
+ value: "",
166
+ },
167
+ label: "secondary-field-4",
168
+ name: "secondary-field-4",
169
+ type: "string",
170
+ values: null,
171
+ widget: "string",
172
+ },
173
+ {
174
+ cardinality: "?",
175
+ default: {
176
+ origin: "default",
177
+ value: "",
178
+ },
179
+ label: "secondary-field-2",
180
+ name: "secondary-field-2",
181
+ subscribable: false,
182
+ type: "string",
183
+ values: {
184
+ fixed: ["A", "B", "C"],
185
+ },
186
+ widget: "dropdown",
187
+ },
188
+ {
189
+ cardinality: "?",
190
+ default: {
191
+ origin: "default",
192
+ value: "",
193
+ },
194
+ label: "secondary-field-3",
195
+ name: "secondary-field-3",
196
+ subscribable: false,
197
+ type: "string",
198
+ values: {
199
+ fixed: ["a", "b", "c"],
200
+ },
201
+ widget: "dropdown",
202
+ },
203
+ {
204
+ cardinality: "?",
205
+ default: {
206
+ origin: "default",
207
+ value: "a",
208
+ },
209
+ label: "secondary-field-default",
210
+ name: "secondary-field-default",
211
+ subscribable: false,
212
+ type: "string",
213
+ values: {
214
+ fixed: ["a", "b", "c"],
215
+ },
216
+ widget: "dropdown",
217
+ },
218
+ ],
219
+ name: "Secondary Group",
220
+ },
221
+ {
222
+ fields: [
223
+ {
224
+ cardinality: "?",
225
+ default: {
226
+ origin: "default",
227
+ value: "",
228
+ },
229
+ label: "tertiary-field-1",
230
+ name: "tertiary-field-1",
231
+ type: "string",
232
+ values: null,
233
+ widget: "string",
234
+ },
235
+ {
236
+ cardinality: "?",
237
+ default: {
238
+ origin: "default",
239
+ value: "",
240
+ },
241
+ label: "tertiary-field-2",
242
+ name: "tertiary-field-2",
243
+ type: "string",
244
+ values: null,
245
+ widget: "string",
246
+ },
247
+ ],
248
+ name: "Tertiary Group",
249
+ },
250
+ ],
251
+ },
252
+ };
253
+
254
+ const renderOpts = {
255
+ messages: {},
256
+ fallback: "lazy",
257
+ };
258
+
259
+ describe("TranslationModal", () => {
260
+ it("renders without crashing", () => {
261
+ const { getByText } = render(<TranslationModal {...props} />, renderOpts);
262
+ expect(getByText("Concept translation")).toBeInTheDocument();
263
+ });
264
+
265
+ it("displays the correct default locale name", () => {
266
+ const { getByText } = render(<TranslationModal {...props} />, renderOpts);
267
+ expect(getByText("concept-draft")).toBeInTheDocument();
268
+ });
269
+
270
+ it("displays the fields to translate", () => {
271
+ const { getByText } = render(<TranslationModal {...props} />, renderOpts);
272
+ expect(getByText("Fields to translate")).toBeInTheDocument();
273
+ expect(getByText("concept-field-1")).toBeInTheDocument();
274
+ expect(getByText("concept-field-2")).toBeInTheDocument();
275
+ });
276
+
277
+ it("displays available languages", () => {
278
+ const { getByText } = render(<TranslationModal {...props} />, renderOpts);
279
+ expect(getByText("Available languages")).toBeInTheDocument();
280
+ expect(getByText("es *")).toBeInTheDocument();
281
+ expect(getByText("de")).toBeInTheDocument();
282
+ expect(getByText("fr")).toBeInTheDocument();
283
+ });
284
+
285
+ it("requests translation and displays results", async () => {
286
+ const { getByText, getAllByText, getByRole } = render(
287
+ <TranslationModal {...props} />,
288
+ renderOpts
289
+ );
290
+
291
+ await act(async () => {
292
+ fireEvent.click(getByRole("button", { name: /Request Translation/i }));
293
+ });
294
+
295
+ await waitFor(() => {
296
+ expect(getByText("Translation preview")).toBeInTheDocument();
297
+ expect(getByText("Ein Messer in der Dunkelheit")).toBeInTheDocument();
298
+ expect(
299
+ getAllByText("Un cuchillo en la oscuridad").length
300
+ ).toBeGreaterThan(0);
301
+ expect(getByText("Un couteau dans le noir")).toBeInTheDocument();
302
+ });
303
+ });
304
+
305
+ it.skip("handles translation error", async () => {
306
+ jest.resetAllMocks();
307
+ jest.mock("@truedat/ai/hooks/useTranslations", () => ({
308
+ useRequestTranslation: jest.fn(() => ({
309
+ trigger: jest.fn(() => Promise.reject(new Error("Translation error"))),
310
+ })),
311
+ }));
312
+
313
+ const { getByText, getByRole } = render(
314
+ <TranslationModal {...props} />,
315
+ renderOpts
316
+ );
317
+
318
+ await act(async () => {
319
+ fireEvent.click(getByRole("button", { name: /Request Translation/i }));
320
+ });
321
+
322
+ await waitFor(() => {
323
+ expect(getByText("Translation Error")).toBeInTheDocument();
324
+ });
325
+ });
326
+
327
+ it("applies translation and closes modal", async () => {
328
+ const { getByRole } = render(<TranslationModal {...props} />, renderOpts);
329
+
330
+ await act(async () => {
331
+ fireEvent.click(getByRole("button", { name: /Request Translation/i }));
332
+ });
333
+
334
+ await waitFor(() => {
335
+ fireEvent.click(getByRole("button", { name: /Apply/i }));
336
+ });
337
+
338
+ expect(props.setTranslations).toHaveBeenCalled();
339
+ expect(props.onClose).toHaveBeenCalled();
340
+ });
341
+
342
+ it("cancels translation and closes modal", async () => {
343
+ const { getByRole } = render(<TranslationModal {...props} />, renderOpts);
344
+
345
+ await act(async () => {
346
+ fireEvent.click(getByRole("button", { name: /Cancel/i }));
347
+ });
348
+
349
+ expect(props.onClose).toHaveBeenCalled();
350
+ });
351
+ });
@@ -3,6 +3,8 @@ import { useIntl } from "react-intl";
3
3
 
4
4
  export const resourceTypes = ["data_structure", "business_concept"];
5
5
 
6
+ export const promptResourceTypes = [...resourceTypes, "translation"];
7
+
6
8
  export const providerTypes = [
7
9
  "openai",
8
10
  "azure_openai",
@@ -12,6 +14,8 @@ export const providerTypes = [
12
14
 
13
15
  export const useResourceTypeOptions = () =>
14
16
  useOptions(resourceTypes, "resourceMappings.resourceType");
17
+ export const usePromptResourceTypeOptions = () =>
18
+ useOptions(promptResourceTypes, "resourceMappings.resourceType");
15
19
  export const useProviderTypeOptions = () =>
16
20
  useOptions(providerTypes, "providers.type");
17
21
 
@@ -1,2 +1,3 @@
1
1
  import AiRoutes from "./AiRoutes";
2
- export { AiRoutes };
2
+ import TranslationModal from "./TranslationModal";
3
+ export { AiRoutes, TranslationModal };
@@ -1,5 +1,5 @@
1
1
  import _ from "lodash/fp";
2
- import React, { Fragment, useEffect } from "react";
2
+ import React, { Fragment, useEffect, useState } from "react";
3
3
  import PropTypes from "prop-types";
4
4
  import { useIntl } from "react-intl";
5
5
  import {
@@ -13,7 +13,7 @@ import {
13
13
  import { FormProvider, useForm, Controller } from "react-hook-form";
14
14
  import { ConfirmModal } from "@truedat/core/components";
15
15
  import { useLanguage } from "@truedat/core/i18n";
16
- import { useResourceTypeOptions } from "../constants";
16
+ import { usePromptResourceTypeOptions } from "../constants";
17
17
 
18
18
  export default function PromptEditor({
19
19
  selectedPrompt,
@@ -26,8 +26,11 @@ export default function PromptEditor({
26
26
  providersOptions,
27
27
  }) {
28
28
  const { formatMessage } = useIntl();
29
- const resourceTypeOptions = useResourceTypeOptions();
30
- const { enabledLangs } = useLanguage();
29
+ const resourceTypeOptions = usePromptResourceTypeOptions();
30
+ const { defaultLang, enabledLangs } = useLanguage();
31
+ const [isTranslationPrompt, setIsTranslationPrompt] = useState(
32
+ selectedPrompt?.resource_type === "translation"
33
+ );
31
34
 
32
35
  const localeOptions = _.map((key) => ({
33
36
  key,
@@ -48,14 +51,21 @@ export default function PromptEditor({
48
51
 
49
52
  if (!selectedPrompt) return null;
50
53
 
54
+ const handleResourceTypeChange = (value, onChange) => {
55
+ const isValueTranslation = value === "translation";
56
+ setIsTranslationPrompt(isValueTranslation);
57
+ onChange(value);
58
+ if (isValueTranslation) {
59
+ form.setValue("language", defaultLang);
60
+ }
61
+ };
62
+
51
63
  const name =
52
64
  watch("name") ||
53
65
  formatMessage({
54
66
  id: "prompts.form.name.new",
55
67
  });
56
68
 
57
- const provider = watch("provider");
58
-
59
69
  return (
60
70
  <Fragment>
61
71
  <FormProvider {...form}>
@@ -122,7 +132,9 @@ export default function PromptEditor({
122
132
  selection
123
133
  onBlur={onBlur}
124
134
  options={resourceTypeOptions}
125
- onChange={(_e, { value }) => onChange(value)}
135
+ onChange={(_e, { value }) =>
136
+ handleResourceTypeChange(value, onChange)
137
+ }
126
138
  value={value}
127
139
  />
128
140
  </Form.Field>
@@ -143,13 +155,25 @@ export default function PromptEditor({
143
155
  ),
144
156
  }}
145
157
  render={({ field: { onBlur, onChange, value } }) => (
146
- <Form.Field required>
158
+ <Form.Field
159
+ required
160
+ data-tooltip={
161
+ isTranslationPrompt
162
+ ? formatMessage({
163
+ id: "prompts.form.language.tooltip.disabled",
164
+ defaultMessage:
165
+ "Language is disabled for translations type prompts",
166
+ })
167
+ : null
168
+ }
169
+ >
147
170
  <label>
148
171
  {formatMessage({
149
172
  id: "prompts.form.language",
150
173
  })}
151
174
  </label>
152
175
  <Dropdown
176
+ disabled={isTranslationPrompt}
153
177
  selection
154
178
  onBlur={onBlur}
155
179
  options={localeOptions}
@@ -332,6 +356,7 @@ export default function PromptEditor({
332
356
  PromptEditor.propTypes = {
333
357
  selectedPrompt: PropTypes.object,
334
358
  onSubmit: PropTypes.func,
359
+ onSetActive: PropTypes.func,
335
360
  onCancel: PropTypes.func,
336
361
  onDelete: PropTypes.func,
337
362
  isSubmitting: PropTypes.bool,
@@ -1,3 +1,4 @@
1
+ import _ from "lodash/fp";
1
2
  import React from "react";
2
3
  import { act } from "react-dom/test-utils";
3
4
  import { waitFor } from "@testing-library/react";
@@ -64,6 +65,17 @@ describe("<PromptEditor />", () => {
64
65
  expect(container).toMatchSnapshot();
65
66
  });
66
67
 
68
+ it("matches snapshot for translation resource type", async () => {
69
+ const translationProps = _.set(
70
+ "selectedPrompt.resource_type",
71
+ "translation"
72
+ )(props);
73
+ const { container, queryByText } = renderComponent(translationProps);
74
+ await waitFor(() => expect(queryByText(/lazy/i)).not.toBeInTheDocument());
75
+ await waitFor(() => expect(queryByText(/lazy/i)).not.toBeInTheDocument());
76
+ expect(container).toMatchSnapshot();
77
+ });
78
+
67
79
  it("matches snapshot without onDelete", async () => {
68
80
  const thisProps = {
69
81
  ...props,