@nyaruka/temba-components 0.131.2 → 0.131.3
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 +14 -0
- package/demo/components/floating-tabs/example.html +400 -0
- package/demo/components/flow/index.html +1 -1
- package/demo/data/flows/sample-flow.json +41 -2
- package/demo/data/flows/voicemail.json +613 -0
- package/demo/index.html +6 -0
- package/dist/locales/es.js +5 -5
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +5 -5
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/locale-codes.js +11 -2
- package/dist/locales/locale-codes.js.map +1 -1
- package/dist/locales/pt.js +5 -5
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +1109 -535
- package/dist/temba-components.js.map +1 -1
- package/out-tsc/src/display/FloatingTab.js +167 -0
- package/out-tsc/src/display/FloatingTab.js.map +1 -0
- package/out-tsc/src/display/ProgressBar.js +22 -2
- package/out-tsc/src/display/ProgressBar.js.map +1 -1
- package/out-tsc/src/events.js.map +1 -1
- package/out-tsc/src/flow/CanvasNode.js +165 -31
- package/out-tsc/src/flow/CanvasNode.js.map +1 -1
- package/out-tsc/src/flow/Editor.js +857 -3
- package/out-tsc/src/flow/Editor.js.map +1 -1
- package/out-tsc/src/flow/NodeEditor.js +239 -19
- package/out-tsc/src/flow/NodeEditor.js.map +1 -1
- package/out-tsc/src/flow/NodeTypeSelector.js +44 -3
- package/out-tsc/src/flow/NodeTypeSelector.js.map +1 -1
- package/out-tsc/src/flow/StickyNote.js +12 -3
- package/out-tsc/src/flow/StickyNote.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_groups.js +2 -1
- package/out-tsc/src/flow/actions/add_contact_groups.js.map +1 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js +2 -1
- package/out-tsc/src/flow/actions/add_contact_urn.js.map +1 -1
- package/out-tsc/src/flow/actions/add_input_labels.js +2 -1
- package/out-tsc/src/flow/actions/add_input_labels.js.map +1 -1
- package/out-tsc/src/flow/actions/play_audio.js +2 -1
- package/out-tsc/src/flow/actions/play_audio.js.map +1 -1
- package/out-tsc/src/flow/actions/remove_contact_groups.js +2 -1
- package/out-tsc/src/flow/actions/remove_contact_groups.js.map +1 -1
- package/out-tsc/src/flow/actions/request_optin.js +1 -0
- package/out-tsc/src/flow/actions/request_optin.js.map +1 -1
- package/out-tsc/src/flow/actions/say_msg.js +2 -1
- package/out-tsc/src/flow/actions/say_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/send_broadcast.js +2 -1
- package/out-tsc/src/flow/actions/send_broadcast.js.map +1 -1
- package/out-tsc/src/flow/actions/send_email.js +2 -1
- package/out-tsc/src/flow/actions/send_email.js.map +1 -1
- package/out-tsc/src/flow/actions/send_msg.js +93 -3
- package/out-tsc/src/flow/actions/send_msg.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_channel.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_field.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_field.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_language.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_language.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_name.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_name.js.map +1 -1
- package/out-tsc/src/flow/actions/set_contact_status.js +2 -1
- package/out-tsc/src/flow/actions/set_contact_status.js.map +1 -1
- package/out-tsc/src/flow/actions/set_run_result.js +2 -1
- package/out-tsc/src/flow/actions/set_run_result.js.map +1 -1
- package/out-tsc/src/flow/actions/start_session.js +2 -1
- package/out-tsc/src/flow/actions/start_session.js.map +1 -1
- package/out-tsc/src/flow/config.js +2 -10
- package/out-tsc/src/flow/config.js.map +1 -1
- package/out-tsc/src/flow/nodes/shared.js +54 -0
- package/out-tsc/src/flow/nodes/shared.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_airtime.js +9 -3
- package/out-tsc/src/flow/nodes/split_by_airtime.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_contact_field.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_contact_field.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_expression.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_expression.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_groups.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_groups.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_intent.js +3 -2
- package/out-tsc/src/flow/nodes/split_by_intent.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm.js +9 -2
- package/out-tsc/src/flow/nodes/split_by_llm.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js +9 -2
- package/out-tsc/src/flow/nodes/split_by_llm_categorize.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_random.js +8 -2
- package/out-tsc/src/flow/nodes/split_by_random.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_resthook.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_resthook.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_run_result.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_run_result.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_scheme.js +8 -3
- package/out-tsc/src/flow/nodes/split_by_scheme.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_subflow.js +8 -2
- package/out-tsc/src/flow/nodes/split_by_subflow.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_ticket.js +8 -2
- package/out-tsc/src/flow/nodes/split_by_ticket.js.map +1 -1
- package/out-tsc/src/flow/nodes/split_by_webhook.js +8 -2
- package/out-tsc/src/flow/nodes/split_by_webhook.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_digits.js +3 -2
- package/out-tsc/src/flow/nodes/wait_for_digits.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_menu.js +3 -2
- package/out-tsc/src/flow/nodes/wait_for_menu.js.map +1 -1
- package/out-tsc/src/flow/nodes/wait_for_response.js +8 -3
- package/out-tsc/src/flow/nodes/wait_for_response.js.map +1 -1
- package/out-tsc/src/flow/types.js +15 -0
- package/out-tsc/src/flow/types.js.map +1 -1
- package/out-tsc/src/layout/FloatingWindow.js +346 -0
- package/out-tsc/src/layout/FloatingWindow.js.map +1 -0
- package/out-tsc/src/live/ContactChat.js +3 -19
- package/out-tsc/src/live/ContactChat.js.map +1 -1
- package/out-tsc/src/locales/es.js +5 -5
- package/out-tsc/src/locales/es.js.map +1 -1
- package/out-tsc/src/locales/fr.js +5 -5
- package/out-tsc/src/locales/fr.js.map +1 -1
- package/out-tsc/src/locales/locale-codes.js +11 -2
- package/out-tsc/src/locales/locale-codes.js.map +1 -1
- package/out-tsc/src/locales/pt.js +5 -5
- package/out-tsc/src/locales/pt.js.map +1 -1
- package/out-tsc/src/store/AppState.js +67 -0
- package/out-tsc/src/store/AppState.js.map +1 -1
- package/out-tsc/temba-modules.js +4 -0
- package/out-tsc/temba-modules.js.map +1 -1
- package/out-tsc/test/temba-floating-tab.test.js +91 -0
- package/out-tsc/test/temba-floating-tab.test.js.map +1 -0
- package/out-tsc/test/temba-floating-window.test.js +301 -0
- package/out-tsc/test/temba-floating-window.test.js.map +1 -0
- package/out-tsc/test/temba-flow-editor-node.test.js +117 -0
- package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
- package/out-tsc/test/temba-localization.test.js +471 -0
- package/out-tsc/test/temba-localization.test.js.map +1 -0
- package/out-tsc/test/temba-node-type-selector.test.js +150 -0
- package/out-tsc/test/temba-node-type-selector.test.js.map +1 -1
- package/out-tsc/test/utils.test.js +18 -0
- package/out-tsc/test/utils.test.js.map +1 -1
- package/package.json +1 -1
- package/screenshots/truth/floating-tab/default.png +0 -0
- package/screenshots/truth/floating-tab/gray.png +0 -0
- package/screenshots/truth/floating-tab/green.png +0 -0
- package/screenshots/truth/floating-tab/hidden.png +0 -0
- package/screenshots/truth/floating-tab/hover.png +0 -0
- package/screenshots/truth/floating-tab/purple.png +0 -0
- package/screenshots/truth/floating-window/chromeless.png +0 -0
- package/screenshots/truth/floating-window/custom-size.png +0 -0
- package/screenshots/truth/floating-window/default.png +0 -0
- package/screenshots/truth/floating-window/with-header.png +0 -0
- package/screenshots/truth/node-type-selector/action-mode.png +0 -0
- package/screenshots/truth/node-type-selector/split-mode.png +0 -0
- package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
- package/src/display/FloatingTab.ts +174 -0
- package/src/display/ProgressBar.ts +22 -2
- package/src/events.ts +2 -4
- package/src/flow/CanvasNode.ts +190 -32
- package/src/flow/Editor.ts +1040 -3
- package/src/flow/NodeEditor.ts +317 -19
- package/src/flow/NodeTypeSelector.ts +47 -3
- package/src/flow/StickyNote.ts +12 -3
- package/src/flow/actions/add_contact_groups.ts +2 -1
- package/src/flow/actions/add_contact_urn.ts +3 -1
- package/src/flow/actions/add_input_labels.ts +2 -1
- package/src/flow/actions/play_audio.ts +2 -1
- package/src/flow/actions/remove_contact_groups.ts +3 -1
- package/src/flow/actions/request_optin.ts +1 -0
- package/src/flow/actions/say_msg.ts +2 -1
- package/src/flow/actions/send_broadcast.ts +2 -1
- package/src/flow/actions/send_email.ts +3 -1
- package/src/flow/actions/send_msg.ts +134 -3
- package/src/flow/actions/set_contact_channel.ts +2 -1
- package/src/flow/actions/set_contact_field.ts +2 -1
- package/src/flow/actions/set_contact_language.ts +3 -1
- package/src/flow/actions/set_contact_name.ts +2 -1
- package/src/flow/actions/set_contact_status.ts +2 -1
- package/src/flow/actions/set_run_result.ts +2 -1
- package/src/flow/actions/start_session.ts +3 -1
- package/src/flow/config.ts +2 -12
- package/src/flow/nodes/shared.ts +70 -1
- package/src/flow/nodes/split_by_airtime.ts +20 -3
- package/src/flow/nodes/split_by_contact_field.ts +13 -3
- package/src/flow/nodes/split_by_expression.ts +13 -3
- package/src/flow/nodes/split_by_groups.ts +13 -3
- package/src/flow/nodes/split_by_intent.ts +3 -2
- package/src/flow/nodes/split_by_llm.ts +19 -2
- package/src/flow/nodes/split_by_llm_categorize.ts +19 -2
- package/src/flow/nodes/split_by_random.ts +12 -2
- package/src/flow/nodes/split_by_resthook.ts +13 -3
- package/src/flow/nodes/split_by_run_result.ts +13 -3
- package/src/flow/nodes/split_by_scheme.ts +13 -3
- package/src/flow/nodes/split_by_subflow.ts +12 -2
- package/src/flow/nodes/split_by_ticket.ts +12 -2
- package/src/flow/nodes/split_by_webhook.ts +12 -2
- package/src/flow/nodes/wait_for_digits.ts +3 -2
- package/src/flow/nodes/wait_for_menu.ts +3 -2
- package/src/flow/nodes/wait_for_response.ts +13 -3
- package/src/flow/types.ts +47 -0
- package/src/layout/FloatingWindow.ts +386 -0
- package/src/live/ContactChat.ts +4 -19
- package/src/locales/es.ts +18 -13
- package/src/locales/fr.ts +18 -13
- package/src/locales/locale-codes.ts +11 -2
- package/src/locales/pt.ts +18 -13
- package/src/store/AppState.ts +104 -0
- package/static/api/llms.json +18 -0
- package/temba-modules.ts +4 -0
- package/test/temba-floating-tab.test.ts +110 -0
- package/test/temba-floating-window.test.ts +477 -0
- package/test/temba-flow-editor-node.test.ts +144 -0
- package/test/temba-localization.test.ts +611 -0
- package/test/temba-node-type-selector.test.ts +203 -0
- package/test/utils.test.ts +20 -0
- package/test-assets/contacts/history.json +5 -6
- package/test-assets/select/llms.json +2 -2
- package/web-dev-server.config.mjs +47 -1
- package/web-test-runner.config.mjs +0 -1
- package/out-tsc/src/flow/nodes/wait_for_audio.js +0 -7
- package/out-tsc/src/flow/nodes/wait_for_audio.js.map +0 -1
- package/out-tsc/src/flow/nodes/wait_for_image.js +0 -7
- package/out-tsc/src/flow/nodes/wait_for_image.js.map +0 -1
- package/out-tsc/src/flow/nodes/wait_for_location.js +0 -7
- package/out-tsc/src/flow/nodes/wait_for_location.js.map +0 -1
- package/out-tsc/src/flow/nodes/wait_for_video.js +0 -7
- package/out-tsc/src/flow/nodes/wait_for_video.js.map +0 -1
- package/src/flow/nodes/wait_for_audio.ts +0 -7
- package/src/flow/nodes/wait_for_image.ts +0 -7
- package/src/flow/nodes/wait_for_location.ts +0 -7
- package/src/flow/nodes/wait_for_video.ts +0 -7
package/src/flow/Editor.ts
CHANGED
|
@@ -13,9 +13,10 @@ import { AppState, fromStore, zustand } from '../store/AppState';
|
|
|
13
13
|
import { RapidElement } from '../RapidElement';
|
|
14
14
|
import { repeat } from 'lit-html/directives/repeat.js';
|
|
15
15
|
import { CustomEventType } from '../interfaces';
|
|
16
|
-
import { generateUUID } from '../utils';
|
|
16
|
+
import { generateUUID, postJSON } from '../utils';
|
|
17
17
|
import { ACTION_CONFIG, NODE_CONFIG } from './config';
|
|
18
18
|
import { ACTION_GROUP_METADATA } from './types';
|
|
19
|
+
import { Checkbox } from '../form/Checkbox';
|
|
19
20
|
|
|
20
21
|
import { Plumber } from './Plumber';
|
|
21
22
|
import { CanvasNode } from './CanvasNode';
|
|
@@ -60,6 +61,35 @@ export interface SelectionBox {
|
|
|
60
61
|
|
|
61
62
|
const DRAG_THRESHOLD = 5;
|
|
62
63
|
|
|
64
|
+
type TranslationType = 'property' | 'category';
|
|
65
|
+
|
|
66
|
+
interface TranslationEntry {
|
|
67
|
+
uuid: string;
|
|
68
|
+
type: TranslationType;
|
|
69
|
+
attribute: string;
|
|
70
|
+
from: string;
|
|
71
|
+
to: string | null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface TranslationBundle {
|
|
75
|
+
nodeUuid: string;
|
|
76
|
+
actionUuid?: string;
|
|
77
|
+
translations: TranslationEntry[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface TranslationModel {
|
|
81
|
+
uuid: string;
|
|
82
|
+
name: string;
|
|
83
|
+
description?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface LocalizationUpdate {
|
|
87
|
+
uuid: string;
|
|
88
|
+
translations: Record<string, string>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const AUTO_TRANSLATE_MODELS_ENDPOINT = '/api/internal/llms.json';
|
|
92
|
+
|
|
63
93
|
// Offset for positioning dropped action node relative to mouse cursor
|
|
64
94
|
// Keep small to make drop location close to cursor position
|
|
65
95
|
const DROP_PREVIEW_OFFSET_X = 20;
|
|
@@ -83,6 +113,12 @@ export class Editor extends RapidElement {
|
|
|
83
113
|
@property({ type: String })
|
|
84
114
|
public version: string;
|
|
85
115
|
|
|
116
|
+
@property({ type: String })
|
|
117
|
+
public flowType: string = 'message';
|
|
118
|
+
|
|
119
|
+
@property({ type: Array })
|
|
120
|
+
public features: string[] = [];
|
|
121
|
+
|
|
86
122
|
@fromStore(zustand, (state: AppState) => state.flowDefinition)
|
|
87
123
|
private definition!: FlowDefinition;
|
|
88
124
|
|
|
@@ -92,6 +128,12 @@ export class Editor extends RapidElement {
|
|
|
92
128
|
@fromStore(zustand, (state: AppState) => state.dirtyDate)
|
|
93
129
|
private dirtyDate!: Date;
|
|
94
130
|
|
|
131
|
+
@fromStore(zustand, (state: AppState) => state.languageCode)
|
|
132
|
+
private languageCode!: string;
|
|
133
|
+
|
|
134
|
+
@fromStore(zustand, (state: AppState) => state.isTranslating)
|
|
135
|
+
private isTranslating!: boolean;
|
|
136
|
+
|
|
95
137
|
// Drag state
|
|
96
138
|
@state()
|
|
97
139
|
private isDragging = false;
|
|
@@ -129,6 +171,31 @@ export class Editor extends RapidElement {
|
|
|
129
171
|
@state()
|
|
130
172
|
private isValidTarget = true;
|
|
131
173
|
|
|
174
|
+
@state()
|
|
175
|
+
private localizationWindowHidden = true;
|
|
176
|
+
|
|
177
|
+
@state()
|
|
178
|
+
private translationFilters: { categories: boolean } = {
|
|
179
|
+
categories: false
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
@state()
|
|
183
|
+
private translationSettingsExpanded = false;
|
|
184
|
+
|
|
185
|
+
@state()
|
|
186
|
+
private autoTranslateDialogOpen = false;
|
|
187
|
+
|
|
188
|
+
@state()
|
|
189
|
+
private autoTranslating = false;
|
|
190
|
+
|
|
191
|
+
@state()
|
|
192
|
+
private autoTranslateModel: TranslationModel | null = null;
|
|
193
|
+
|
|
194
|
+
@state()
|
|
195
|
+
private autoTranslateError: string | null = null;
|
|
196
|
+
|
|
197
|
+
private translationCache = new Map<string, string>();
|
|
198
|
+
|
|
132
199
|
// NodeEditor state - handles both node and action editing
|
|
133
200
|
@state()
|
|
134
201
|
private editingNode: Node | null = null;
|
|
@@ -166,6 +233,27 @@ export class Editor extends RapidElement {
|
|
|
166
233
|
|
|
167
234
|
private canvasMouseDown = false;
|
|
168
235
|
|
|
236
|
+
// Default languages if not specified in flow definition
|
|
237
|
+
private readonly DEFAULT_LANGUAGES = [
|
|
238
|
+
{ code: 'eng', name: 'English' },
|
|
239
|
+
{ code: 'fra', name: 'French' },
|
|
240
|
+
{ code: 'esp', name: 'Spanish' }
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
private getAvailableLanguages(): Array<{ code: string; name: string }> {
|
|
244
|
+
// Use languages from flow definition if available, otherwise use defaults
|
|
245
|
+
if (
|
|
246
|
+
this.definition?._ui?.languages &&
|
|
247
|
+
this.definition._ui.languages.length > 0
|
|
248
|
+
) {
|
|
249
|
+
return this.definition._ui.languages.map((lang: any) => ({
|
|
250
|
+
code: typeof lang === 'string' ? lang : lang.iso || lang.code,
|
|
251
|
+
name: typeof lang === 'string' ? lang : lang.name
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
return this.DEFAULT_LANGUAGES;
|
|
255
|
+
}
|
|
256
|
+
|
|
169
257
|
// Bound event handlers to maintain proper 'this' context
|
|
170
258
|
private boundMouseMove = this.handleMouseMove.bind(this);
|
|
171
259
|
private boundMouseUp = this.handleMouseUp.bind(this);
|
|
@@ -316,6 +404,185 @@ export class Editor extends RapidElement {
|
|
|
316
404
|
.jtk-floating-endpoint {
|
|
317
405
|
pointer-events: none;
|
|
318
406
|
}
|
|
407
|
+
|
|
408
|
+
.localization-window-content {
|
|
409
|
+
display: flex;
|
|
410
|
+
flex-direction: column;
|
|
411
|
+
gap: 16px;
|
|
412
|
+
height: 100%;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.localization-header {
|
|
416
|
+
font-size: 13px;
|
|
417
|
+
color: #4b5563;
|
|
418
|
+
line-height: 1.4;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.localization-language-select {
|
|
422
|
+
--color-widget-border: #d1d5db;
|
|
423
|
+
--color-widget-background: #fff;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.localization-language-row {
|
|
427
|
+
display: flex;
|
|
428
|
+
align-items: flex-end;
|
|
429
|
+
gap: 12px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.localization-language-row temba-select {
|
|
433
|
+
flex: 1;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
.localization-progress {
|
|
437
|
+
margin-top: auto;
|
|
438
|
+
display: flex;
|
|
439
|
+
flex-direction: column;
|
|
440
|
+
gap: 8px;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.localization-progress-bar-row {
|
|
444
|
+
display: flex;
|
|
445
|
+
align-items: center;
|
|
446
|
+
gap: 8px;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.localization-progress-trigger {
|
|
450
|
+
flex: 1;
|
|
451
|
+
border-radius: 6px;
|
|
452
|
+
cursor: pointer;
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.localization-progress-trigger:focus-visible {
|
|
458
|
+
outline: 2px solid #94a3b8;
|
|
459
|
+
outline-offset: 2px;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.localization-progress-trigger temba-progress {
|
|
463
|
+
flex: 1;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.localization-progress h5 {
|
|
467
|
+
margin: 0;
|
|
468
|
+
font-size: 13px;
|
|
469
|
+
font-weight: 600;
|
|
470
|
+
color: #374151;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.localization-progress-summary {
|
|
474
|
+
font-size: 12px;
|
|
475
|
+
color: #6b7280;
|
|
476
|
+
display: flex;
|
|
477
|
+
align-items: center;
|
|
478
|
+
gap: 6px;
|
|
479
|
+
min-height: 20px;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.translation-settings-toggle {
|
|
483
|
+
display: inline-flex;
|
|
484
|
+
align-items: center;
|
|
485
|
+
gap: 6px;
|
|
486
|
+
background: transparent;
|
|
487
|
+
border: none;
|
|
488
|
+
color: #6b7280;
|
|
489
|
+
font-size: 12px;
|
|
490
|
+
font-weight: 600;
|
|
491
|
+
cursor: pointer;
|
|
492
|
+
padding: 4px;
|
|
493
|
+
border-radius: 4px;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
.translation-settings-label {
|
|
497
|
+
font-size: 12px;
|
|
498
|
+
color: #6b7280;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.translation-settings-toggle:focus-visible {
|
|
502
|
+
outline: 2px solid #94a3b8;
|
|
503
|
+
outline-offset: 2px;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.translation-settings-arrow {
|
|
507
|
+
width: 8px;
|
|
508
|
+
height: 8px;
|
|
509
|
+
border-right: 2px solid currentColor;
|
|
510
|
+
border-bottom: 2px solid currentColor;
|
|
511
|
+
transform: rotate(-45deg);
|
|
512
|
+
transition: transform 0.2s ease;
|
|
513
|
+
margin-left: 2px;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.translation-settings-arrow.expanded {
|
|
517
|
+
transform: rotate(45deg);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.translation-settings {
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.translation-settings-row {
|
|
524
|
+
display: flex;
|
|
525
|
+
align-items: center;
|
|
526
|
+
justify-content: space-between;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.translation-settings-row temba-checkbox {
|
|
530
|
+
width: 100%;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.auto-translate-button {
|
|
534
|
+
background: var(--color-primary-dark);
|
|
535
|
+
border: none;
|
|
536
|
+
color: #fff;
|
|
537
|
+
padding: 10px 12px;
|
|
538
|
+
border-radius: var(--curvature);
|
|
539
|
+
font-size: 12px;
|
|
540
|
+
font-weight: 600;
|
|
541
|
+
cursor: pointer;
|
|
542
|
+
transition: opacity 0.2s ease;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
.auto-translate-button[disabled] {
|
|
546
|
+
opacity: 0.5;
|
|
547
|
+
cursor: not-allowed;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
.auto-translate-error {
|
|
551
|
+
font-size: 12px;
|
|
552
|
+
color: #b91c1c;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.auto-translate-dialog-content {
|
|
556
|
+
padding: 20px;
|
|
557
|
+
display: flex;
|
|
558
|
+
flex-direction: column;
|
|
559
|
+
gap: 12px;
|
|
560
|
+
font-size: 14px;
|
|
561
|
+
color: #374151;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.auto-translate-dialog-content p {
|
|
565
|
+
margin: 0;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.auto-translate-loading {
|
|
569
|
+
display: flex;
|
|
570
|
+
align-items: center;
|
|
571
|
+
gap: 8px;
|
|
572
|
+
font-size: 13px;
|
|
573
|
+
color: #6b7280;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.auto-translate-empty {
|
|
577
|
+
font-size: 13px;
|
|
578
|
+
color: #6b7280;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.localization-empty {
|
|
582
|
+
font-size: 13px;
|
|
583
|
+
color: #9ca3af;
|
|
584
|
+
white-space: nowrap;
|
|
585
|
+
}
|
|
319
586
|
`;
|
|
320
587
|
}
|
|
321
588
|
|
|
@@ -389,6 +656,24 @@ export class Editor extends RapidElement {
|
|
|
389
656
|
|
|
390
657
|
if (changes.has('definition')) {
|
|
391
658
|
this.updateCanvasSize();
|
|
659
|
+
|
|
660
|
+
// Set flowType from the loaded definition
|
|
661
|
+
if (this.definition?.type) {
|
|
662
|
+
this.flowType = this.getFlowTypeFromDefinition(this.definition.type);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const filters = this.definition?._ui?.translation_filters || {
|
|
666
|
+
categories: false
|
|
667
|
+
};
|
|
668
|
+
const normalizedFilters = {
|
|
669
|
+
categories: !!filters.categories
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
if (this.translationFilters.categories !== normalizedFilters.categories) {
|
|
673
|
+
this.translationFilters = normalizedFilters;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
this.translationCache.clear();
|
|
392
677
|
}
|
|
393
678
|
|
|
394
679
|
if (changes.has('dirtyDate')) {
|
|
@@ -396,6 +681,29 @@ export class Editor extends RapidElement {
|
|
|
396
681
|
this.debouncedSave();
|
|
397
682
|
}
|
|
398
683
|
}
|
|
684
|
+
|
|
685
|
+
if (changes.has('languageCode')) {
|
|
686
|
+
this.translationCache.clear();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Map FlowDefinition type to Editor flowType
|
|
692
|
+
* FlowDefinition uses: 'messaging', 'messaging_background', 'messaging_offline', 'voice'
|
|
693
|
+
* Editor uses: 'message', 'voice', 'background'
|
|
694
|
+
*/
|
|
695
|
+
private getFlowTypeFromDefinition(definitionType: string): string {
|
|
696
|
+
if (definitionType === 'voice') {
|
|
697
|
+
return 'voice';
|
|
698
|
+
} else if (
|
|
699
|
+
definitionType === 'messaging_background' ||
|
|
700
|
+
definitionType === 'messaging_offline'
|
|
701
|
+
) {
|
|
702
|
+
return 'background';
|
|
703
|
+
} else {
|
|
704
|
+
// 'messaging' or any other messaging type defaults to 'message'
|
|
705
|
+
return 'message';
|
|
706
|
+
}
|
|
399
707
|
}
|
|
400
708
|
|
|
401
709
|
private debouncedSave(): void {
|
|
@@ -434,6 +742,17 @@ export class Editor extends RapidElement {
|
|
|
434
742
|
getStore().getState().setDirtyDate(null);
|
|
435
743
|
}
|
|
436
744
|
|
|
745
|
+
private handleLanguageChange(languageCode: string): void {
|
|
746
|
+
zustand.getState().setLanguageCode(languageCode);
|
|
747
|
+
|
|
748
|
+
// Repaint connections after language change since node sizes can change
|
|
749
|
+
if (this.plumber) {
|
|
750
|
+
requestAnimationFrame(() => {
|
|
751
|
+
this.plumber.repaintEverything();
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
437
756
|
disconnectedCallback(): void {
|
|
438
757
|
super.disconnectedCallback();
|
|
439
758
|
if (this.saveTimer !== null) {
|
|
@@ -1699,6 +2018,719 @@ export class Editor extends RapidElement {
|
|
|
1699
2018
|
}
|
|
1700
2019
|
}
|
|
1701
2020
|
|
|
2021
|
+
private getLocalizationLanguages(): Array<{ code: string; name: string }> {
|
|
2022
|
+
if (!this.definition) {
|
|
2023
|
+
return [];
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
const baseLanguage = this.definition.language;
|
|
2027
|
+
return this.getAvailableLanguages().filter(
|
|
2028
|
+
(lang) => lang.code !== baseLanguage
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
private getLocalizationProgress(languageCode: string): {
|
|
2033
|
+
total: number;
|
|
2034
|
+
localized: number;
|
|
2035
|
+
} {
|
|
2036
|
+
if (
|
|
2037
|
+
!this.definition ||
|
|
2038
|
+
!languageCode ||
|
|
2039
|
+
languageCode === this.definition.language
|
|
2040
|
+
) {
|
|
2041
|
+
return { total: 0, localized: 0 };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
const bundles = this.buildTranslationBundles(
|
|
2045
|
+
this.translationFilters.categories,
|
|
2046
|
+
languageCode
|
|
2047
|
+
);
|
|
2048
|
+
return this.getTranslationCounts(bundles);
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
private getLanguageLocalization(languageCode: string): Record<string, any> {
|
|
2052
|
+
if (!this.definition?.localization) {
|
|
2053
|
+
return {};
|
|
2054
|
+
}
|
|
2055
|
+
return this.definition.localization[languageCode] || {};
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
private buildTranslationBundles(
|
|
2059
|
+
includeCategories: boolean,
|
|
2060
|
+
languageCode: string = this.languageCode
|
|
2061
|
+
): TranslationBundle[] {
|
|
2062
|
+
if (
|
|
2063
|
+
!this.definition ||
|
|
2064
|
+
!languageCode ||
|
|
2065
|
+
languageCode === this.definition.language
|
|
2066
|
+
) {
|
|
2067
|
+
return [];
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
const languageLocalization = this.getLanguageLocalization(languageCode);
|
|
2071
|
+
const bundles: TranslationBundle[] = [];
|
|
2072
|
+
|
|
2073
|
+
this.definition.nodes.forEach((node) => {
|
|
2074
|
+
node.actions.forEach((action) => {
|
|
2075
|
+
const config = ACTION_CONFIG[action.type];
|
|
2076
|
+
if (!config?.localizable || config.localizable.length === 0) {
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// For send_msg actions, only count 'text' for progress tracking
|
|
2081
|
+
// (quick_replies and attachments are still localizable but don't count toward progress)
|
|
2082
|
+
const localizableKeys =
|
|
2083
|
+
action.type === 'send_msg'
|
|
2084
|
+
? config.localizable.filter((key) => key === 'text')
|
|
2085
|
+
: config.localizable;
|
|
2086
|
+
|
|
2087
|
+
const translations = this.findTranslations(
|
|
2088
|
+
'property',
|
|
2089
|
+
action.uuid,
|
|
2090
|
+
localizableKeys,
|
|
2091
|
+
action,
|
|
2092
|
+
languageLocalization
|
|
2093
|
+
);
|
|
2094
|
+
|
|
2095
|
+
if (translations.length > 0) {
|
|
2096
|
+
bundles.push({
|
|
2097
|
+
nodeUuid: node.uuid,
|
|
2098
|
+
actionUuid: action.uuid,
|
|
2099
|
+
translations
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
|
|
2104
|
+
if (!includeCategories) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const nodeUI = this.definition._ui?.nodes?.[node.uuid];
|
|
2109
|
+
const nodeType = nodeUI?.type;
|
|
2110
|
+
if (!nodeType) {
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
const nodeConfig = NODE_CONFIG[nodeType];
|
|
2115
|
+
if (
|
|
2116
|
+
nodeConfig?.localizable === 'categories' &&
|
|
2117
|
+
node.router?.categories?.length
|
|
2118
|
+
) {
|
|
2119
|
+
const categoryTranslations = node.router.categories.flatMap(
|
|
2120
|
+
(category) =>
|
|
2121
|
+
this.findTranslations(
|
|
2122
|
+
'category',
|
|
2123
|
+
category.uuid,
|
|
2124
|
+
['name'],
|
|
2125
|
+
category,
|
|
2126
|
+
languageLocalization
|
|
2127
|
+
)
|
|
2128
|
+
);
|
|
2129
|
+
|
|
2130
|
+
if (categoryTranslations.length > 0) {
|
|
2131
|
+
bundles.push({
|
|
2132
|
+
nodeUuid: node.uuid,
|
|
2133
|
+
translations: categoryTranslations
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
|
|
2139
|
+
return bundles;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
private findTranslations(
|
|
2143
|
+
type: TranslationType,
|
|
2144
|
+
uuid: string,
|
|
2145
|
+
localizeableKeys: string[],
|
|
2146
|
+
source: any,
|
|
2147
|
+
localization: Record<string, any>
|
|
2148
|
+
): TranslationEntry[] {
|
|
2149
|
+
const translations: TranslationEntry[] = [];
|
|
2150
|
+
|
|
2151
|
+
localizeableKeys.forEach((attribute) => {
|
|
2152
|
+
if (attribute === 'quick_replies') {
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
const pathSegments = attribute.split('.');
|
|
2157
|
+
let from: any = source;
|
|
2158
|
+
let to: any = [];
|
|
2159
|
+
|
|
2160
|
+
while (pathSegments.length > 0 && from) {
|
|
2161
|
+
if (from.uuid) {
|
|
2162
|
+
to = localization[from.uuid];
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
const path = pathSegments.shift();
|
|
2166
|
+
if (!path) {
|
|
2167
|
+
break;
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
if (to) {
|
|
2171
|
+
to = to[path];
|
|
2172
|
+
}
|
|
2173
|
+
from = from[path];
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
if (!from) {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
const fromValue = this.formatTranslationValue(from);
|
|
2181
|
+
if (!fromValue) {
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
const toValue = to ? this.formatTranslationValue(to) : null;
|
|
2186
|
+
|
|
2187
|
+
translations.push({
|
|
2188
|
+
uuid,
|
|
2189
|
+
type,
|
|
2190
|
+
attribute,
|
|
2191
|
+
from: fromValue,
|
|
2192
|
+
to: toValue
|
|
2193
|
+
});
|
|
2194
|
+
});
|
|
2195
|
+
|
|
2196
|
+
return translations;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
private formatTranslationValue(value: any): string | null {
|
|
2200
|
+
if (value === null || value === undefined) {
|
|
2201
|
+
return null;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
if (Array.isArray(value)) {
|
|
2205
|
+
const normalized = value
|
|
2206
|
+
.map((entry) => this.formatTranslationValue(entry))
|
|
2207
|
+
.filter((entry) => !!entry) as string[];
|
|
2208
|
+
return normalized.length > 0 ? normalized.join(', ') : null;
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
if (typeof value === 'object') {
|
|
2212
|
+
if ('name' in value && value.name) {
|
|
2213
|
+
return String(value.name);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
if ('arguments' in value && Array.isArray(value.arguments)) {
|
|
2217
|
+
return value.arguments.join(' ');
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
return null;
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
if (typeof value === 'number') {
|
|
2224
|
+
return value.toString();
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
if (typeof value === 'string') {
|
|
2228
|
+
const trimmed = value.trim();
|
|
2229
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
return null;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
private getTranslationCounts(bundles: TranslationBundle[]): {
|
|
2236
|
+
total: number;
|
|
2237
|
+
localized: number;
|
|
2238
|
+
} {
|
|
2239
|
+
return bundles.reduce(
|
|
2240
|
+
(counts, bundle) => {
|
|
2241
|
+
bundle.translations.forEach((translation) => {
|
|
2242
|
+
counts.total += 1;
|
|
2243
|
+
if (translation.to && translation.to.trim().length > 0) {
|
|
2244
|
+
counts.localized += 1;
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
return counts;
|
|
2248
|
+
},
|
|
2249
|
+
{ total: 0, localized: 0 }
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
private handleLocalizationTabClick(): void {
|
|
2254
|
+
const languages = this.getLocalizationLanguages();
|
|
2255
|
+
if (!languages.length) {
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
this.localizationWindowHidden = false;
|
|
2260
|
+
|
|
2261
|
+
const alreadySelected = languages.some(
|
|
2262
|
+
(lang) => lang.code === this.languageCode
|
|
2263
|
+
);
|
|
2264
|
+
|
|
2265
|
+
if (!alreadySelected) {
|
|
2266
|
+
this.handleLanguageChange(languages[0].code);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
private handleLocalizationLanguageSelect(languageCode: string): void {
|
|
2271
|
+
if (languageCode === this.languageCode) {
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
this.handleLanguageChange(languageCode);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
private handleLocalizationLanguageSelectChange(event: CustomEvent): void {
|
|
2278
|
+
const select = event.target as any;
|
|
2279
|
+
const nextValue = select?.values?.[0]?.value;
|
|
2280
|
+
if (nextValue) {
|
|
2281
|
+
this.handleLocalizationLanguageSelect(nextValue);
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
private handleLocalizationWindowClosed(): void {
|
|
2286
|
+
this.localizationWindowHidden = true;
|
|
2287
|
+
|
|
2288
|
+
const baseLanguage = this.definition?.language;
|
|
2289
|
+
if (baseLanguage && this.languageCode !== baseLanguage) {
|
|
2290
|
+
this.handleLanguageChange(baseLanguage);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
private toggleTranslationSettings(): void {
|
|
2295
|
+
this.translationSettingsExpanded = !this.translationSettingsExpanded;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
private handleLocalizationProgressToggleClick(event: MouseEvent): void {
|
|
2299
|
+
const target = event.target as HTMLElement;
|
|
2300
|
+
if (target.closest('.translation-settings-toggle')) {
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
this.toggleTranslationSettings();
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
private handleLocalizationProgressToggleKeydown(event: KeyboardEvent): void {
|
|
2307
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
2308
|
+
event.preventDefault();
|
|
2309
|
+
this.toggleTranslationSettings();
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
private handleIncludeCategoriesChange(event: Event): void {
|
|
2314
|
+
const checkbox = event.target as Checkbox;
|
|
2315
|
+
const categories = checkbox?.checked ?? false;
|
|
2316
|
+
this.translationFilters = { categories };
|
|
2317
|
+
getStore()?.getState().setTranslationFilters({ categories });
|
|
2318
|
+
this.requestUpdate();
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
private async handleAutoTranslateClick(event: Event): Promise<void> {
|
|
2322
|
+
event.preventDefault();
|
|
2323
|
+
event.stopPropagation();
|
|
2324
|
+
|
|
2325
|
+
if (this.autoTranslating) {
|
|
2326
|
+
this.autoTranslating = false;
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
this.autoTranslateDialogOpen = true;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
private handleAutoTranslateDialogButton(event: CustomEvent): void {
|
|
2334
|
+
const button = event.detail?.button;
|
|
2335
|
+
if (!button) {
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
if (button.name === 'Translate') {
|
|
2340
|
+
if (!this.autoTranslateModel) {
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
this.autoTranslateDialogOpen = false;
|
|
2344
|
+
this.autoTranslateError = null;
|
|
2345
|
+
this.autoTranslating = true;
|
|
2346
|
+
this.runAutoTranslation().catch((error) => {
|
|
2347
|
+
console.error('Auto translation failed', error);
|
|
2348
|
+
this.autoTranslateError = 'Auto translation failed. Please try again.';
|
|
2349
|
+
this.autoTranslating = false;
|
|
2350
|
+
});
|
|
2351
|
+
} else if (button.name === 'Cancel' || button.name === 'Close') {
|
|
2352
|
+
this.autoTranslateDialogOpen = false;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
private handleAutoTranslateModelChange(event: Event): void {
|
|
2357
|
+
const select = event.target as any;
|
|
2358
|
+
const nextModel = select?.values?.[0] || null;
|
|
2359
|
+
this.autoTranslateModel = nextModel;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
private shouldTranslateValue(text: string): boolean {
|
|
2363
|
+
if (!text) {
|
|
2364
|
+
return false;
|
|
2365
|
+
}
|
|
2366
|
+
const trimmed = text.trim();
|
|
2367
|
+
if (trimmed.length <= 1) {
|
|
2368
|
+
return false;
|
|
2369
|
+
}
|
|
2370
|
+
if (/^\d+$/.test(trimmed)) {
|
|
2371
|
+
return false;
|
|
2372
|
+
}
|
|
2373
|
+
return true;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
private async requestAutoTranslation(text: string): Promise<string | null> {
|
|
2377
|
+
if (!this.autoTranslateModel || !this.definition) {
|
|
2378
|
+
return null;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const payload = {
|
|
2382
|
+
text,
|
|
2383
|
+
lang: {
|
|
2384
|
+
from: this.definition.language,
|
|
2385
|
+
to: this.languageCode
|
|
2386
|
+
}
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
const response = await postJSON(
|
|
2390
|
+
`/llm/translate/${this.autoTranslateModel.uuid}/`,
|
|
2391
|
+
payload
|
|
2392
|
+
);
|
|
2393
|
+
|
|
2394
|
+
if (response?.status === 200) {
|
|
2395
|
+
const result = response.json?.result || response.json?.text;
|
|
2396
|
+
return result ? String(result) : null;
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
throw new Error('Auto translation request failed');
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
private applyLocalizationUpdates(
|
|
2403
|
+
updates: LocalizationUpdate[],
|
|
2404
|
+
autoTranslated = false
|
|
2405
|
+
): void {
|
|
2406
|
+
if (!updates.length || !this.definition) {
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
const store = getStore();
|
|
2411
|
+
if (!store) {
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
updates.forEach(({ uuid, translations }) => {
|
|
2416
|
+
const normalized = Object.entries(translations).reduce(
|
|
2417
|
+
(acc, [key, value]) => {
|
|
2418
|
+
if (!value) {
|
|
2419
|
+
return acc;
|
|
2420
|
+
}
|
|
2421
|
+
acc[key] = Array.isArray(value) ? value : [value];
|
|
2422
|
+
return acc;
|
|
2423
|
+
},
|
|
2424
|
+
{} as Record<string, any>
|
|
2425
|
+
);
|
|
2426
|
+
|
|
2427
|
+
const existing =
|
|
2428
|
+
this.definition.localization?.[this.languageCode]?.[uuid] || {};
|
|
2429
|
+
const merged = { ...existing, ...normalized };
|
|
2430
|
+
|
|
2431
|
+
store.getState().updateLocalization(this.languageCode, uuid, merged);
|
|
2432
|
+
|
|
2433
|
+
if (autoTranslated) {
|
|
2434
|
+
zustand
|
|
2435
|
+
.getState()
|
|
2436
|
+
.markAutoTranslated(
|
|
2437
|
+
this.languageCode,
|
|
2438
|
+
uuid,
|
|
2439
|
+
Object.keys(translations)
|
|
2440
|
+
);
|
|
2441
|
+
}
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
private async runAutoTranslation(): Promise<void> {
|
|
2446
|
+
if (
|
|
2447
|
+
!this.definition ||
|
|
2448
|
+
this.languageCode === this.definition.language ||
|
|
2449
|
+
!this.autoTranslateModel
|
|
2450
|
+
) {
|
|
2451
|
+
this.autoTranslating = false;
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
const bundles = this.buildTranslationBundles(
|
|
2456
|
+
this.translationFilters.categories
|
|
2457
|
+
);
|
|
2458
|
+
|
|
2459
|
+
for (const bundle of bundles) {
|
|
2460
|
+
if (!this.autoTranslating) {
|
|
2461
|
+
break;
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
const untranslated = bundle.translations.filter(
|
|
2465
|
+
(translation) => !translation.to || translation.to.trim().length === 0
|
|
2466
|
+
);
|
|
2467
|
+
|
|
2468
|
+
if (untranslated.length === 0) {
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
const updates: LocalizationUpdate[] = [];
|
|
2473
|
+
|
|
2474
|
+
for (const translation of untranslated) {
|
|
2475
|
+
if (!this.autoTranslating) {
|
|
2476
|
+
break;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
if (!this.shouldTranslateValue(translation.from)) {
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
const cached = this.translationCache.get(translation.from);
|
|
2484
|
+
if (cached) {
|
|
2485
|
+
updates.push({
|
|
2486
|
+
uuid: translation.uuid,
|
|
2487
|
+
translations: { [translation.attribute]: cached }
|
|
2488
|
+
});
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
try {
|
|
2493
|
+
const result = await this.requestAutoTranslation(translation.from);
|
|
2494
|
+
if (result) {
|
|
2495
|
+
this.translationCache.set(translation.from, result);
|
|
2496
|
+
updates.push({
|
|
2497
|
+
uuid: translation.uuid,
|
|
2498
|
+
translations: { [translation.attribute]: result }
|
|
2499
|
+
});
|
|
2500
|
+
}
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
console.error('Auto translation request failed', error);
|
|
2503
|
+
this.autoTranslateError =
|
|
2504
|
+
'Auto translation failed. Please try again.';
|
|
2505
|
+
this.autoTranslating = false;
|
|
2506
|
+
break;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
if (updates.length > 0) {
|
|
2511
|
+
this.applyLocalizationUpdates(updates, true);
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
if (!this.autoTranslating) {
|
|
2515
|
+
break;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
this.autoTranslating = false;
|
|
2520
|
+
}
|
|
2521
|
+
|
|
2522
|
+
private renderLocalizationWindow(): TemplateResult | string {
|
|
2523
|
+
const languages = this.getLocalizationLanguages();
|
|
2524
|
+
if (!languages.length) {
|
|
2525
|
+
return html``;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
const baseLanguage = this.definition?.language;
|
|
2529
|
+
const availableLanguages = this.getAvailableLanguages();
|
|
2530
|
+
const baseName =
|
|
2531
|
+
availableLanguages.find((lang) => lang.code === baseLanguage)?.name ||
|
|
2532
|
+
'Base Language';
|
|
2533
|
+
|
|
2534
|
+
const activeLanguageCode = languages.some(
|
|
2535
|
+
(lang) => lang.code === this.languageCode
|
|
2536
|
+
)
|
|
2537
|
+
? this.languageCode
|
|
2538
|
+
: languages[0]?.code;
|
|
2539
|
+
const activeLanguage = activeLanguageCode
|
|
2540
|
+
? languages.find((lang) => lang.code === activeLanguageCode)
|
|
2541
|
+
: null;
|
|
2542
|
+
const progress = this.getLocalizationProgress(activeLanguageCode || '');
|
|
2543
|
+
const includeCategories = this.translationFilters.categories;
|
|
2544
|
+
const settingsPanelId = 'translation-settings-panel';
|
|
2545
|
+
const remainingTranslations = Math.max(
|
|
2546
|
+
progress.total - progress.localized,
|
|
2547
|
+
0
|
|
2548
|
+
);
|
|
2549
|
+
const hasTranslations = progress.total > 0;
|
|
2550
|
+
const hasPendingTranslations = remainingTranslations > 0;
|
|
2551
|
+
const autoTranslateButtonLabel = this.autoTranslating
|
|
2552
|
+
? 'Stop Auto Translate'
|
|
2553
|
+
: 'Auto Translate';
|
|
2554
|
+
const autoTranslateButtonDisabled =
|
|
2555
|
+
!this.autoTranslating && !hasTranslations;
|
|
2556
|
+
|
|
2557
|
+
return html`
|
|
2558
|
+
<temba-floating-window
|
|
2559
|
+
id="localization-window"
|
|
2560
|
+
header="Translations"
|
|
2561
|
+
.width=${360}
|
|
2562
|
+
.maxHeight=${600}
|
|
2563
|
+
.top=${20}
|
|
2564
|
+
color="#6b7280"
|
|
2565
|
+
.hidden=${this.localizationWindowHidden}
|
|
2566
|
+
@temba-dialog-hidden=${this.handleLocalizationWindowClosed}
|
|
2567
|
+
>
|
|
2568
|
+
<div class="localization-window-content">
|
|
2569
|
+
<div class="localization-header">
|
|
2570
|
+
Translate from <strong>${baseName}</strong> to the languages below.
|
|
2571
|
+
Closing this window returns you to editing in ${baseName}.
|
|
2572
|
+
</div>
|
|
2573
|
+
<div class="localization-language-row">
|
|
2574
|
+
<temba-select
|
|
2575
|
+
flavor="small"
|
|
2576
|
+
class="localization-language-select"
|
|
2577
|
+
.values=${activeLanguage
|
|
2578
|
+
? [{ name: activeLanguage.name, value: activeLanguage.code }]
|
|
2579
|
+
: []}
|
|
2580
|
+
@change=${this.handleLocalizationLanguageSelectChange}
|
|
2581
|
+
>
|
|
2582
|
+
${languages.map(
|
|
2583
|
+
(lang) => html`<temba-option
|
|
2584
|
+
value="${lang.code}"
|
|
2585
|
+
name="${lang.name}"
|
|
2586
|
+
></temba-option>`
|
|
2587
|
+
)}
|
|
2588
|
+
</temba-select>
|
|
2589
|
+
<button
|
|
2590
|
+
class="auto-translate-button"
|
|
2591
|
+
type="button"
|
|
2592
|
+
?disabled=${autoTranslateButtonDisabled}
|
|
2593
|
+
@click=${this.handleAutoTranslateClick}
|
|
2594
|
+
>
|
|
2595
|
+
${autoTranslateButtonLabel}
|
|
2596
|
+
</button>
|
|
2597
|
+
</div>
|
|
2598
|
+
<div class="localization-progress">
|
|
2599
|
+
<div class="localization-progress-summary">
|
|
2600
|
+
${this.autoTranslating
|
|
2601
|
+
? html`<temba-loading units="3" size="8"></temba-loading>
|
|
2602
|
+
<span>Auto translating remaining text…</span>`
|
|
2603
|
+
: !hasTranslations
|
|
2604
|
+
? html`<span>
|
|
2605
|
+
Add content or enable more options to start translating.
|
|
2606
|
+
</span>`
|
|
2607
|
+
: hasPendingTranslations
|
|
2608
|
+
? html`<span>
|
|
2609
|
+
${progress.localized} of ${progress.total} items translated
|
|
2610
|
+
</span>`
|
|
2611
|
+
: html`<span>All items are translated.</span>`}
|
|
2612
|
+
</div>
|
|
2613
|
+
${this.autoTranslateError
|
|
2614
|
+
? html`<div class="auto-translate-error">
|
|
2615
|
+
${this.autoTranslateError}
|
|
2616
|
+
</div>`
|
|
2617
|
+
: ''}
|
|
2618
|
+
<div class="localization-progress-bar-row">
|
|
2619
|
+
<div
|
|
2620
|
+
class="localization-progress-trigger"
|
|
2621
|
+
role="button"
|
|
2622
|
+
tabindex="0"
|
|
2623
|
+
aria-expanded="${this.translationSettingsExpanded}"
|
|
2624
|
+
aria-controls="${settingsPanelId}"
|
|
2625
|
+
@click=${this.handleLocalizationProgressToggleClick}
|
|
2626
|
+
@keydown=${this.handleLocalizationProgressToggleKeydown}
|
|
2627
|
+
>
|
|
2628
|
+
<temba-progress
|
|
2629
|
+
.current=${progress.localized}
|
|
2630
|
+
.total=${Math.max(progress.total, 1)}
|
|
2631
|
+
.animated=${false}
|
|
2632
|
+
></temba-progress>
|
|
2633
|
+
</div>
|
|
2634
|
+
<button
|
|
2635
|
+
class="translation-settings-toggle"
|
|
2636
|
+
type="button"
|
|
2637
|
+
@click=${this.toggleTranslationSettings}
|
|
2638
|
+
aria-expanded="${this.translationSettingsExpanded}"
|
|
2639
|
+
aria-controls="${settingsPanelId}"
|
|
2640
|
+
>
|
|
2641
|
+
<span
|
|
2642
|
+
class="translation-settings-arrow ${this
|
|
2643
|
+
.translationSettingsExpanded
|
|
2644
|
+
? 'expanded'
|
|
2645
|
+
: ''}"
|
|
2646
|
+
></span>
|
|
2647
|
+
</button>
|
|
2648
|
+
</div>
|
|
2649
|
+
${this.translationSettingsExpanded
|
|
2650
|
+
? html`<div id="${settingsPanelId}" class="translation-settings">
|
|
2651
|
+
<div class="translation-settings-row">
|
|
2652
|
+
<temba-checkbox
|
|
2653
|
+
name="include-categories"
|
|
2654
|
+
label="Include categories"
|
|
2655
|
+
?checked=${includeCategories}
|
|
2656
|
+
style="--checkbox-padding:5px; border-radius:var(--curvature);"
|
|
2657
|
+
@change=${this.handleIncludeCategoriesChange}
|
|
2658
|
+
></temba-checkbox>
|
|
2659
|
+
</div>
|
|
2660
|
+
</div>`
|
|
2661
|
+
: ''}
|
|
2662
|
+
</div>
|
|
2663
|
+
</div>
|
|
2664
|
+
</temba-floating-window>
|
|
2665
|
+
`;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
private renderAutoTranslateDialog(): TemplateResult | string {
|
|
2669
|
+
if (!this.autoTranslateDialogOpen) {
|
|
2670
|
+
return html``;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
const selectedModel = this.autoTranslateModel
|
|
2674
|
+
? [this.autoTranslateModel]
|
|
2675
|
+
: [];
|
|
2676
|
+
const disableTranslate = !this.autoTranslateModel;
|
|
2677
|
+
|
|
2678
|
+
return html`
|
|
2679
|
+
<temba-dialog
|
|
2680
|
+
header="Auto translate"
|
|
2681
|
+
.open=${this.autoTranslateDialogOpen}
|
|
2682
|
+
primaryButtonName="Translate"
|
|
2683
|
+
cancelButtonName="Cancel"
|
|
2684
|
+
size="small"
|
|
2685
|
+
.disabled=${disableTranslate}
|
|
2686
|
+
@temba-button-clicked=${this.handleAutoTranslateDialogButton}
|
|
2687
|
+
>
|
|
2688
|
+
<div class="auto-translate-dialog-content">
|
|
2689
|
+
<p>
|
|
2690
|
+
We'll send any untranslated text to the selected AI model and save
|
|
2691
|
+
the responses automatically.
|
|
2692
|
+
</p>
|
|
2693
|
+
<div class="auto-translate-models">
|
|
2694
|
+
<temba-select
|
|
2695
|
+
class="auto-translate-model-select"
|
|
2696
|
+
endpoint="${AUTO_TRANSLATE_MODELS_ENDPOINT}"
|
|
2697
|
+
.valueKey=${'uuid'}
|
|
2698
|
+
.values=${selectedModel}
|
|
2699
|
+
?searchable=${true}
|
|
2700
|
+
?clearable=${true}
|
|
2701
|
+
placeholder="Select an AI model"
|
|
2702
|
+
@change=${this.handleAutoTranslateModelChange}
|
|
2703
|
+
></temba-select>
|
|
2704
|
+
</div>
|
|
2705
|
+
<p>Only text without translations will be sent.</p>
|
|
2706
|
+
${this.autoTranslateError
|
|
2707
|
+
? html`<div class="auto-translate-error">
|
|
2708
|
+
${this.autoTranslateError}
|
|
2709
|
+
</div>`
|
|
2710
|
+
: ''}
|
|
2711
|
+
</div>
|
|
2712
|
+
</temba-dialog>
|
|
2713
|
+
`;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
private renderLocalizationTab(): TemplateResult | string {
|
|
2717
|
+
const languages = this.getLocalizationLanguages();
|
|
2718
|
+
if (!languages.length) {
|
|
2719
|
+
return html``;
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
return html`
|
|
2723
|
+
<temba-floating-tab
|
|
2724
|
+
id="localization-tab"
|
|
2725
|
+
icon="language"
|
|
2726
|
+
label="Translate Flow"
|
|
2727
|
+
color="#6b7280"
|
|
2728
|
+
.hidden=${!this.localizationWindowHidden}
|
|
2729
|
+
@temba-button-clicked=${this.handleLocalizationTabClick}
|
|
2730
|
+
></temba-floating-tab>
|
|
2731
|
+
`;
|
|
2732
|
+
}
|
|
2733
|
+
|
|
1702
2734
|
public render(): TemplateResult {
|
|
1703
2735
|
// we have to embed our own style since we are in light DOM
|
|
1704
2736
|
const style = html`<style>
|
|
@@ -1708,7 +2740,8 @@ export class Editor extends RapidElement {
|
|
|
1708
2740
|
|
|
1709
2741
|
const stickies = this.definition?._ui?.stickies || {};
|
|
1710
2742
|
|
|
1711
|
-
return html`${style}
|
|
2743
|
+
return html`${style} ${this.renderLocalizationWindow()}
|
|
2744
|
+
${this.renderAutoTranslateDialog()}
|
|
1712
2745
|
<div id="editor">
|
|
1713
2746
|
<div
|
|
1714
2747
|
id="grid"
|
|
@@ -1792,6 +2825,10 @@ export class Editor extends RapidElement {
|
|
|
1792
2825
|
: ''}
|
|
1793
2826
|
|
|
1794
2827
|
<temba-canvas-menu></temba-canvas-menu>
|
|
1795
|
-
<temba-node-type-selector
|
|
2828
|
+
<temba-node-type-selector
|
|
2829
|
+
.flowType=${this.flowType}
|
|
2830
|
+
.features=${this.features}
|
|
2831
|
+
></temba-node-type-selector>
|
|
2832
|
+
${this.renderLocalizationTab()} `;
|
|
1796
2833
|
}
|
|
1797
2834
|
}
|