@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyaruka/temba-components",
3
- "version": "0.156.13",
3
+ "version": "0.156.15",
4
4
  "description": "Web components to support rapidpro and related projects",
5
5
  "author": "Nyaruka <code@nyaruka.coim>",
6
6
  "main": "dist/index.js",
@@ -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 = (results || []).map((r: any) => ({
210
- uuid: r.uuid,
211
- name: r.name
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 pending: { key: string; entry: TranslationEntry }[] = [];
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 (entry.to && entry.to.trim().length > 0) {
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.from || entry.from.trim().length === 0) {
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, entry } of pending) {
293
- const prevList = current[key];
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]: [entry.from] };
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 } = this.buildTranslationBatches();
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
- this.applyBatchTranslations(returned, keyToEntries);
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] = values;
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. Remember, AI models can make mistakes so it is important to review all of your translations to verify they are correct.
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
- : html`<div class="auto-translate-single-model">
566
- Using <strong>${this.models[0]?.name}</strong>
567
- </div>`}
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
- or check the AI model's settings if the problem persists.
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"
@@ -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
 
@@ -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',
@@ -434,7 +434,10 @@ export class RichEditor extends FieldElement {
434
434
  }
435
435
 
436
436
  private handleKeydown(e: KeyboardEvent): void {
437
- const mod = e.metaKey || e.ctrlKey;
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
@@ -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;
@@ -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: '',
@@ -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
@@ -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
  }