@nyaruka/temba-components 0.156.13 → 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 +18 -0
- package/dist/temba-components.js +159 -161
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Options.ts +13 -0
- package/src/flow/AutoTranslate.ts +200 -36
- package/src/flow/Editor.ts +4 -2
- package/src/flow/flow-translations.ts +24 -1
- package/src/flow/flow-utils.ts +17 -0
- package/src/flow/nodes/split_by_llm.ts +3 -1
- package/src/flow/nodes/split_by_llm_categorize.ts +3 -1
- package/src/form/RichEditor.ts +4 -1
- package/src/layout/Dialog.ts +1 -4
- package/src/store/AppState.ts +2 -0
- package/src/store/Store.ts +4 -0
- package/static/api/llms.json +11 -2
package/package.json
CHANGED
package/src/display/Options.ts
CHANGED
|
@@ -562,6 +562,19 @@ export class Options extends RapidElement {
|
|
|
562
562
|
return;
|
|
563
563
|
}
|
|
564
564
|
|
|
565
|
+
// Only intercept keys when the event originated within our owning component.
|
|
566
|
+
// Without this, any temba-options with populated options would swallow arrow
|
|
567
|
+
// keys document-wide, breaking cursor movement in unrelated text editors.
|
|
568
|
+
// All current usages render temba-options inside another custom element's
|
|
569
|
+
// shadow root, so scoping by host correctly distinguishes between editors.
|
|
570
|
+
if (!this.block) {
|
|
571
|
+
const root = this.getRootNode();
|
|
572
|
+
const host = root instanceof ShadowRoot ? root.host : null;
|
|
573
|
+
if (host && !evt.composedPath().includes(host)) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
565
578
|
if (this.options && this.options.length > 0) {
|
|
566
579
|
if ((evt.ctrlKey && evt.key === 'n') || evt.key === 'ArrowDown') {
|
|
567
580
|
this.moveCursor(1);
|
|
@@ -3,10 +3,11 @@ 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';
|
|
10
|
+
import { LLMModel, hasLLMRole } from './flow-utils';
|
|
10
11
|
|
|
11
12
|
interface TranslationModel {
|
|
12
13
|
uuid: string;
|
|
@@ -65,10 +66,6 @@ export class AutoTranslate extends RapidElement {
|
|
|
65
66
|
text-decoration: underline;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
.auto-translate-single-model {
|
|
69
|
-
font-size: 13px;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
69
|
.auto-translate-status {
|
|
73
70
|
display: flex;
|
|
74
71
|
align-items: center;
|
|
@@ -140,6 +137,9 @@ export class AutoTranslate extends RapidElement {
|
|
|
140
137
|
@property({ type: Boolean })
|
|
141
138
|
disabled = false;
|
|
142
139
|
|
|
140
|
+
@fromStore(zustand, (state: AppState) => state.brand)
|
|
141
|
+
private brand!: string;
|
|
142
|
+
|
|
143
143
|
// Reactive flag the host can read to show "translating" state in UI
|
|
144
144
|
// adjacent to this component (e.g. the toolbar button).
|
|
145
145
|
@property({ type: Boolean, reflect: true })
|
|
@@ -169,6 +169,9 @@ export class AutoTranslate extends RapidElement {
|
|
|
169
169
|
@state()
|
|
170
170
|
interrupt = false;
|
|
171
171
|
|
|
172
|
+
@state()
|
|
173
|
+
updateExisting = false;
|
|
174
|
+
|
|
172
175
|
// Tracks whether the dialog has ever opened so we can keep it mounted
|
|
173
176
|
// afterwards (so it sees its own close transition) without paying
|
|
174
177
|
// for an empty hidden dialog before that.
|
|
@@ -191,6 +194,7 @@ export class AutoTranslate extends RapidElement {
|
|
|
191
194
|
this.error = null;
|
|
192
195
|
this.errorExpanded = false;
|
|
193
196
|
this.selectedModel = null;
|
|
197
|
+
this.updateExisting = false;
|
|
194
198
|
this.dialogOpen = true;
|
|
195
199
|
await this.loadModels();
|
|
196
200
|
|
|
@@ -203,13 +207,15 @@ export class AutoTranslate extends RapidElement {
|
|
|
203
207
|
this.modelsLoading = true;
|
|
204
208
|
try {
|
|
205
209
|
const store = getStore();
|
|
206
|
-
const results = store
|
|
207
|
-
? await store.getResults(MODELS_ENDPOINT, { force: true })
|
|
210
|
+
const results: LLMModel[] = store
|
|
211
|
+
? ((await store.getResults(MODELS_ENDPOINT, { force: true })) ?? [])
|
|
208
212
|
: [];
|
|
209
|
-
this.models =
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
this.models = results
|
|
214
|
+
.filter((r) => hasLLMRole(r, 'editing'))
|
|
215
|
+
.map((r) => ({
|
|
216
|
+
uuid: r.uuid,
|
|
217
|
+
name: r.name
|
|
218
|
+
}));
|
|
213
219
|
} catch (err) {
|
|
214
220
|
console.error('Failed to load AI models', err);
|
|
215
221
|
this.models = [];
|
|
@@ -225,6 +231,11 @@ export class AutoTranslate extends RapidElement {
|
|
|
225
231
|
this.selectedModel = next ? { uuid: next.uuid, name: next.name } : null;
|
|
226
232
|
}
|
|
227
233
|
|
|
234
|
+
private handleUpdateExistingChange(event: Event): void {
|
|
235
|
+
const checkbox = event.target as any;
|
|
236
|
+
this.updateExisting = !!checkbox?.checked;
|
|
237
|
+
}
|
|
238
|
+
|
|
228
239
|
private confirmTranslate(): void {
|
|
229
240
|
if (!this.selectedModel) {
|
|
230
241
|
return;
|
|
@@ -250,36 +261,123 @@ export class AutoTranslate extends RapidElement {
|
|
|
250
261
|
/**
|
|
251
262
|
* Builds batches of translation requests from all untranslated entries,
|
|
252
263
|
* grouping so each request's serialized payload stays under
|
|
253
|
-
* TRANSLATION_BATCH_CHAR_LIMIT.
|
|
264
|
+
* TRANSLATION_BATCH_CHAR_LIMIT. Skips entries whose source matches a
|
|
265
|
+
* translation already present in the flow (those are returned in
|
|
266
|
+
* preTranslated for direct application), and dedupes pending entries
|
|
267
|
+
* sharing identical source arrays so each unique array is sent at most
|
|
268
|
+
* once (duplicates are returned in duplicateKeyToCanonical for
|
|
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.
|
|
254
273
|
*/
|
|
255
274
|
private buildTranslationBatches(): {
|
|
256
275
|
keyToEntries: Map<string, TranslationEntry[]>;
|
|
257
276
|
batches: { items: Record<string, string[]> }[];
|
|
277
|
+
preTranslated: Map<string, string[]>;
|
|
278
|
+
duplicateKeyToCanonical: Map<string, string>;
|
|
258
279
|
} {
|
|
259
280
|
const keyToEntries = new Map<string, TranslationEntry[]>();
|
|
260
281
|
const batches: { items: Record<string, string[]> }[] = [];
|
|
282
|
+
const preTranslated = new Map<string, string[]>();
|
|
283
|
+
const duplicateKeyToCanonical = new Map<string, string>();
|
|
261
284
|
|
|
262
285
|
if (!this.definition) {
|
|
263
|
-
return { keyToEntries, batches };
|
|
286
|
+
return { keyToEntries, batches, preTranslated, duplicateKeyToCanonical };
|
|
264
287
|
}
|
|
265
288
|
|
|
266
289
|
const bundles = buildTranslationBundles(this.definition, this.languageCode);
|
|
267
|
-
const
|
|
290
|
+
const updateExisting = this.updateExisting;
|
|
291
|
+
|
|
292
|
+
// Map source-content -> already-stored translation array, built from
|
|
293
|
+
// entries that have a translation in the live localization map. Reading
|
|
294
|
+
// the stored array (not entry.to) preserves the original shape. Skipped
|
|
295
|
+
// in update mode so we always re-translate via the LLM.
|
|
296
|
+
const existingByContent = new Map<string, string[]>();
|
|
297
|
+
const localization =
|
|
298
|
+
this.definition.localization?.[this.languageCode] || {};
|
|
299
|
+
|
|
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
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
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.
|
|
334
|
+
const valuesByKey = new Map<string, string[]>();
|
|
268
335
|
|
|
269
336
|
for (const bundle of bundles) {
|
|
270
337
|
for (const entry of bundle.translations) {
|
|
271
|
-
if (
|
|
338
|
+
if (
|
|
339
|
+
!updateExisting &&
|
|
340
|
+
entry.toValues &&
|
|
341
|
+
entry.toValues.some((v) => v && v.trim().length > 0)
|
|
342
|
+
) {
|
|
272
343
|
continue;
|
|
273
344
|
}
|
|
274
|
-
if (!entry.
|
|
345
|
+
if (!entry.fromValues || entry.fromValues.length === 0) {
|
|
275
346
|
continue;
|
|
276
347
|
}
|
|
277
348
|
const key = `${entry.uuid}:${entry.attribute}`;
|
|
278
|
-
pending.push({ key, entry });
|
|
279
349
|
const list = keyToEntries.get(key) || [];
|
|
280
350
|
list.push(entry);
|
|
281
351
|
keyToEntries.set(key, list);
|
|
352
|
+
if (!valuesByKey.has(key)) {
|
|
353
|
+
valuesByKey.set(key, entry.fromValues);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Split pending keys into pre-translated (matching an existing
|
|
359
|
+
// translation), duplicates (sharing content with an earlier pending
|
|
360
|
+
// key), and unique canonical keys that need to be sent to the LLM.
|
|
361
|
+
const canonicalByContent = new Map<string, string>();
|
|
362
|
+
const canonicalKeysWithValues: { key: string; values: string[] }[] = [];
|
|
363
|
+
|
|
364
|
+
for (const [key, values] of valuesByKey) {
|
|
365
|
+
const contentKey = JSON.stringify(values);
|
|
366
|
+
|
|
367
|
+
const existing = existingByContent.get(contentKey);
|
|
368
|
+
if (existing && existing.length === values.length) {
|
|
369
|
+
preTranslated.set(key, existing);
|
|
370
|
+
continue;
|
|
282
371
|
}
|
|
372
|
+
|
|
373
|
+
const canonical = canonicalByContent.get(contentKey);
|
|
374
|
+
if (canonical) {
|
|
375
|
+
duplicateKeyToCanonical.set(key, canonical);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
canonicalByContent.set(contentKey, key);
|
|
380
|
+
canonicalKeysWithValues.push({ key, values });
|
|
283
381
|
}
|
|
284
382
|
|
|
285
383
|
const source = this.definition.language;
|
|
@@ -289,18 +387,15 @@ export class AutoTranslate extends RapidElement {
|
|
|
289
387
|
|
|
290
388
|
let current: Record<string, string[]> = {};
|
|
291
389
|
|
|
292
|
-
for (const { key,
|
|
293
|
-
const
|
|
294
|
-
const nextList = prevList ? [...prevList, entry.from] : [entry.from];
|
|
295
|
-
const tentative = { ...current, [key]: nextList };
|
|
296
|
-
|
|
390
|
+
for (const { key, values } of canonicalKeysWithValues) {
|
|
391
|
+
const tentative = { ...current, [key]: values };
|
|
297
392
|
const tentativeSize = measurePayload(tentative);
|
|
298
393
|
const batchHasItems = Object.keys(current).length > 0;
|
|
299
394
|
|
|
300
395
|
if (tentativeSize > TRANSLATION_BATCH_CHAR_LIMIT && batchHasItems) {
|
|
301
396
|
// a single oversized entry still ships on its own
|
|
302
397
|
batches.push({ items: current });
|
|
303
|
-
current = { [key]:
|
|
398
|
+
current = { [key]: values };
|
|
304
399
|
} else {
|
|
305
400
|
current = tentative;
|
|
306
401
|
}
|
|
@@ -310,7 +405,7 @@ export class AutoTranslate extends RapidElement {
|
|
|
310
405
|
batches.push({ items: current });
|
|
311
406
|
}
|
|
312
407
|
|
|
313
|
-
return { keyToEntries, batches };
|
|
408
|
+
return { keyToEntries, batches, preTranslated, duplicateKeyToCanonical };
|
|
314
409
|
}
|
|
315
410
|
|
|
316
411
|
/**
|
|
@@ -326,11 +421,31 @@ export class AutoTranslate extends RapidElement {
|
|
|
326
421
|
return;
|
|
327
422
|
}
|
|
328
423
|
|
|
329
|
-
const { keyToEntries, batches } =
|
|
424
|
+
const { keyToEntries, batches, preTranslated, duplicateKeyToCanonical } =
|
|
425
|
+
this.buildTranslationBatches();
|
|
426
|
+
|
|
427
|
+
// Apply entries that match an existing translation immediately so we
|
|
428
|
+
// don't need an LLM round-trip for them.
|
|
429
|
+
if (preTranslated.size > 0) {
|
|
430
|
+
this.applyBatchTranslations(
|
|
431
|
+
Object.fromEntries(preTranslated),
|
|
432
|
+
keyToEntries
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
330
436
|
if (batches.length === 0) {
|
|
331
437
|
return;
|
|
332
438
|
}
|
|
333
439
|
|
|
440
|
+
// Inverse of duplicateKeyToCanonical so we can quickly find which keys
|
|
441
|
+
// need to be back-filled when a canonical key's translation lands.
|
|
442
|
+
const duplicatesByCanonical = new Map<string, string[]>();
|
|
443
|
+
for (const [dup, canonical] of duplicateKeyToCanonical) {
|
|
444
|
+
const list = duplicatesByCanonical.get(canonical) || [];
|
|
445
|
+
list.push(dup);
|
|
446
|
+
duplicatesByCanonical.set(canonical, list);
|
|
447
|
+
}
|
|
448
|
+
|
|
334
449
|
this.running = true;
|
|
335
450
|
this.interrupt = false;
|
|
336
451
|
this.progress = { done: 0, total: batches.length };
|
|
@@ -358,7 +473,16 @@ export class AutoTranslate extends RapidElement {
|
|
|
358
473
|
|
|
359
474
|
if (response.status >= 200 && response.status < 300) {
|
|
360
475
|
const returned: Record<string, string[]> = response.json?.items || {};
|
|
361
|
-
|
|
476
|
+
const expanded: Record<string, string[]> = { ...returned };
|
|
477
|
+
for (const canonical of Object.keys(returned)) {
|
|
478
|
+
const dups = duplicatesByCanonical.get(canonical);
|
|
479
|
+
if (dups) {
|
|
480
|
+
for (const dup of dups) {
|
|
481
|
+
expanded[dup] = returned[canonical];
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
this.applyBatchTranslations(expanded, keyToEntries);
|
|
362
486
|
} else {
|
|
363
487
|
this.error =
|
|
364
488
|
response.json?.error ||
|
|
@@ -412,8 +536,39 @@ export class AutoTranslate extends RapidElement {
|
|
|
412
536
|
const liveDefinition = zustand.getState().flowDefinition;
|
|
413
537
|
const existing =
|
|
414
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
|
+
|
|
415
570
|
const merged = updatesByUuid.get(uuid) || { ...existing };
|
|
416
|
-
merged[attribute] =
|
|
571
|
+
merged[attribute] = mergedValues;
|
|
417
572
|
updatesByUuid.set(uuid, merged);
|
|
418
573
|
}
|
|
419
574
|
|
|
@@ -547,10 +702,15 @@ export class AutoTranslate extends RapidElement {
|
|
|
547
702
|
|
|
548
703
|
const selected = this.selectedModel ? [this.selectedModel] : [];
|
|
549
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,`;
|
|
550
709
|
return html`
|
|
551
710
|
<p>
|
|
552
711
|
All remaining text for <strong>${languageName}</strong> will be
|
|
553
|
-
translated automatically.
|
|
712
|
+
translated automatically. ${aiClause} so it is important to review all
|
|
713
|
+
of your translations to verify they are correct.
|
|
554
714
|
</p>
|
|
555
715
|
${this.models.length > 1
|
|
556
716
|
? html`<temba-select
|
|
@@ -558,13 +718,19 @@ export class AutoTranslate extends RapidElement {
|
|
|
558
718
|
endpoint="${MODELS_ENDPOINT}"
|
|
559
719
|
valueKey="uuid"
|
|
560
720
|
.values=${selected}
|
|
721
|
+
.shouldExclude=${(option: LLMModel) =>
|
|
722
|
+
!hasLLMRole(option, 'editing')}
|
|
561
723
|
?searchable=${true}
|
|
562
724
|
placeholder="Select an AI model"
|
|
563
725
|
@change=${this.handleModelChange}
|
|
564
726
|
></temba-select>`
|
|
565
|
-
:
|
|
566
|
-
|
|
567
|
-
|
|
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>
|
|
568
734
|
`;
|
|
569
735
|
}
|
|
570
736
|
|
|
@@ -610,13 +776,11 @@ export class AutoTranslate extends RapidElement {
|
|
|
610
776
|
return html`
|
|
611
777
|
<div class="auto-translate-error-block">
|
|
612
778
|
<p class="auto-translate-error-help">
|
|
613
|
-
Any translations already applied have been kept. You can try again,
|
|
614
|
-
|
|
779
|
+
Any translations already applied have been kept. You can try again, or
|
|
780
|
+
check the AI model's settings if the problem persists.
|
|
615
781
|
</p>
|
|
616
782
|
${this.errorExpanded
|
|
617
|
-
? html`<pre class="auto-translate-error-details"
|
|
618
|
-
${this.error}</pre
|
|
619
|
-
>`
|
|
783
|
+
? html`<pre class="auto-translate-error-details">${this.error}</pre>`
|
|
620
784
|
: html`<button
|
|
621
785
|
class="auto-translate-error-toggle"
|
|
622
786
|
type="button"
|
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/flow/flow-utils.ts
CHANGED
|
@@ -15,3 +15,20 @@ export function shouldExcludeFlow(flow: any): boolean {
|
|
|
15
15
|
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
export type LLMRole = 'engine' | 'editing';
|
|
20
|
+
|
|
21
|
+
export interface LLMModel {
|
|
22
|
+
uuid: string;
|
|
23
|
+
name: string;
|
|
24
|
+
type?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
roles?: LLMRole[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasLLMRole(
|
|
30
|
+
model: { roles?: string[] } | null | undefined,
|
|
31
|
+
role: LLMRole
|
|
32
|
+
): boolean {
|
|
33
|
+
return model?.roles?.includes(role) ?? false;
|
|
34
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
renderLineItem,
|
|
9
9
|
getLlmIcon
|
|
10
10
|
} from '../utils';
|
|
11
|
+
import { LLMModel, hasLLMRole } from '../flow-utils';
|
|
11
12
|
|
|
12
13
|
export const split_by_llm: NodeConfig = {
|
|
13
14
|
type: 'split_by_llm',
|
|
@@ -47,7 +48,8 @@ export const split_by_llm: NodeConfig = {
|
|
|
47
48
|
searchable: true,
|
|
48
49
|
valueKey: 'uuid',
|
|
49
50
|
nameKey: 'name',
|
|
50
|
-
placeholder: 'Select an LLM...'
|
|
51
|
+
placeholder: 'Select an LLM...',
|
|
52
|
+
shouldExclude: (option: LLMModel) => !hasLLMRole(option, 'engine')
|
|
51
53
|
},
|
|
52
54
|
input: {
|
|
53
55
|
type: 'text',
|
|
@@ -3,6 +3,7 @@ import { CallLLM, Node } from '../../store/flow-definition';
|
|
|
3
3
|
import { generateUUID, createMultiCategoryRouter } from '../../utils';
|
|
4
4
|
import { html } from 'lit';
|
|
5
5
|
import { validateWith } from '../utils';
|
|
6
|
+
import { LLMModel, hasLLMRole } from '../flow-utils';
|
|
6
7
|
|
|
7
8
|
export const split_by_llm_categorize: NodeConfig = {
|
|
8
9
|
type: 'split_by_llm_categorize',
|
|
@@ -19,7 +20,8 @@ export const split_by_llm_categorize: NodeConfig = {
|
|
|
19
20
|
endpoint: '/api/internal/llms.json',
|
|
20
21
|
valueKey: 'uuid',
|
|
21
22
|
nameKey: 'name',
|
|
22
|
-
placeholder: 'Select an LLM...'
|
|
23
|
+
placeholder: 'Select an LLM...',
|
|
24
|
+
shouldExclude: (option: LLMModel) => !hasLLMRole(option, 'engine')
|
|
23
25
|
},
|
|
24
26
|
input: {
|
|
25
27
|
type: 'text',
|
package/src/form/RichEditor.ts
CHANGED
|
@@ -434,7 +434,10 @@ export class RichEditor extends FieldElement {
|
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
private handleKeydown(e: KeyboardEvent): void {
|
|
437
|
-
|
|
437
|
+
// On macOS, Cmd is the undo/redo modifier; leave Ctrl alone so Cocoa
|
|
438
|
+
// text bindings like Ctrl+K (kill) and Ctrl+Y (yank) keep working.
|
|
439
|
+
const isMac = /Mac|iPhone|iPad/.test(navigator.userAgent);
|
|
440
|
+
const mod = isMac ? e.metaKey : e.ctrlKey;
|
|
438
441
|
|
|
439
442
|
// Undo/redo via keyboard: preventDefault here suppresses the subsequent
|
|
440
443
|
// beforeinput event, avoiding double-handling
|
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
|
package/static/api/llms.json
CHANGED
|
@@ -6,13 +6,22 @@
|
|
|
6
6
|
"uuid": "2399e7d6-fcdf-4e47-a835-f3bdb7f80938",
|
|
7
7
|
"name": "GPT 4.1",
|
|
8
8
|
"type": "openai",
|
|
9
|
-
"description": "General-purpose reasoning model"
|
|
9
|
+
"description": "General-purpose reasoning model",
|
|
10
|
+
"roles": ["engine", "editing"]
|
|
10
11
|
},
|
|
11
12
|
{
|
|
12
13
|
"uuid": "4399e7d6-fcdf-4e47-a835-f3bdb7f80938",
|
|
13
14
|
"name": "GPT 5",
|
|
14
15
|
"type": "openai",
|
|
15
|
-
"description": "Latest experimental model"
|
|
16
|
+
"description": "Latest experimental model",
|
|
17
|
+
"roles": ["engine"]
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"uuid": "5499e7d6-fcdf-4e47-a835-f3bdb7f80938",
|
|
21
|
+
"name": "GPT 4.1 Mini",
|
|
22
|
+
"type": "openai",
|
|
23
|
+
"description": "Lightweight model for editing tasks",
|
|
24
|
+
"roles": ["editing"]
|
|
16
25
|
}
|
|
17
26
|
]
|
|
18
27
|
}
|