@nyaruka/temba-components 0.156.14 → 0.156.15
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/CHANGELOG.md +10 -0
- package/dist/temba-components.js +24 -26
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/flow/AutoTranslate.ts +104 -36
- package/src/flow/Editor.ts +4 -2
- package/src/flow/flow-translations.ts +24 -1
- package/src/layout/Dialog.ts +1 -4
- package/src/store/AppState.ts +2 -0
- package/src/store/Store.ts +4 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { css, PropertyValues } from 'lit';
|
|
|
3
3
|
import { property, state } from 'lit/decorators.js';
|
|
4
4
|
import { RapidElement } from '../RapidElement';
|
|
5
5
|
import { getStore } from '../store/Store';
|
|
6
|
-
import { zustand } from '../store/AppState';
|
|
6
|
+
import { AppState, fromStore, zustand } from '../store/AppState';
|
|
7
7
|
import { FlowDefinition } from '../store/flow-definition';
|
|
8
8
|
import { TranslationEntry, buildTranslationBundles } from './flow-translations';
|
|
9
9
|
import { getLanguageDisplayName } from './utils';
|
|
@@ -66,10 +66,6 @@ export class AutoTranslate extends RapidElement {
|
|
|
66
66
|
text-decoration: underline;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
.auto-translate-single-model {
|
|
70
|
-
font-size: 13px;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
69
|
.auto-translate-status {
|
|
74
70
|
display: flex;
|
|
75
71
|
align-items: center;
|
|
@@ -141,6 +137,9 @@ export class AutoTranslate extends RapidElement {
|
|
|
141
137
|
@property({ type: Boolean })
|
|
142
138
|
disabled = false;
|
|
143
139
|
|
|
140
|
+
@fromStore(zustand, (state: AppState) => state.brand)
|
|
141
|
+
private brand!: string;
|
|
142
|
+
|
|
144
143
|
// Reactive flag the host can read to show "translating" state in UI
|
|
145
144
|
// adjacent to this component (e.g. the toolbar button).
|
|
146
145
|
@property({ type: Boolean, reflect: true })
|
|
@@ -170,6 +169,9 @@ export class AutoTranslate extends RapidElement {
|
|
|
170
169
|
@state()
|
|
171
170
|
interrupt = false;
|
|
172
171
|
|
|
172
|
+
@state()
|
|
173
|
+
updateExisting = false;
|
|
174
|
+
|
|
173
175
|
// Tracks whether the dialog has ever opened so we can keep it mounted
|
|
174
176
|
// afterwards (so it sees its own close transition) without paying
|
|
175
177
|
// for an empty hidden dialog before that.
|
|
@@ -192,6 +194,7 @@ export class AutoTranslate extends RapidElement {
|
|
|
192
194
|
this.error = null;
|
|
193
195
|
this.errorExpanded = false;
|
|
194
196
|
this.selectedModel = null;
|
|
197
|
+
this.updateExisting = false;
|
|
195
198
|
this.dialogOpen = true;
|
|
196
199
|
await this.loadModels();
|
|
197
200
|
|
|
@@ -228,6 +231,11 @@ export class AutoTranslate extends RapidElement {
|
|
|
228
231
|
this.selectedModel = next ? { uuid: next.uuid, name: next.name } : null;
|
|
229
232
|
}
|
|
230
233
|
|
|
234
|
+
private handleUpdateExistingChange(event: Event): void {
|
|
235
|
+
const checkbox = event.target as any;
|
|
236
|
+
this.updateExisting = !!checkbox?.checked;
|
|
237
|
+
}
|
|
238
|
+
|
|
231
239
|
private confirmTranslate(): void {
|
|
232
240
|
if (!this.selectedModel) {
|
|
233
241
|
return;
|
|
@@ -258,7 +266,10 @@ export class AutoTranslate extends RapidElement {
|
|
|
258
266
|
* preTranslated for direct application), and dedupes pending entries
|
|
259
267
|
* sharing identical source arrays so each unique array is sent at most
|
|
260
268
|
* once (duplicates are returned in duplicateKeyToCanonical for
|
|
261
|
-
* propagation when the canonical key's translation lands).
|
|
269
|
+
* propagation when the canonical key's translation lands). When
|
|
270
|
+
* updateExisting is true, already-translated entries are also included
|
|
271
|
+
* and the pre-translation reuse path is skipped so all localizable
|
|
272
|
+
* entries are sent to the LLM.
|
|
262
273
|
*/
|
|
263
274
|
private buildTranslationBatches(): {
|
|
264
275
|
keyToEntries: Map<string, TranslationEntry[]>;
|
|
@@ -276,53 +287,71 @@ export class AutoTranslate extends RapidElement {
|
|
|
276
287
|
}
|
|
277
288
|
|
|
278
289
|
const bundles = buildTranslationBundles(this.definition, this.languageCode);
|
|
290
|
+
const updateExisting = this.updateExisting;
|
|
279
291
|
|
|
280
292
|
// Map source-content -> already-stored translation array, built from
|
|
281
293
|
// entries that have a translation in the live localization map. Reading
|
|
282
|
-
// the stored array (not entry.to) preserves the original shape.
|
|
294
|
+
// the stored array (not entry.to) preserves the original shape. Skipped
|
|
295
|
+
// in update mode so we always re-translate via the LLM.
|
|
283
296
|
const existingByContent = new Map<string, string[]>();
|
|
284
297
|
const localization =
|
|
285
298
|
this.definition.localization?.[this.languageCode] || {};
|
|
286
299
|
|
|
287
|
-
|
|
288
|
-
for (const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
300
|
+
if (!updateExisting) {
|
|
301
|
+
for (const bundle of bundles) {
|
|
302
|
+
for (const entry of bundle.translations) {
|
|
303
|
+
// Skip entries with no real translation: an empty array OR an
|
|
304
|
+
// array of only empty/whitespace strings shouldn't seed reuse,
|
|
305
|
+
// or we'd silently propagate a blank to other matching sources.
|
|
306
|
+
if (
|
|
307
|
+
!entry.toValues ||
|
|
308
|
+
!entry.toValues.some((v) => v && v.trim().length > 0)
|
|
309
|
+
) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (!entry.fromValues || entry.fromValues.length === 0) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const stored = localization[entry.uuid]?.[entry.attribute];
|
|
316
|
+
if (!Array.isArray(stored) || stored.length === 0) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const contentKey = JSON.stringify(entry.fromValues);
|
|
320
|
+
if (!existingByContent.has(contentKey)) {
|
|
321
|
+
existingByContent.set(contentKey, stored);
|
|
322
|
+
}
|
|
302
323
|
}
|
|
303
324
|
}
|
|
304
325
|
}
|
|
305
326
|
|
|
306
|
-
// Collect pending entries
|
|
307
|
-
//
|
|
308
|
-
//
|
|
327
|
+
// Collect pending entries preserving order. Each unique key (uuid +
|
|
328
|
+
// attribute) maps to its source array (entry.fromValues), preserving
|
|
329
|
+
// multi-item structure for attributes like router-case `arguments`.
|
|
330
|
+
// In update mode we include entries that already have a translation.
|
|
331
|
+
// Invariant: findTranslations produces at most one entry per
|
|
332
|
+
// (uuid, attribute) pair, so the first fromValues is authoritative
|
|
333
|
+
// and any later entry with the same key is treated as a duplicate.
|
|
309
334
|
const valuesByKey = new Map<string, string[]>();
|
|
310
335
|
|
|
311
336
|
for (const bundle of bundles) {
|
|
312
337
|
for (const entry of bundle.translations) {
|
|
313
|
-
if (
|
|
338
|
+
if (
|
|
339
|
+
!updateExisting &&
|
|
340
|
+
entry.toValues &&
|
|
341
|
+
entry.toValues.some((v) => v && v.trim().length > 0)
|
|
342
|
+
) {
|
|
314
343
|
continue;
|
|
315
344
|
}
|
|
316
|
-
if (!entry.
|
|
345
|
+
if (!entry.fromValues || entry.fromValues.length === 0) {
|
|
317
346
|
continue;
|
|
318
347
|
}
|
|
319
348
|
const key = `${entry.uuid}:${entry.attribute}`;
|
|
320
349
|
const list = keyToEntries.get(key) || [];
|
|
321
350
|
list.push(entry);
|
|
322
351
|
keyToEntries.set(key, list);
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
352
|
+
if (!valuesByKey.has(key)) {
|
|
353
|
+
valuesByKey.set(key, entry.fromValues);
|
|
354
|
+
}
|
|
326
355
|
}
|
|
327
356
|
}
|
|
328
357
|
|
|
@@ -507,8 +536,39 @@ export class AutoTranslate extends RapidElement {
|
|
|
507
536
|
const liveDefinition = zustand.getState().flowDefinition;
|
|
508
537
|
const existing =
|
|
509
538
|
liveDefinition?.localization?.[this.languageCode]?.[uuid] || {};
|
|
539
|
+
const existingValues = Array.isArray(existing[attribute])
|
|
540
|
+
? (existing[attribute] as string[])
|
|
541
|
+
: null;
|
|
542
|
+
|
|
543
|
+
// Never replace an existing non-empty translation with an empty
|
|
544
|
+
// value: when re-translating already-localized content the LLM may
|
|
545
|
+
// return blanks for some entries; keep the prior translation in
|
|
546
|
+
// those slots. Only safe when the arrays align by index — if the
|
|
547
|
+
// source array length has changed since the last translation, the
|
|
548
|
+
// old values aren't tied to the same items anymore, so skip the
|
|
549
|
+
// per-item preserve and let the LLM result through unchanged.
|
|
550
|
+
const canPreservePerItem =
|
|
551
|
+
!!existingValues && existingValues.length === values.length;
|
|
552
|
+
const mergedValues = values.map((v, i) => {
|
|
553
|
+
const isEmpty =
|
|
554
|
+
v === null ||
|
|
555
|
+
v === undefined ||
|
|
556
|
+
(typeof v === 'string' && v.trim().length === 0);
|
|
557
|
+
if (isEmpty && canPreservePerItem && existingValues![i]) {
|
|
558
|
+
return existingValues![i];
|
|
559
|
+
}
|
|
560
|
+
return v;
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const hasContent = mergedValues.some(
|
|
564
|
+
(v) => v && (typeof v !== 'string' || v.trim().length > 0)
|
|
565
|
+
);
|
|
566
|
+
if (!hasContent) {
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
|
|
510
570
|
const merged = updatesByUuid.get(uuid) || { ...existing };
|
|
511
|
-
merged[attribute] =
|
|
571
|
+
merged[attribute] = mergedValues;
|
|
512
572
|
updatesByUuid.set(uuid, merged);
|
|
513
573
|
}
|
|
514
574
|
|
|
@@ -642,11 +702,15 @@ export class AutoTranslate extends RapidElement {
|
|
|
642
702
|
|
|
643
703
|
const selected = this.selectedModel ? [this.selectedModel] : [];
|
|
644
704
|
const languageName = getLanguageDisplayName(this.languageCode);
|
|
705
|
+
const aiClause = this.brand
|
|
706
|
+
? html`${this.brand} uses AI for automatic translation, which can make
|
|
707
|
+
mistakes,`
|
|
708
|
+
: html`Automatic translation uses AI, which can make mistakes,`;
|
|
645
709
|
return html`
|
|
646
710
|
<p>
|
|
647
711
|
All remaining text for <strong>${languageName}</strong> will be
|
|
648
|
-
translated automatically.
|
|
649
|
-
|
|
712
|
+
translated automatically. ${aiClause} so it is important to review all
|
|
713
|
+
of your translations to verify they are correct.
|
|
650
714
|
</p>
|
|
651
715
|
${this.models.length > 1
|
|
652
716
|
? html`<temba-select
|
|
@@ -660,9 +724,13 @@ export class AutoTranslate extends RapidElement {
|
|
|
660
724
|
placeholder="Select an AI model"
|
|
661
725
|
@change=${this.handleModelChange}
|
|
662
726
|
></temba-select>`
|
|
663
|
-
:
|
|
664
|
-
|
|
665
|
-
|
|
727
|
+
: ''}
|
|
728
|
+
<temba-checkbox
|
|
729
|
+
class="auto-translate-update-existing"
|
|
730
|
+
label="Update existing translations"
|
|
731
|
+
?checked=${this.updateExisting}
|
|
732
|
+
@change=${this.handleUpdateExistingChange}
|
|
733
|
+
></temba-checkbox>
|
|
666
734
|
`;
|
|
667
735
|
}
|
|
668
736
|
|
package/src/flow/Editor.ts
CHANGED
|
@@ -3690,11 +3690,13 @@ export class Editor extends RapidElement {
|
|
|
3690
3690
|
})
|
|
3691
3691
|
];
|
|
3692
3692
|
|
|
3693
|
+
// shown whenever the active language has any localizable content, even
|
|
3694
|
+
// at 100% — the dialog's "update existing" option lets users re-run
|
|
3695
|
+
// translation on already-translated entries.
|
|
3693
3696
|
const hasPendingTranslations =
|
|
3694
3697
|
this.autoTranslateEnabled &&
|
|
3695
3698
|
Boolean(activeLanguage) &&
|
|
3696
|
-
progress.total > 0
|
|
3697
|
-
progress.localized < progress.total;
|
|
3699
|
+
progress.total > 0;
|
|
3698
3700
|
|
|
3699
3701
|
return html`
|
|
3700
3702
|
<temba-editor-toolbar
|
|
@@ -10,6 +10,12 @@ export interface TranslationEntry {
|
|
|
10
10
|
attribute: string;
|
|
11
11
|
from: string;
|
|
12
12
|
to: string | null;
|
|
13
|
+
// Original array form of the source/target. For attributes whose value
|
|
14
|
+
// is an array (e.g. router case `arguments`), these preserve the per-item
|
|
15
|
+
// structure that `from`/`to` would otherwise lose to comma-joining. For
|
|
16
|
+
// scalar attributes the arrays contain a single item.
|
|
17
|
+
fromValues: string[];
|
|
18
|
+
toValues: string[] | null;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
export interface TranslationBundle {
|
|
@@ -97,12 +103,29 @@ export function findTranslations(
|
|
|
97
103
|
|
|
98
104
|
const toValue = to ? formatTranslationValue(to) : null;
|
|
99
105
|
|
|
106
|
+
const toArray = (value: any): string[] => {
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
return value.map((v) =>
|
|
109
|
+
v === null || v === undefined ? '' : String(v)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (value === null || value === undefined) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return [String(value)];
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const fromValues = toArray(from);
|
|
119
|
+
const toValues = to !== null && to !== undefined ? toArray(to) : null;
|
|
120
|
+
|
|
100
121
|
translations.push({
|
|
101
122
|
uuid,
|
|
102
123
|
type,
|
|
103
124
|
attribute,
|
|
104
125
|
from: fromValue,
|
|
105
|
-
to: toValue
|
|
126
|
+
to: toValue,
|
|
127
|
+
fromValues,
|
|
128
|
+
toValues
|
|
106
129
|
});
|
|
107
130
|
});
|
|
108
131
|
|
package/src/layout/Dialog.ts
CHANGED
|
@@ -172,10 +172,7 @@ export class Dialog extends ResizeElement {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
.dialog-footer {
|
|
175
|
-
background: var(
|
|
176
|
-
--dialog-footer-background,
|
|
177
|
-
var(--color-primary-light)
|
|
178
|
-
);
|
|
175
|
+
background: var(--dialog-footer-background, var(--color-primary-light));
|
|
179
176
|
padding: var(--dialog-footer-padding-top, 10px) 10px 10px;
|
|
180
177
|
display: flex;
|
|
181
178
|
flex-flow: row;
|
package/src/store/AppState.ts
CHANGED
|
@@ -185,6 +185,7 @@ export interface Activity {
|
|
|
185
185
|
|
|
186
186
|
export interface AppState {
|
|
187
187
|
features: string[];
|
|
188
|
+
brand: string;
|
|
188
189
|
|
|
189
190
|
flowDefinition: FlowDefinition;
|
|
190
191
|
flowInfo: FlowInfo;
|
|
@@ -265,6 +266,7 @@ export const zustand = createStore<AppState>()(
|
|
|
265
266
|
subscribeWithSelector(
|
|
266
267
|
immer((set, get) => ({
|
|
267
268
|
features: [] as string[],
|
|
269
|
+
brand: '',
|
|
268
270
|
languageNames: {},
|
|
269
271
|
canvasSize: { width: 0, height: 0 },
|
|
270
272
|
languageCode: '',
|
package/src/store/Store.ts
CHANGED
|
@@ -93,6 +93,9 @@ export class Store extends RapidElement {
|
|
|
93
93
|
@property({ type: String, attribute: 'shortcuts' })
|
|
94
94
|
shortcutsEndpoint: string;
|
|
95
95
|
|
|
96
|
+
@property({ type: String })
|
|
97
|
+
brand = '';
|
|
98
|
+
|
|
96
99
|
@property({ type: Object, attribute: false })
|
|
97
100
|
private schema: CompletionSchema;
|
|
98
101
|
|
|
@@ -154,6 +157,7 @@ export class Store extends RapidElement {
|
|
|
154
157
|
this.ready = false;
|
|
155
158
|
this.clearCache();
|
|
156
159
|
this.settings = JSON.parse(getCookie('settings') || '{}');
|
|
160
|
+
zustand.setState({ brand: this.brand });
|
|
157
161
|
|
|
158
162
|
/*
|
|
159
163
|
// This will create a shorthand unit
|