@arqel-dev/ai 0.15.0 → 0.16.0
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/dist/index.js +136 -69
- package/dist/index.js.map +1 -1
- package/dist/register.js +145 -73
- package/dist/register.js.map +1 -1
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useArqelTranslations, useArqelLocale } from '@arqel-dev/react/utils';
|
|
1
2
|
import { Label, Button, Card, CardContent, Badge, Alert, AlertDescription, Select, SelectTrigger, SelectValue, SelectContent, SelectItem, Textarea } from '@arqel-dev/ui';
|
|
2
3
|
import { useId, useState, useCallback, useMemo } from 'react';
|
|
3
4
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
@@ -82,9 +83,11 @@ function AiExtractInput(props) {
|
|
|
82
83
|
csrfToken,
|
|
83
84
|
onPopulateField
|
|
84
85
|
} = props;
|
|
86
|
+
const t = useArqelTranslations();
|
|
85
87
|
const sourceField = fieldProps?.sourceField ?? "";
|
|
86
88
|
const targetFields = fieldProps?.targetFields ?? [];
|
|
87
|
-
const
|
|
89
|
+
const serverLabel = fieldProps?.buttonLabel;
|
|
90
|
+
const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.extract", "Extract with AI");
|
|
88
91
|
const reactId = useId();
|
|
89
92
|
const previewId = `arqel-ai-extract-${reactId}`;
|
|
90
93
|
const [extracted, setExtracted] = useState(value);
|
|
@@ -115,7 +118,12 @@ function AiExtractInput(props) {
|
|
|
115
118
|
const handleExtract = useCallback(async () => {
|
|
116
119
|
const url = buildExtractUrl(extractUrl, resource, field);
|
|
117
120
|
if (url === null) {
|
|
118
|
-
setError(
|
|
121
|
+
setError(
|
|
122
|
+
t(
|
|
123
|
+
"arqel.ai.missing_extract_url",
|
|
124
|
+
"Missing extract URL: provide `extractUrl` or both `resource` and `field`."
|
|
125
|
+
)
|
|
126
|
+
);
|
|
119
127
|
return;
|
|
120
128
|
}
|
|
121
129
|
const sourceText = coerceSourceText(
|
|
@@ -144,27 +152,28 @@ function AiExtractInput(props) {
|
|
|
144
152
|
} catch {
|
|
145
153
|
message = null;
|
|
146
154
|
}
|
|
147
|
-
setError(
|
|
155
|
+
setError(
|
|
156
|
+
message ?? t("arqel.ai.extract_error_http", "Extraction failed (HTTP :status).", {
|
|
157
|
+
status: String(response.status)
|
|
158
|
+
})
|
|
159
|
+
);
|
|
148
160
|
return;
|
|
149
161
|
}
|
|
150
162
|
const body = await response.json();
|
|
151
163
|
if (!isPlainRecord(body.extracted)) {
|
|
152
|
-
setError("Extraction failed: invalid response body.");
|
|
164
|
+
setError(t("arqel.ai.extract_error_invalid", "Extraction failed: invalid response body."));
|
|
153
165
|
return;
|
|
154
166
|
}
|
|
155
167
|
setExtracted(body.extracted);
|
|
156
168
|
} catch {
|
|
157
|
-
setError("Extraction failed: network error.");
|
|
169
|
+
setError(t("arqel.ai.extract_error_network", "Extraction failed: network error."));
|
|
158
170
|
} finally {
|
|
159
171
|
setIsLoading(false);
|
|
160
172
|
}
|
|
161
|
-
}, [csrfToken, extractUrl, field, formData, resource, sourceField]);
|
|
173
|
+
}, [csrfToken, extractUrl, field, formData, resource, sourceField, t]);
|
|
162
174
|
const hasExtraction = extracted !== null && Object.keys(extracted).length > 0;
|
|
163
175
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiExtract", "data-field-name": name, children: [
|
|
164
|
-
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */
|
|
165
|
-
"Source: ",
|
|
166
|
-
sourceField
|
|
167
|
-
] }) }),
|
|
176
|
+
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(Label, { className: "text-muted-foreground", children: t("arqel.ai.source", "Source: :field", { field: sourceField }) }) }),
|
|
168
177
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
169
178
|
/* @__PURE__ */ jsxs(
|
|
170
179
|
Button,
|
|
@@ -177,7 +186,7 @@ function AiExtractInput(props) {
|
|
|
177
186
|
disabled: isLoading,
|
|
178
187
|
"aria-label": buttonLabel,
|
|
179
188
|
children: [
|
|
180
|
-
isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Extracting", children: /* @__PURE__ */ jsx(Spinner, {}) }) : null,
|
|
189
|
+
isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_extracting", "Extracting"), children: /* @__PURE__ */ jsx(Spinner, {}) }) : null,
|
|
181
190
|
/* @__PURE__ */ jsx("span", { children: buttonLabel })
|
|
182
191
|
]
|
|
183
192
|
}
|
|
@@ -190,7 +199,7 @@ function AiExtractInput(props) {
|
|
|
190
199
|
onClick: () => {
|
|
191
200
|
applyAll(extracted);
|
|
192
201
|
},
|
|
193
|
-
children: "Apply all"
|
|
202
|
+
children: t("arqel.ai.apply_all", "Apply all")
|
|
194
203
|
}
|
|
195
204
|
) : null
|
|
196
205
|
] }),
|
|
@@ -219,8 +228,8 @@ function AiExtractInput(props) {
|
|
|
219
228
|
onClick: () => {
|
|
220
229
|
applyOne(target, val);
|
|
221
230
|
},
|
|
222
|
-
"aria-label":
|
|
223
|
-
children: "Apply"
|
|
231
|
+
"aria-label": t("arqel.ai.apply_field", "Apply :field", { field: target }),
|
|
232
|
+
children: t("arqel.ai.apply", "Apply")
|
|
224
233
|
}
|
|
225
234
|
)
|
|
226
235
|
] })
|
|
@@ -228,7 +237,7 @@ function AiExtractInput(props) {
|
|
|
228
237
|
},
|
|
229
238
|
target
|
|
230
239
|
);
|
|
231
|
-
}) }) }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground italic", children: "No extraction yet \u2014 click button to start." }),
|
|
240
|
+
}) }) }) }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground italic", children: t("arqel.ai.extract_empty", "No extraction yet \u2014 click button to start.") }),
|
|
232
241
|
error !== null ? /* @__PURE__ */ jsx(Alert, { variant: "destructive", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null
|
|
233
242
|
] });
|
|
234
243
|
}
|
|
@@ -269,14 +278,15 @@ function readFileAsDataUrl(file) {
|
|
|
269
278
|
reader.readAsDataURL(file);
|
|
270
279
|
});
|
|
271
280
|
}
|
|
272
|
-
function formatBytes(bytes) {
|
|
281
|
+
function formatBytes(bytes, locale) {
|
|
282
|
+
const round1 = (value) => new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
|
|
273
283
|
if (bytes >= 1048576) {
|
|
274
|
-
return `${(bytes / 1048576)
|
|
284
|
+
return `${round1(bytes / 1048576)} MB`;
|
|
275
285
|
}
|
|
276
286
|
if (bytes >= 1024) {
|
|
277
|
-
return `${(bytes / 1024)
|
|
287
|
+
return `${round1(bytes / 1024)} KB`;
|
|
278
288
|
}
|
|
279
|
-
return `${
|
|
289
|
+
return `${round1(bytes)} B`;
|
|
280
290
|
}
|
|
281
291
|
function Spinner2() {
|
|
282
292
|
return /* @__PURE__ */ jsxs(
|
|
@@ -320,7 +330,10 @@ function AiImageInput(props) {
|
|
|
320
330
|
const populateFields = fieldProps?.populateFields ?? {};
|
|
321
331
|
const acceptedMimes = fieldProps?.acceptedMimes ?? [];
|
|
322
332
|
const maxFileSize = fieldProps?.maxFileSize ?? 0;
|
|
323
|
-
const
|
|
333
|
+
const t = useArqelTranslations();
|
|
334
|
+
const locale = useArqelLocale();
|
|
335
|
+
const serverLabel = fieldProps?.buttonLabel;
|
|
336
|
+
const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.analyze", "Analyze with AI");
|
|
324
337
|
const reactId = useId();
|
|
325
338
|
const inputId = `arqel-ai-image-${reactId}`;
|
|
326
339
|
const [file, setFile] = useState(null);
|
|
@@ -340,7 +353,12 @@ function AiImageInput(props) {
|
|
|
340
353
|
return;
|
|
341
354
|
}
|
|
342
355
|
if (maxFileSize > 0 && next.size > maxFileSize) {
|
|
343
|
-
setError(
|
|
356
|
+
setError(
|
|
357
|
+
t("arqel.ai.file_too_large", "File too large: :size (max :max).", {
|
|
358
|
+
size: formatBytes(next.size, locale),
|
|
359
|
+
max: formatBytes(maxFileSize, locale)
|
|
360
|
+
})
|
|
361
|
+
);
|
|
344
362
|
setFile(null);
|
|
345
363
|
setPreviewUrl(null);
|
|
346
364
|
return;
|
|
@@ -352,7 +370,7 @@ function AiImageInput(props) {
|
|
|
352
370
|
setPreviewUrl(null);
|
|
353
371
|
}
|
|
354
372
|
},
|
|
355
|
-
[maxFileSize]
|
|
373
|
+
[locale, maxFileSize, t]
|
|
356
374
|
);
|
|
357
375
|
const handleAnalyze = useCallback(async () => {
|
|
358
376
|
if (file === null) {
|
|
@@ -360,7 +378,12 @@ function AiImageInput(props) {
|
|
|
360
378
|
}
|
|
361
379
|
const url = buildAnalyzeUrl(analyzeUrl, resource, field);
|
|
362
380
|
if (url === null) {
|
|
363
|
-
setError(
|
|
381
|
+
setError(
|
|
382
|
+
t(
|
|
383
|
+
"arqel.ai.missing_analyze_url",
|
|
384
|
+
"Missing analyze URL: provide `analyzeUrl` or both `resource` and `field`."
|
|
385
|
+
)
|
|
386
|
+
);
|
|
364
387
|
return;
|
|
365
388
|
}
|
|
366
389
|
setIsLoading(true);
|
|
@@ -387,12 +410,16 @@ function AiImageInput(props) {
|
|
|
387
410
|
} catch {
|
|
388
411
|
message = null;
|
|
389
412
|
}
|
|
390
|
-
setError(
|
|
413
|
+
setError(
|
|
414
|
+
message ?? t("arqel.ai.analyze_error_http", "Analysis failed (HTTP :status).", {
|
|
415
|
+
status: String(response.status)
|
|
416
|
+
})
|
|
417
|
+
);
|
|
391
418
|
return;
|
|
392
419
|
}
|
|
393
420
|
const body = await response.json();
|
|
394
421
|
if (!isStringRecord(body.analyses)) {
|
|
395
|
-
setError("Analysis failed: invalid response body.");
|
|
422
|
+
setError(t("arqel.ai.analyze_error_invalid", "Analysis failed: invalid response body."));
|
|
396
423
|
return;
|
|
397
424
|
}
|
|
398
425
|
setResults(body.analyses);
|
|
@@ -403,11 +430,11 @@ function AiImageInput(props) {
|
|
|
403
430
|
onChange(imageBase64);
|
|
404
431
|
}
|
|
405
432
|
} catch {
|
|
406
|
-
setError("Analysis failed: network error.");
|
|
433
|
+
setError(t("arqel.ai.analyze_error_network", "Analysis failed: network error."));
|
|
407
434
|
} finally {
|
|
408
435
|
setIsLoading(false);
|
|
409
436
|
}
|
|
410
|
-
}, [analyzeUrl, csrfToken, field, file, onChange, resource]);
|
|
437
|
+
}, [analyzeUrl, csrfToken, field, file, onChange, resource, t]);
|
|
411
438
|
const applyOne = useCallback(
|
|
412
439
|
(analysisKey, val) => {
|
|
413
440
|
const target = populateMapping[analysisKey];
|
|
@@ -448,7 +475,9 @@ function AiImageInput(props) {
|
|
|
448
475
|
htmlFor: inputId,
|
|
449
476
|
className: "flex items-center justify-center w-full min-h-[6rem] rounded-sm border-2 border-dashed border-border bg-muted/40 px-4 py-6 text-sm text-muted-foreground hover:bg-muted cursor-pointer transition-colors",
|
|
450
477
|
children: [
|
|
451
|
-
/* @__PURE__ */ jsx("span", { children: file !== null ? file.name :
|
|
478
|
+
/* @__PURE__ */ jsx("span", { children: file !== null ? file.name : t("arqel.ai.image_dropzone", "Click or drop image (:accept)", {
|
|
479
|
+
accept: accept !== "" ? accept : t("arqel.ai.image_accept_any", "any")
|
|
480
|
+
}) }),
|
|
452
481
|
/* @__PURE__ */ jsx(
|
|
453
482
|
"input",
|
|
454
483
|
{
|
|
@@ -457,7 +486,7 @@ function AiImageInput(props) {
|
|
|
457
486
|
accept,
|
|
458
487
|
onChange: handleFileChange,
|
|
459
488
|
className: "sr-only",
|
|
460
|
-
"aria-label": "Image file"
|
|
489
|
+
"aria-label": t("arqel.ai.image_file", "Image file")
|
|
461
490
|
}
|
|
462
491
|
)
|
|
463
492
|
]
|
|
@@ -467,7 +496,7 @@ function AiImageInput(props) {
|
|
|
467
496
|
"img",
|
|
468
497
|
{
|
|
469
498
|
src: previewUrl,
|
|
470
|
-
alt: "Selected preview",
|
|
499
|
+
alt: t("arqel.ai.selected_preview_alt", "Selected preview"),
|
|
471
500
|
"data-testid": "image-preview",
|
|
472
501
|
className: "max-h-64 max-w-full rounded-sm"
|
|
473
502
|
}
|
|
@@ -484,7 +513,7 @@ function AiImageInput(props) {
|
|
|
484
513
|
disabled: !canAnalyze,
|
|
485
514
|
"aria-label": buttonLabel,
|
|
486
515
|
children: [
|
|
487
|
-
isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Analyzing", children: /* @__PURE__ */ jsx(Spinner2, {}) }) : null,
|
|
516
|
+
isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_analyzing", "Analyzing"), children: /* @__PURE__ */ jsx(Spinner2, {}) }) : null,
|
|
488
517
|
/* @__PURE__ */ jsx("span", { children: buttonLabel })
|
|
489
518
|
]
|
|
490
519
|
}
|
|
@@ -497,7 +526,7 @@ function AiImageInput(props) {
|
|
|
497
526
|
onClick: () => {
|
|
498
527
|
applyAll(results);
|
|
499
528
|
},
|
|
500
|
-
children: "Apply all"
|
|
529
|
+
children: t("arqel.ai.apply_all", "Apply all")
|
|
501
530
|
}
|
|
502
531
|
) : null
|
|
503
532
|
] }),
|
|
@@ -531,8 +560,8 @@ function AiImageInput(props) {
|
|
|
531
560
|
onClick: () => {
|
|
532
561
|
applyOne(key, val);
|
|
533
562
|
},
|
|
534
|
-
"aria-label":
|
|
535
|
-
children: "Apply"
|
|
563
|
+
"aria-label": t("arqel.ai.apply_field", "Apply :field", { field: key }),
|
|
564
|
+
children: t("arqel.ai.apply", "Apply")
|
|
536
565
|
}
|
|
537
566
|
) : null
|
|
538
567
|
] })
|
|
@@ -547,7 +576,7 @@ function AiImageInput(props) {
|
|
|
547
576
|
);
|
|
548
577
|
}
|
|
549
578
|
var DEFAULT_BUTTON_LABEL = "Classify with AI";
|
|
550
|
-
var
|
|
579
|
+
var NO_CONTEXT_TOOLTIP_FALLBACK = "No context fields configured. Add `classifyFromFields` to enable AI classification.";
|
|
551
580
|
function buildClassifyUrl(override, resource, field) {
|
|
552
581
|
if (override !== void 0 && override !== "") {
|
|
553
582
|
return override;
|
|
@@ -595,9 +624,11 @@ function AiSelectInput(props) {
|
|
|
595
624
|
classifyUrl,
|
|
596
625
|
csrfToken
|
|
597
626
|
} = props;
|
|
627
|
+
const t = useArqelTranslations();
|
|
598
628
|
const options = fieldProps?.options ?? {};
|
|
599
629
|
const fallbackOption = fieldProps?.fallbackOption ?? null;
|
|
600
630
|
const hasContextFields = fieldProps?.hasContextFields ?? false;
|
|
631
|
+
const buttonLabel = t("arqel.ai.classify", DEFAULT_BUTTON_LABEL);
|
|
601
632
|
const reactId = useId();
|
|
602
633
|
const selectId = `arqel-ai-select-${reactId}`;
|
|
603
634
|
const [internalValue, setInternalValue] = useState(value);
|
|
@@ -626,7 +657,12 @@ function AiSelectInput(props) {
|
|
|
626
657
|
const handleClassify = useCallback(async () => {
|
|
627
658
|
const url = buildClassifyUrl(classifyUrl, resource, field);
|
|
628
659
|
if (url === null) {
|
|
629
|
-
setError(
|
|
660
|
+
setError(
|
|
661
|
+
t(
|
|
662
|
+
"arqel.ai.missing_classify_url",
|
|
663
|
+
"Missing classify URL: provide `classifyUrl` or both `resource` and `field`."
|
|
664
|
+
)
|
|
665
|
+
);
|
|
630
666
|
return;
|
|
631
667
|
}
|
|
632
668
|
setIsLoading(true);
|
|
@@ -644,7 +680,11 @@ function AiSelectInput(props) {
|
|
|
644
680
|
body: JSON.stringify({ formData: formData ?? {} })
|
|
645
681
|
});
|
|
646
682
|
if (!response.ok) {
|
|
647
|
-
setError(
|
|
683
|
+
setError(
|
|
684
|
+
t("arqel.ai.classify_error_http", "Classification failed (HTTP :status).", {
|
|
685
|
+
status: String(response.status)
|
|
686
|
+
})
|
|
687
|
+
);
|
|
648
688
|
return;
|
|
649
689
|
}
|
|
650
690
|
const body = await response.json();
|
|
@@ -659,19 +699,19 @@ function AiSelectInput(props) {
|
|
|
659
699
|
setSuggestion("fallback");
|
|
660
700
|
return;
|
|
661
701
|
}
|
|
662
|
-
setError("Could not classify.");
|
|
702
|
+
setError(t("arqel.ai.classify_error_none", "Could not classify."));
|
|
663
703
|
} catch {
|
|
664
|
-
setError("Classification failed: network error.");
|
|
704
|
+
setError(t("arqel.ai.classify_error_network", "Classification failed: network error."));
|
|
665
705
|
} finally {
|
|
666
706
|
setIsLoading(false);
|
|
667
707
|
}
|
|
668
|
-
}, [applyValue, classifyUrl, csrfToken, fallbackOption, field, formData, resource]);
|
|
708
|
+
}, [applyValue, classifyUrl, csrfToken, fallbackOption, field, formData, resource, t]);
|
|
669
709
|
const buttonDisabled = isLoading || !hasContextFields;
|
|
670
|
-
const buttonTitle = !hasContextFields ?
|
|
710
|
+
const buttonTitle = !hasContextFields ? t("arqel.ai.classify_no_context_tooltip", NO_CONTEXT_TOOLTIP_FALLBACK) : void 0;
|
|
671
711
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiSelect", "data-field-name": name, children: [
|
|
672
712
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
673
713
|
/* @__PURE__ */ jsxs(Select, { value: currentValue ?? "", onValueChange: handleSelectChange, name, children: [
|
|
674
|
-
/* @__PURE__ */ jsx(SelectTrigger, { id: selectId, className: "flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Select..." }) }),
|
|
714
|
+
/* @__PURE__ */ jsx(SelectTrigger, { id: selectId, className: "flex-1", children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("arqel.ai.select_placeholder", "Select...") }) }),
|
|
675
715
|
/* @__PURE__ */ jsx(SelectContent, { children: Object.entries(options).map(([key, label]) => /* @__PURE__ */ jsx(SelectItem, { value: key, children: label }, key)) })
|
|
676
716
|
] }),
|
|
677
717
|
/* @__PURE__ */ jsxs(
|
|
@@ -683,11 +723,11 @@ function AiSelectInput(props) {
|
|
|
683
723
|
void handleClassify();
|
|
684
724
|
},
|
|
685
725
|
disabled: buttonDisabled,
|
|
686
|
-
"aria-label":
|
|
726
|
+
"aria-label": buttonLabel,
|
|
687
727
|
...buttonTitle !== void 0 ? { title: buttonTitle } : {},
|
|
688
728
|
children: [
|
|
689
|
-
isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Classifying", children: /* @__PURE__ */ jsx(Spinner3, {}) }) : null,
|
|
690
|
-
/* @__PURE__ */ jsx("span", { children:
|
|
729
|
+
isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_classifying", "Classifying"), children: /* @__PURE__ */ jsx(Spinner3, {}) }) : null,
|
|
730
|
+
/* @__PURE__ */ jsx("span", { children: buttonLabel })
|
|
691
731
|
]
|
|
692
732
|
}
|
|
693
733
|
)
|
|
@@ -698,7 +738,7 @@ function AiSelectInput(props) {
|
|
|
698
738
|
{
|
|
699
739
|
variant: "secondary",
|
|
700
740
|
className: suggestion === "ai" ? "border-emerald-500/30 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300" : "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
|
|
701
|
-
children: suggestion === "ai" ? "Suggested by AI" : "Used fallback"
|
|
741
|
+
children: suggestion === "ai" ? t("arqel.ai.suggestion_ai", "Suggested by AI") : t("arqel.ai.suggestion_fallback", "Used fallback")
|
|
702
742
|
}
|
|
703
743
|
),
|
|
704
744
|
/* @__PURE__ */ jsx(
|
|
@@ -709,7 +749,7 @@ function AiSelectInput(props) {
|
|
|
709
749
|
onClick: () => {
|
|
710
750
|
setSuggestion(null);
|
|
711
751
|
},
|
|
712
|
-
children: "Accept"
|
|
752
|
+
children: t("arqel.ai.suggestion_accept", "Accept")
|
|
713
753
|
}
|
|
714
754
|
),
|
|
715
755
|
/* @__PURE__ */ jsx(
|
|
@@ -720,7 +760,7 @@ function AiSelectInput(props) {
|
|
|
720
760
|
onClick: () => {
|
|
721
761
|
setSuggestion(null);
|
|
722
762
|
},
|
|
723
|
-
children: "Pick another"
|
|
763
|
+
children: t("arqel.ai.suggestion_pick_another", "Pick another")
|
|
724
764
|
}
|
|
725
765
|
)
|
|
726
766
|
] }) : null,
|
|
@@ -776,7 +816,11 @@ function AiTextInput(props) {
|
|
|
776
816
|
generateUrl,
|
|
777
817
|
csrfToken
|
|
778
818
|
} = props;
|
|
779
|
-
const
|
|
819
|
+
const t = useArqelTranslations();
|
|
820
|
+
const locale = useArqelLocale();
|
|
821
|
+
const numberFormatter = useMemo(() => new Intl.NumberFormat(locale), [locale]);
|
|
822
|
+
const serverLabel = fieldProps?.buttonLabel;
|
|
823
|
+
const buttonLabel = serverLabel !== void 0 && serverLabel !== "" ? serverLabel : t("arqel.ai.generate", DEFAULT_BUTTON_LABEL2);
|
|
780
824
|
const maxLength = fieldProps?.maxLength ?? null;
|
|
781
825
|
const reactId = useId();
|
|
782
826
|
const textareaId = `arqel-ai-text-${reactId}`;
|
|
@@ -810,7 +854,12 @@ function AiTextInput(props) {
|
|
|
810
854
|
const handleGenerate = useCallback(async () => {
|
|
811
855
|
const url = buildGenerateUrl(generateUrl, resource, field);
|
|
812
856
|
if (url === null) {
|
|
813
|
-
setError(
|
|
857
|
+
setError(
|
|
858
|
+
t(
|
|
859
|
+
"arqel.ai.missing_generate_url",
|
|
860
|
+
"Missing generate URL: provide `generateUrl` or both `resource` and `field`."
|
|
861
|
+
)
|
|
862
|
+
);
|
|
814
863
|
return;
|
|
815
864
|
}
|
|
816
865
|
setIsLoading(true);
|
|
@@ -827,7 +876,11 @@ function AiTextInput(props) {
|
|
|
827
876
|
body: JSON.stringify({ formData: formData ?? {} })
|
|
828
877
|
});
|
|
829
878
|
if (!response.ok) {
|
|
830
|
-
setError(
|
|
879
|
+
setError(
|
|
880
|
+
t("arqel.ai.error_http", "Generation failed (HTTP :status).", {
|
|
881
|
+
status: String(response.status)
|
|
882
|
+
})
|
|
883
|
+
);
|
|
831
884
|
return;
|
|
832
885
|
}
|
|
833
886
|
const body = await response.json();
|
|
@@ -835,12 +888,12 @@ function AiTextInput(props) {
|
|
|
835
888
|
applyGeneratedText(text);
|
|
836
889
|
setHasGenerated(true);
|
|
837
890
|
} catch {
|
|
838
|
-
setError("Generation failed: network error.");
|
|
891
|
+
setError(t("arqel.ai.error_network", "Generation failed: network error."));
|
|
839
892
|
} finally {
|
|
840
893
|
setIsLoading(false);
|
|
841
894
|
}
|
|
842
|
-
}, [applyGeneratedText, csrfToken, field, formData, generateUrl, resource]);
|
|
843
|
-
const triggerLabel = hasGenerated ? REGENERATE_LABEL : buttonLabel;
|
|
895
|
+
}, [applyGeneratedText, csrfToken, field, formData, generateUrl, resource, t]);
|
|
896
|
+
const triggerLabel = hasGenerated ? t("arqel.ai.regenerate", REGENERATE_LABEL) : buttonLabel;
|
|
844
897
|
const charCount = currentValue.length;
|
|
845
898
|
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", "data-arqel-field": "aiText", "data-field-name": name, children: [
|
|
846
899
|
/* @__PURE__ */ jsx(
|
|
@@ -855,9 +908,9 @@ function AiTextInput(props) {
|
|
|
855
908
|
}
|
|
856
909
|
),
|
|
857
910
|
maxLength !== null ? /* @__PURE__ */ jsxs("div", { className: "text-xs text-muted-foreground self-end", "aria-live": "polite", children: [
|
|
858
|
-
charCount,
|
|
911
|
+
numberFormatter.format(charCount),
|
|
859
912
|
" / ",
|
|
860
|
-
maxLength
|
|
913
|
+
numberFormatter.format(maxLength)
|
|
861
914
|
] }) : null,
|
|
862
915
|
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxs(
|
|
863
916
|
Button,
|
|
@@ -870,7 +923,7 @@ function AiTextInput(props) {
|
|
|
870
923
|
disabled: isLoading,
|
|
871
924
|
"aria-label": triggerLabel,
|
|
872
925
|
children: [
|
|
873
|
-
isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Generating", children: /* @__PURE__ */ jsx(Spinner4, {}) }) : null,
|
|
926
|
+
isLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_generating", "Generating"), children: /* @__PURE__ */ jsx(Spinner4, {}) }) : null,
|
|
874
927
|
/* @__PURE__ */ jsx("span", { children: triggerLabel })
|
|
875
928
|
]
|
|
876
929
|
}
|
|
@@ -940,6 +993,7 @@ function AiTranslateInput(props) {
|
|
|
940
993
|
translateUrl,
|
|
941
994
|
csrfToken
|
|
942
995
|
} = props;
|
|
996
|
+
const t = useArqelTranslations();
|
|
943
997
|
const languages = useMemo(() => fieldProps?.languages ?? [], [fieldProps?.languages]);
|
|
944
998
|
const defaultLanguage = fieldProps?.defaultLanguage ?? languages[0] ?? "";
|
|
945
999
|
const reactId = useId();
|
|
@@ -988,7 +1042,12 @@ function AiTranslateInput(props) {
|
|
|
988
1042
|
}
|
|
989
1043
|
const url = buildTranslateUrl(translateUrl, resource, field);
|
|
990
1044
|
if (url === null) {
|
|
991
|
-
setError(
|
|
1045
|
+
setError(
|
|
1046
|
+
t(
|
|
1047
|
+
"arqel.ai.missing_translate_url",
|
|
1048
|
+
"Missing translate URL: provide `translateUrl` or both `resource` and `field`."
|
|
1049
|
+
)
|
|
1050
|
+
);
|
|
992
1051
|
return;
|
|
993
1052
|
}
|
|
994
1053
|
setError(null);
|
|
@@ -1009,19 +1068,25 @@ function AiTranslateInput(props) {
|
|
|
1009
1068
|
})
|
|
1010
1069
|
});
|
|
1011
1070
|
if (!response.ok) {
|
|
1012
|
-
setError(
|
|
1071
|
+
setError(
|
|
1072
|
+
t("arqel.ai.translate_error_http", "Translation failed (HTTP :status).", {
|
|
1073
|
+
status: String(response.status)
|
|
1074
|
+
})
|
|
1075
|
+
);
|
|
1013
1076
|
return;
|
|
1014
1077
|
}
|
|
1015
1078
|
const body = await response.json();
|
|
1016
1079
|
const incoming = body.translations;
|
|
1017
1080
|
if (!isStringRecord2(incoming)) {
|
|
1018
|
-
setError(
|
|
1081
|
+
setError(
|
|
1082
|
+
t("arqel.ai.translate_error_invalid", "Translation failed: invalid response body.")
|
|
1083
|
+
);
|
|
1019
1084
|
return;
|
|
1020
1085
|
}
|
|
1021
1086
|
const merged = { ...currentValue, ...incoming };
|
|
1022
1087
|
applyValue(merged);
|
|
1023
1088
|
} catch {
|
|
1024
|
-
setError("Translation failed: network error.");
|
|
1089
|
+
setError(t("arqel.ai.translate_error_network", "Translation failed: network error."));
|
|
1025
1090
|
} finally {
|
|
1026
1091
|
setLoadingFor(targetLanguages, false);
|
|
1027
1092
|
}
|
|
@@ -1034,6 +1099,7 @@ function AiTranslateInput(props) {
|
|
|
1034
1099
|
field,
|
|
1035
1100
|
resource,
|
|
1036
1101
|
setLoadingFor,
|
|
1102
|
+
t,
|
|
1037
1103
|
translateUrl
|
|
1038
1104
|
]
|
|
1039
1105
|
);
|
|
@@ -1062,7 +1128,7 @@ function AiTranslateInput(props) {
|
|
|
1062
1128
|
size: "sm",
|
|
1063
1129
|
onClick: handleTranslateAllMissing,
|
|
1064
1130
|
disabled: missingCount === 0 || loadingLangs.size > 0,
|
|
1065
|
-
children: "Translate all missing"
|
|
1131
|
+
children: t("arqel.ai.translate_all_missing", "Translate all missing")
|
|
1066
1132
|
}
|
|
1067
1133
|
) }),
|
|
1068
1134
|
/* @__PURE__ */ jsx("div", { role: "tablist", id: tablistId, className: "flex items-center gap-1 border-b border-border", children: languages.map((lang) => {
|
|
@@ -1085,7 +1151,7 @@ function AiTranslateInput(props) {
|
|
|
1085
1151
|
{
|
|
1086
1152
|
variant: "secondary",
|
|
1087
1153
|
role: "img",
|
|
1088
|
-
"aria-label": "Missing translation",
|
|
1154
|
+
"aria-label": t("arqel.ai.missing_translation", "Missing translation"),
|
|
1089
1155
|
"data-testid": `missing-dot-${lang}`,
|
|
1090
1156
|
className: "px-1 py-0",
|
|
1091
1157
|
children: "\u2022"
|
|
@@ -1104,7 +1170,9 @@ function AiTranslateInput(props) {
|
|
|
1104
1170
|
value: activeText,
|
|
1105
1171
|
onChange: handleTextareaChange,
|
|
1106
1172
|
rows: 6,
|
|
1107
|
-
"aria-label":
|
|
1173
|
+
"aria-label": t("arqel.ai.translate_textarea_aria", "Translation in :language", {
|
|
1174
|
+
language: activeLang
|
|
1175
|
+
})
|
|
1108
1176
|
}
|
|
1109
1177
|
),
|
|
1110
1178
|
activeLang !== defaultLanguage ? /* @__PURE__ */ jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxs(
|
|
@@ -1115,11 +1183,10 @@ function AiTranslateInput(props) {
|
|
|
1115
1183
|
onClick: () => handleTranslateOne(activeLang),
|
|
1116
1184
|
disabled: activeIsLoading,
|
|
1117
1185
|
children: [
|
|
1118
|
-
activeIsLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": "Translating", children: /* @__PURE__ */ jsx(Spinner5, {}) }) : null,
|
|
1119
|
-
/* @__PURE__ */
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
] })
|
|
1186
|
+
activeIsLoading ? /* @__PURE__ */ jsx("span", { role: "status", "aria-label": t("arqel.ai.status_translating", "Translating"), children: /* @__PURE__ */ jsx(Spinner5, {}) }) : null,
|
|
1187
|
+
/* @__PURE__ */ jsx("span", { children: t("arqel.ai.translate_from", "Translate from :language", {
|
|
1188
|
+
language: defaultLanguage
|
|
1189
|
+
}) })
|
|
1123
1190
|
]
|
|
1124
1191
|
}
|
|
1125
1192
|
) }) : null
|