@parhelia/localization 0.1.12907 → 0.1.12910
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/LocalizeItemCommand.d.ts +7 -1
- package/dist/LocalizeItemCommand.d.ts.map +1 -1
- package/dist/LocalizeItemCommand.js +31 -18
- package/dist/LocalizeItemDialog.d.ts.map +1 -1
- package/dist/LocalizeItemDialog.js +97 -35
- package/dist/LocalizeItemUtils.d.ts +1 -2
- package/dist/LocalizeItemUtils.d.ts.map +1 -1
- package/dist/LocalizeItemUtils.js +78 -36
- package/dist/api/discovery.d.ts +25 -0
- package/dist/api/discovery.d.ts.map +1 -1
- package/dist/api/discovery.js +106 -2
- package/dist/hooks/useTranslationWizard.d.ts.map +1 -1
- package/dist/hooks/useTranslationWizard.js +3 -3
- package/dist/index.d.ts +10 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +36 -32
- package/dist/services/translationService.d.ts +41 -10
- package/dist/services/translationService.d.ts.map +1 -1
- package/dist/services/translationService.js +48 -6
- package/dist/settings/TranslationServicesPanel.d.ts.map +1 -1
- package/dist/settings/TranslationServicesPanel.js +21 -36
- package/dist/setup/LocalizationSetupStep.d.ts.map +1 -1
- package/dist/setup/LocalizationSetupStep.js +29 -18
- package/dist/sidebar/TranslationSidebar.d.ts.map +1 -1
- package/dist/sidebar/TranslationSidebar.js +20 -10
- package/dist/steps/ItemSelectionStep.d.ts +3 -0
- package/dist/steps/ItemSelectionStep.d.ts.map +1 -0
- package/dist/steps/ItemSelectionStep.js +24 -0
- package/dist/steps/ItemSelectionTree.d.ts +13 -0
- package/dist/steps/ItemSelectionTree.d.ts.map +1 -0
- package/dist/steps/ItemSelectionTree.js +327 -0
- package/dist/steps/PromptCustomizationStep.d.ts +1 -1
- package/dist/steps/PromptCustomizationStep.d.ts.map +1 -1
- package/dist/steps/PromptCustomizationStep.js +159 -56
- package/dist/steps/ServiceLanguageSelectionStep.d.ts +6 -1
- package/dist/steps/ServiceLanguageSelectionStep.d.ts.map +1 -1
- package/dist/steps/ServiceLanguageSelectionStep.js +92 -165
- package/dist/steps/WizardStepShell.d.ts +17 -0
- package/dist/steps/WizardStepShell.d.ts.map +1 -0
- package/dist/steps/WizardStepShell.js +11 -0
- package/dist/steps/types.d.ts +17 -1
- package/dist/steps/types.d.ts.map +1 -1
- package/dist/translation-center/TranslationBatches.d.ts +2 -0
- package/dist/translation-center/TranslationBatches.d.ts.map +1 -0
- package/dist/translation-center/TranslationBatches.js +1218 -0
- package/dist/translation-center/TranslationManagement.d.ts.map +1 -1
- package/dist/translation-center/TranslationManagement.js +25 -15
- package/dist/translation-center/TranslationsTitlebar.d.ts +6 -0
- package/dist/translation-center/TranslationsTitlebar.d.ts.map +1 -0
- package/dist/translation-center/TranslationsTitlebar.js +25 -0
- package/dist/translationEvents.d.ts +6 -0
- package/dist/translationEvents.d.ts.map +1 -0
- package/dist/translationEvents.js +4 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/constants.d.ts +0 -15
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -21
- package/dist/steps/MetadataInputStep.d.ts +0 -4
- package/dist/steps/MetadataInputStep.d.ts.map +0 -1
- package/dist/steps/MetadataInputStep.js +0 -41
- package/dist/steps/SubitemDiscoveryStep.d.ts +0 -3
- package/dist/steps/SubitemDiscoveryStep.d.ts.map +0 -1
- package/dist/steps/SubitemDiscoveryStep.js +0 -313
- package/dist/steps/index.d.ts +0 -5
- package/dist/steps/index.d.ts.map +0 -1
- package/dist/steps/index.js +0 -4
- package/dist/translation-center/BatchTranslationView.d.ts +0 -8
- package/dist/translation-center/BatchTranslationView.d.ts.map +0 -1
- package/dist/translation-center/BatchTranslationView.js +0 -870
- package/dist/translation-center/RecentTranslations.d.ts +0 -2
- package/dist/translation-center/RecentTranslations.d.ts.map +0 -1
- package/dist/translation-center/RecentTranslations.js +0 -309
- package/dist/utils/createVersions.d.ts +0 -14
- package/dist/utils/createVersions.d.ts.map +0 -1
- package/dist/utils/createVersions.js +0 -26
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { ItemCollectionEditor, } from "@parhelia/core";
|
|
4
|
+
import { streamSubitemCount } from "../api/discovery";
|
|
5
|
+
const SITECORE_ROOT = "11111111-1111-1111-1111-111111111111";
|
|
6
|
+
function fullItemFromDescriptor(descriptor) {
|
|
7
|
+
return {
|
|
8
|
+
id: descriptor.id,
|
|
9
|
+
language: descriptor.language,
|
|
10
|
+
version: descriptor.version,
|
|
11
|
+
descriptor: {
|
|
12
|
+
id: descriptor.id,
|
|
13
|
+
language: descriptor.language,
|
|
14
|
+
version: descriptor.version,
|
|
15
|
+
name: descriptor.name,
|
|
16
|
+
displayName: descriptor.displayName,
|
|
17
|
+
path: descriptor.path,
|
|
18
|
+
},
|
|
19
|
+
name: descriptor.name ?? "",
|
|
20
|
+
templateId: descriptor.templateId ?? "",
|
|
21
|
+
templateName: "",
|
|
22
|
+
icon: descriptor.icon ?? "",
|
|
23
|
+
largeIcon: descriptor.icon ?? "",
|
|
24
|
+
thumbnail: "",
|
|
25
|
+
path: descriptor.path ?? "",
|
|
26
|
+
fields: [],
|
|
27
|
+
canWriteItem: false,
|
|
28
|
+
canWriteLanguage: false,
|
|
29
|
+
canWriteWorkflow: false,
|
|
30
|
+
canRename: false,
|
|
31
|
+
canDelete: false,
|
|
32
|
+
canCreate: false,
|
|
33
|
+
workflowState: "",
|
|
34
|
+
hasLock: false,
|
|
35
|
+
hasLayout: true,
|
|
36
|
+
idPath: descriptor.idPath ?? "",
|
|
37
|
+
canLock: false,
|
|
38
|
+
lockedBy: "",
|
|
39
|
+
isInheritedFromMasterLanguage: false,
|
|
40
|
+
hasChildren: descriptor.hasChildren ?? false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function fullItemToWithSubtree(item, includeSubitems) {
|
|
44
|
+
return {
|
|
45
|
+
descriptor: {
|
|
46
|
+
id: item.descriptor?.id ?? item.id,
|
|
47
|
+
language: item.descriptor?.language ?? item.language,
|
|
48
|
+
version: item.descriptor?.version ?? item.version,
|
|
49
|
+
name: item.descriptor?.name ?? item.name,
|
|
50
|
+
displayName: item.descriptor?.displayName ?? item.displayName,
|
|
51
|
+
path: item.descriptor?.path ?? item.path,
|
|
52
|
+
},
|
|
53
|
+
includeSubitems,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export function ItemSelectionTree({ data, setData, editContext, isActive = true, onSelectionValidChange, height = 360, }) {
|
|
57
|
+
const language = useMemo(() => editContext?.item?.language ??
|
|
58
|
+
editContext?.currentItemDescriptor?.language ??
|
|
59
|
+
"en", [editContext?.item?.language, editContext?.currentItemDescriptor?.language]);
|
|
60
|
+
// Seed selection: prefer previously saved tree items, then discoveredItems,
|
|
61
|
+
// then the wizard's initial `items`. Default includeSubitems = legacy flag.
|
|
62
|
+
const initialItems = useMemo(() => {
|
|
63
|
+
if (data.selectionTreeItems && data.selectionTreeItems.length > 0) {
|
|
64
|
+
return data.selectionTreeItems;
|
|
65
|
+
}
|
|
66
|
+
const seed = data.discoveredItems && data.discoveredItems.length > 0
|
|
67
|
+
? data.discoveredItems
|
|
68
|
+
: data.items;
|
|
69
|
+
return seed.map((item) => fullItemToWithSubtree(item, !!data.includeSubitems));
|
|
70
|
+
// First mount only — once the user edits, wizard data is downstream.
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
72
|
+
}, []);
|
|
73
|
+
const [items, setItems] = useState(initialItems);
|
|
74
|
+
const [selectedInTree, setSelectedInTree] = useState([]);
|
|
75
|
+
const [selectedFromList, setSelectedFromList] = useState([]);
|
|
76
|
+
// Async subitem counts keyed by item id. Filtered server-side via
|
|
77
|
+
// IItemExportFilter (see TranslationController.DiscoverItemsTree). The
|
|
78
|
+
// running count climbs as chunks arrive; `complete` flips true on the
|
|
79
|
+
// final chunk so consumers know when to drop the "still counting" hint.
|
|
80
|
+
const [subitemCounts, setSubitemCounts] = useState(() => data.subitemCounts ?? {});
|
|
81
|
+
// Mirror local subitemCounts into wizardData so later steps (e.g. the
|
|
82
|
+
// Start Translation button) can show a live total.
|
|
83
|
+
const dataRef = useRef(data);
|
|
84
|
+
dataRef.current = data;
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setData({ ...dataRef.current, subitemCounts });
|
|
87
|
+
}, [subitemCounts, setData]);
|
|
88
|
+
// Keep a cache of the richest FullItem we've ever seen for each id so we
|
|
89
|
+
// can rebuild discoveredItems without re-fetching.
|
|
90
|
+
const itemCacheRef = useRef(new Map());
|
|
91
|
+
if (itemCacheRef.current.size === 0) {
|
|
92
|
+
for (const it of data.items)
|
|
93
|
+
itemCacheRef.current.set(it.id, it);
|
|
94
|
+
for (const it of data.discoveredItems || [])
|
|
95
|
+
itemCacheRef.current.set(it.id, it);
|
|
96
|
+
}
|
|
97
|
+
// Build a combined idPath that expands every parent of the current
|
|
98
|
+
// selection (but not the items themselves). ContentTree's parser
|
|
99
|
+
// extracts each {guid} segment, so concatenating multiple paths works.
|
|
100
|
+
const expandIdPath = useMemo(() => {
|
|
101
|
+
const segments = [];
|
|
102
|
+
for (const entry of items) {
|
|
103
|
+
const cached = itemCacheRef.current.get(entry.descriptor.id);
|
|
104
|
+
const idPath = cached?.idPath;
|
|
105
|
+
if (!idPath)
|
|
106
|
+
continue;
|
|
107
|
+
const parts = idPath.split("/").filter((s) => s.length > 0);
|
|
108
|
+
// Drop the last segment (the item itself) — we only want parents.
|
|
109
|
+
parts.pop();
|
|
110
|
+
for (const part of parts)
|
|
111
|
+
segments.push(part);
|
|
112
|
+
}
|
|
113
|
+
if (segments.length === 0)
|
|
114
|
+
return undefined;
|
|
115
|
+
return "/" + segments.join("/");
|
|
116
|
+
}, [items]);
|
|
117
|
+
const commitToWizard = useCallback((nextItems) => {
|
|
118
|
+
const cache = itemCacheRef.current;
|
|
119
|
+
const discovered = nextItems.map((entry) => cache.get(entry.descriptor.id) ??
|
|
120
|
+
fullItemFromDescriptor(entry.descriptor));
|
|
121
|
+
setData({
|
|
122
|
+
...data,
|
|
123
|
+
selectionTreeItems: nextItems,
|
|
124
|
+
discoveredItems: discovered,
|
|
125
|
+
includeSubitems: nextItems.some((i) => i.includeSubitems),
|
|
126
|
+
});
|
|
127
|
+
}, [data, setData]);
|
|
128
|
+
// Commit current state on first mount so wizard data reflects the seed.
|
|
129
|
+
const didInitialCommitRef = useRef(false);
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (didInitialCommitRef.current)
|
|
132
|
+
return;
|
|
133
|
+
didInitialCommitRef.current = true;
|
|
134
|
+
commitToWizard(items);
|
|
135
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
136
|
+
}, []);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (isActive)
|
|
139
|
+
onSelectionValidChange?.(items.length > 0);
|
|
140
|
+
}, [isActive, onSelectionValidChange, items.length]);
|
|
141
|
+
// Stream subitem counts in the background for items whose
|
|
142
|
+
// includeSubitems flag is on. Filtering happens server-side (see
|
|
143
|
+
// TranslationController.DiscoverSubitemCountStream). The count grows in
|
|
144
|
+
// real time as the backend walks the subtree. Cached results survive
|
|
145
|
+
// toggle-off/on; in-flight requests are aborted if the user turns the
|
|
146
|
+
// flag back off.
|
|
147
|
+
const sessionId = editContext?.sessionId;
|
|
148
|
+
const inFlightRef = useRef(new Map());
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const inFlight = inFlightRef.current;
|
|
151
|
+
const enabledIds = new Set(items.filter((it) => it.includeSubitems).map((it) => it.descriptor.id));
|
|
152
|
+
// Cancel any streams whose items no longer have the flag on.
|
|
153
|
+
for (const [id, ctrl] of inFlight) {
|
|
154
|
+
if (!enabledIds.has(id)) {
|
|
155
|
+
ctrl.abort();
|
|
156
|
+
inFlight.delete(id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Start a stream for every newly-enabled item that doesn't already
|
|
160
|
+
// have a count or an active stream. Cached complete results survive;
|
|
161
|
+
// a partial (non-complete) object means a previous stream was aborted
|
|
162
|
+
// mid-flight — restart it.
|
|
163
|
+
const toStart = items
|
|
164
|
+
.filter((it) => it.includeSubitems)
|
|
165
|
+
.map((it) => it.descriptor)
|
|
166
|
+
.filter((d) => {
|
|
167
|
+
if (inFlight.has(d.id))
|
|
168
|
+
return false;
|
|
169
|
+
const c = subitemCounts[d.id];
|
|
170
|
+
if (c === undefined || c === "error")
|
|
171
|
+
return true;
|
|
172
|
+
if (c === "loading")
|
|
173
|
+
return false;
|
|
174
|
+
return !c.complete;
|
|
175
|
+
});
|
|
176
|
+
if (toStart.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
setSubitemCounts((prev) => {
|
|
179
|
+
const next = { ...prev };
|
|
180
|
+
for (const d of toStart)
|
|
181
|
+
next[d.id] = "loading";
|
|
182
|
+
return next;
|
|
183
|
+
});
|
|
184
|
+
for (const descriptor of toStart) {
|
|
185
|
+
const controller = new AbortController();
|
|
186
|
+
inFlight.set(descriptor.id, controller);
|
|
187
|
+
void (async () => {
|
|
188
|
+
try {
|
|
189
|
+
const finalCount = await streamSubitemCount({ itemId: descriptor.id, language: descriptor.language }, ({ count, complete }) => {
|
|
190
|
+
setSubitemCounts((prev) => ({
|
|
191
|
+
...prev,
|
|
192
|
+
[descriptor.id]: { count, complete },
|
|
193
|
+
}));
|
|
194
|
+
}, { sessionId, signal: controller.signal });
|
|
195
|
+
// Backend may close the stream without explicitly sending
|
|
196
|
+
// complete=true on the last line — mark complete on a clean exit.
|
|
197
|
+
setSubitemCounts((prev) => {
|
|
198
|
+
const existing = prev[descriptor.id];
|
|
199
|
+
const count = existing && typeof existing === "object"
|
|
200
|
+
? existing.count
|
|
201
|
+
: finalCount;
|
|
202
|
+
return {
|
|
203
|
+
...prev,
|
|
204
|
+
[descriptor.id]: { count, complete: true },
|
|
205
|
+
};
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
if (err?.name === "AbortError")
|
|
210
|
+
return;
|
|
211
|
+
setSubitemCounts((prev) => {
|
|
212
|
+
// Don't overwrite a partial count with "error" — keep what we have.
|
|
213
|
+
const existing = prev[descriptor.id];
|
|
214
|
+
if (existing && typeof existing === "object")
|
|
215
|
+
return prev;
|
|
216
|
+
return { ...prev, [descriptor.id]: "error" };
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
if (inFlight.get(descriptor.id) === controller) {
|
|
221
|
+
inFlight.delete(descriptor.id);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
})();
|
|
225
|
+
}
|
|
226
|
+
}, [items, subitemCounts, sessionId]);
|
|
227
|
+
// Abort any in-flight streams on unmount.
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
const inFlight = inFlightRef.current;
|
|
230
|
+
return () => {
|
|
231
|
+
for (const ctrl of inFlight.values())
|
|
232
|
+
ctrl.abort();
|
|
233
|
+
inFlight.clear();
|
|
234
|
+
};
|
|
235
|
+
}, []);
|
|
236
|
+
const cacheTreeNode = useCallback((node) => {
|
|
237
|
+
const id = node.id;
|
|
238
|
+
if (!id)
|
|
239
|
+
return;
|
|
240
|
+
const cache = itemCacheRef.current;
|
|
241
|
+
if (cache.has(id))
|
|
242
|
+
return;
|
|
243
|
+
const anyNode = node;
|
|
244
|
+
cache.set(id, fullItemFromDescriptor({
|
|
245
|
+
id,
|
|
246
|
+
language: node.language ?? language,
|
|
247
|
+
version: node.version ?? 1,
|
|
248
|
+
name: node.name,
|
|
249
|
+
displayName: node.displayName,
|
|
250
|
+
path: node.path,
|
|
251
|
+
idPath: anyNode.idPath,
|
|
252
|
+
icon: anyNode.icon,
|
|
253
|
+
templateId: anyNode.templateId,
|
|
254
|
+
hasChildren: anyNode.hasChildren,
|
|
255
|
+
}));
|
|
256
|
+
}, [language]);
|
|
257
|
+
const handleAddToList = useCallback(async (itemsToAdd) => {
|
|
258
|
+
const source = itemsToAdd ?? selectedInTree;
|
|
259
|
+
if (!source || source.length === 0)
|
|
260
|
+
return;
|
|
261
|
+
const existing = new Set(items.map((i) => i.descriptor.id));
|
|
262
|
+
const next = [...items];
|
|
263
|
+
for (const node of source) {
|
|
264
|
+
if (!node.id || existing.has(node.id))
|
|
265
|
+
continue;
|
|
266
|
+
cacheTreeNode(node);
|
|
267
|
+
next.push({
|
|
268
|
+
descriptor: {
|
|
269
|
+
id: node.id,
|
|
270
|
+
language: node.language ?? language,
|
|
271
|
+
version: node.version ?? 1,
|
|
272
|
+
name: node.name,
|
|
273
|
+
displayName: node.displayName,
|
|
274
|
+
path: node.path,
|
|
275
|
+
},
|
|
276
|
+
includeSubitems: false,
|
|
277
|
+
});
|
|
278
|
+
existing.add(node.id);
|
|
279
|
+
}
|
|
280
|
+
setItems(next);
|
|
281
|
+
commitToWizard(next);
|
|
282
|
+
setSelectedInTree([]);
|
|
283
|
+
setSelectedFromList([]);
|
|
284
|
+
}, [items, selectedInTree, language, cacheTreeNode, commitToWizard]);
|
|
285
|
+
const handleAddItem = useCallback(async (item) => {
|
|
286
|
+
const id = item.id;
|
|
287
|
+
if (!id || items.some((i) => i.descriptor.id === id))
|
|
288
|
+
return;
|
|
289
|
+
cacheTreeNode(item);
|
|
290
|
+
const next = [
|
|
291
|
+
...items,
|
|
292
|
+
{
|
|
293
|
+
descriptor: {
|
|
294
|
+
id,
|
|
295
|
+
language: item.language ?? language,
|
|
296
|
+
version: item.version ?? 1,
|
|
297
|
+
name: item.name,
|
|
298
|
+
displayName: item.displayName,
|
|
299
|
+
path: item.path,
|
|
300
|
+
},
|
|
301
|
+
includeSubitems: false,
|
|
302
|
+
},
|
|
303
|
+
];
|
|
304
|
+
setItems(next);
|
|
305
|
+
commitToWizard(next);
|
|
306
|
+
setSelectedFromList([]);
|
|
307
|
+
}, [items, language, cacheTreeNode, commitToWizard]);
|
|
308
|
+
const handleRemoveFromList = useCallback(() => {
|
|
309
|
+
const ids = new Set(selectedFromList.map((i) => i.descriptor.id));
|
|
310
|
+
const next = items.filter((i) => !ids.has(i.descriptor.id));
|
|
311
|
+
setItems(next);
|
|
312
|
+
commitToWizard(next);
|
|
313
|
+
setSelectedFromList([]);
|
|
314
|
+
}, [items, selectedFromList, commitToWizard]);
|
|
315
|
+
const handleRemoveItem = useCallback((index) => {
|
|
316
|
+
const next = items.filter((_, i) => i !== index);
|
|
317
|
+
setItems(next);
|
|
318
|
+
commitToWizard(next);
|
|
319
|
+
setSelectedFromList([]);
|
|
320
|
+
}, [items, commitToWizard]);
|
|
321
|
+
const handleToggleSubitems = useCallback((index) => {
|
|
322
|
+
const next = items.map((it, i) => i === index ? { ...it, includeSubitems: !it.includeSubitems } : it);
|
|
323
|
+
setItems(next);
|
|
324
|
+
commitToWizard(next);
|
|
325
|
+
}, [items, commitToWizard]);
|
|
326
|
+
return (_jsx(ItemCollectionEditor, { items: items, selectedInTree: selectedInTree, onSelectedInTreeChange: setSelectedInTree, selectedFromList: selectedFromList, onSelectedFromListChange: setSelectedFromList, onAddToList: handleAddToList, onRemoveFromList: handleRemoveFromList, onAddItem: handleAddItem, onRemoveItem: handleRemoveItem, onToggleSubitems: handleToggleSubitems, language: language, rootItemIds: [SITECORE_ROOT], selectedItemsLabel: "Items to Translate", emptyMessage: "No items selected", emptyHint: "Browse or search to add items", height: height, subitemCounts: subitemCounts, expandIdPath: expandIdPath, localStorageKey: "translation-wizard.itemCollectionSplitter" }));
|
|
327
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { TranslationStepProps } from "./types";
|
|
2
|
-
export declare function PromptCustomizationStep({
|
|
2
|
+
export declare function PromptCustomizationStep({ isActive, data, setData, onStepCompleted, editContext, }: TranslationStepProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=PromptCustomizationStep.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PromptCustomizationStep.d.ts","sourceRoot":"","sources":["../../src/steps/PromptCustomizationStep.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"PromptCustomizationStep.d.ts","sourceRoot":"","sources":["../../src/steps/PromptCustomizationStep.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAyB,MAAM,SAAS,CAAC;AAItE,wBAAgB,uBAAuB,CAAC,EACtC,QAAe,EACf,IAAI,EACJ,OAAO,EACP,eAAe,EACf,WAAW,GACZ,EAAE,oBAAoB,2CAsctB"}
|
|
@@ -1,15 +1,77 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Input, Select, Spinner, Textarea } from "@parhelia/core";
|
|
4
|
+
import { WizardStepShell } from "./WizardStepShell";
|
|
5
|
+
import { suggestBatchName } from "../api/discovery";
|
|
6
|
+
export function PromptCustomizationStep({ isActive = true, data, setData, onStepCompleted, editContext, }) {
|
|
4
7
|
const [customPrompt, setCustomPrompt] = useState("");
|
|
5
8
|
const [customizationType, setCustomizationType] = useState("extend");
|
|
6
|
-
//
|
|
9
|
+
// Local mirror of the wizard's batch name so typing is responsive.
|
|
10
|
+
const [batchName, setBatchName] = useState(data.batchName ?? "");
|
|
11
|
+
const [isSuggesting, setIsSuggesting] = useState(false);
|
|
12
|
+
// Whether the user has manually edited the name. If so, we never overwrite
|
|
13
|
+
// it with a fresh AI suggestion.
|
|
14
|
+
const userTouchedNameRef = useRef(!!data.batchName);
|
|
15
|
+
const dataRef = useRef(data);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
dataRef.current = data;
|
|
18
|
+
}, [data]);
|
|
19
|
+
// Push name changes back into wizard data (debounced via simple equality).
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if ((dataRef.current.batchName ?? "") === batchName)
|
|
22
|
+
return;
|
|
23
|
+
setData({ ...dataRef.current, batchName });
|
|
24
|
+
}, [batchName, setData]);
|
|
25
|
+
// Auto-suggest a name when the step becomes active and the user hasn't
|
|
26
|
+
// already provided/edited one. Fires once per item+language combination so
|
|
27
|
+
// navigating away and back doesn't re-fetch.
|
|
28
|
+
const lastSuggestionKeyRef = useRef("");
|
|
29
|
+
const sessionId = editContext?.sessionId;
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!isActive)
|
|
32
|
+
return;
|
|
33
|
+
if (userTouchedNameRef.current)
|
|
34
|
+
return;
|
|
35
|
+
const itemIds = (data.selectionTreeItems && data.selectionTreeItems.length > 0
|
|
36
|
+
? data.selectionTreeItems.map((s) => s.descriptor.id)
|
|
37
|
+
: data.items.map((i) => i.descriptor.id)).filter(Boolean);
|
|
38
|
+
const langs = [...data.targetLanguages].sort();
|
|
39
|
+
if (itemIds.length === 0 || langs.length === 0)
|
|
40
|
+
return;
|
|
41
|
+
const key = JSON.stringify({ itemIds: [...itemIds].sort(), langs });
|
|
42
|
+
if (lastSuggestionKeyRef.current === key)
|
|
43
|
+
return;
|
|
44
|
+
lastSuggestionKeyRef.current = key;
|
|
45
|
+
let cancelled = false;
|
|
46
|
+
setIsSuggesting(true);
|
|
47
|
+
void (async () => {
|
|
48
|
+
try {
|
|
49
|
+
const includeSubitems = !!data.selectionTreeItems?.some((s) => s.includeSubitems);
|
|
50
|
+
const name = await suggestBatchName({ itemIds, targetLanguages: langs, includeSubitems }, sessionId);
|
|
51
|
+
if (cancelled || userTouchedNameRef.current)
|
|
52
|
+
return;
|
|
53
|
+
if (name)
|
|
54
|
+
setBatchName(name);
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
if (!cancelled)
|
|
58
|
+
setIsSuggesting(false);
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
return () => {
|
|
62
|
+
cancelled = true;
|
|
63
|
+
};
|
|
64
|
+
}, [
|
|
65
|
+
isActive,
|
|
66
|
+
sessionId,
|
|
67
|
+
data.selectionTreeItems,
|
|
68
|
+
data.items,
|
|
69
|
+
data.targetLanguages,
|
|
70
|
+
]);
|
|
7
71
|
const customPromptRef = useRef(customPrompt);
|
|
8
72
|
const customizationTypeRef = useRef(customizationType);
|
|
9
|
-
// Track if we've initialized from parent data to prevent re-initialization during typing
|
|
10
73
|
const hasInitializedRef = useRef(false);
|
|
11
74
|
const lastProviderRef = useRef(data.translationProvider);
|
|
12
|
-
// Debounce timer ref for parent updates
|
|
13
75
|
const updateTimerRef = useRef(null);
|
|
14
76
|
useEffect(() => {
|
|
15
77
|
customPromptRef.current = customPrompt;
|
|
@@ -17,36 +79,32 @@ export function PromptCustomizationStep({ stepIndex, isActive = true, data, setD
|
|
|
17
79
|
useEffect(() => {
|
|
18
80
|
customizationTypeRef.current = customizationType;
|
|
19
81
|
}, [customizationType]);
|
|
20
|
-
|
|
82
|
+
const selectedProvider = useMemo(() => data.translationProviders.find((p) => p.name === data.translationProvider), [data.translationProviders, data.translationProvider]);
|
|
21
83
|
const serviceData = useMemo(() => {
|
|
22
84
|
if (!data.serviceCustomData || !data.translationProvider)
|
|
23
85
|
return null;
|
|
24
86
|
return data.serviceCustomData.get(data.translationProvider);
|
|
25
87
|
}, [data.serviceCustomData, data.translationProvider]);
|
|
26
88
|
const enableCustomPrompt = serviceData?.enableCustomPrompt === true;
|
|
27
|
-
|
|
89
|
+
const supportsPromptCustomization = data.translationProvider === "AI";
|
|
28
90
|
const defaultPrompt = useMemo(() => {
|
|
29
|
-
const
|
|
30
|
-
const prompt = provider?.defaultPrompt;
|
|
31
|
-
// Return null if prompt is null, undefined, or empty string
|
|
91
|
+
const prompt = selectedProvider?.defaultPrompt;
|
|
32
92
|
return prompt && prompt.trim() ? prompt : null;
|
|
33
|
-
}, [
|
|
34
|
-
const hasDefaultPrompt = defaultPrompt != null && defaultPrompt.
|
|
35
|
-
// Initialize from existing data - only on mount or provider change
|
|
93
|
+
}, [selectedProvider?.defaultPrompt]);
|
|
94
|
+
const hasDefaultPrompt = defaultPrompt != null && defaultPrompt.length > 0;
|
|
36
95
|
useEffect(() => {
|
|
37
|
-
// Reset initialization when provider changes
|
|
38
96
|
if (lastProviderRef.current !== data.translationProvider) {
|
|
39
97
|
hasInitializedRef.current = false;
|
|
40
98
|
lastProviderRef.current = data.translationProvider;
|
|
41
99
|
}
|
|
42
|
-
|
|
43
|
-
if (hasInitializedRef.current) {
|
|
100
|
+
if (hasInitializedRef.current)
|
|
44
101
|
return;
|
|
45
|
-
|
|
46
|
-
|
|
102
|
+
const nextCustomizationType = serviceData?.promptCustomizationType ||
|
|
103
|
+
"extend";
|
|
47
104
|
let nextCustomPrompt = serviceData?.customPrompt || "";
|
|
48
|
-
|
|
49
|
-
|
|
105
|
+
if (hasDefaultPrompt &&
|
|
106
|
+
nextCustomizationType === "extend" &&
|
|
107
|
+
nextCustomPrompt.startsWith(defaultPrompt || "")) {
|
|
50
108
|
nextCustomPrompt = nextCustomPrompt.slice((defaultPrompt || "").length);
|
|
51
109
|
nextCustomPrompt = nextCustomPrompt.replace(/^\s*\n\s*\n?/, "");
|
|
52
110
|
}
|
|
@@ -60,29 +118,70 @@ export function PromptCustomizationStep({ stepIndex, isActive = true, data, setD
|
|
|
60
118
|
}
|
|
61
119
|
hasInitializedRef.current = true;
|
|
62
120
|
}, [
|
|
121
|
+
data.translationProvider,
|
|
122
|
+
defaultPrompt,
|
|
63
123
|
enableCustomPrompt,
|
|
64
|
-
serviceData,
|
|
65
124
|
hasDefaultPrompt,
|
|
66
|
-
|
|
67
|
-
data.translationProvider,
|
|
125
|
+
serviceData,
|
|
68
126
|
]);
|
|
69
|
-
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!isActive)
|
|
129
|
+
return;
|
|
130
|
+
onStepCompleted(!!data.translationProvider);
|
|
131
|
+
}, [data.translationProvider, isActive, onStepCompleted]);
|
|
132
|
+
const handleProviderChange = (e) => {
|
|
133
|
+
const newProvider = e.target.value;
|
|
134
|
+
const newServiceCustomData = new Map();
|
|
135
|
+
data.serviceCustomData?.forEach((value, key) => {
|
|
136
|
+
if (key !== "AI" || newProvider === "AI") {
|
|
137
|
+
newServiceCustomData.set(key, value);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
const newData = {
|
|
141
|
+
...data,
|
|
142
|
+
translationProvider: newProvider,
|
|
143
|
+
serviceCustomData: newServiceCustomData,
|
|
144
|
+
};
|
|
145
|
+
setData(newData);
|
|
146
|
+
};
|
|
147
|
+
const handleCustomPromptToggle = (enabled) => {
|
|
148
|
+
const newServiceCustomData = new Map(data.serviceCustomData || new Map());
|
|
149
|
+
if (enabled) {
|
|
150
|
+
newServiceCustomData.set(data.translationProvider, {
|
|
151
|
+
enableCustomPrompt: true,
|
|
152
|
+
customPrompt: "",
|
|
153
|
+
promptCustomizationType: "extend",
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
newServiceCustomData.delete(data.translationProvider);
|
|
158
|
+
}
|
|
159
|
+
setData({
|
|
160
|
+
...data,
|
|
161
|
+
serviceCustomData: newServiceCustomData,
|
|
162
|
+
});
|
|
163
|
+
};
|
|
70
164
|
const previewPrompt = useMemo(() => {
|
|
71
165
|
if (!enableCustomPrompt || !customPrompt.trim()) {
|
|
72
166
|
return defaultPrompt || "";
|
|
73
167
|
}
|
|
74
|
-
// If no default prompt, just show custom prompt
|
|
75
168
|
if (!hasDefaultPrompt) {
|
|
76
169
|
return customPrompt;
|
|
77
170
|
}
|
|
78
|
-
// If default prompt exists, show based on customization type
|
|
79
171
|
if (customizationType === "replace") {
|
|
80
172
|
return customPrompt;
|
|
81
173
|
}
|
|
82
174
|
return `${defaultPrompt}\n\n${customPrompt}`;
|
|
83
|
-
}, [
|
|
84
|
-
|
|
175
|
+
}, [
|
|
176
|
+
customPrompt,
|
|
177
|
+
customizationType,
|
|
178
|
+
defaultPrompt,
|
|
179
|
+
enableCustomPrompt,
|
|
180
|
+
hasDefaultPrompt,
|
|
181
|
+
]);
|
|
85
182
|
const updateParentData = useCallback(() => {
|
|
183
|
+
if (!data.translationProvider)
|
|
184
|
+
return;
|
|
86
185
|
const trimmedCustomPrompt = customPromptRef.current.trim();
|
|
87
186
|
const currentCustomizationType = customizationTypeRef.current;
|
|
88
187
|
const newServiceCustomData = new Map(data.serviceCustomData || new Map());
|
|
@@ -95,33 +194,28 @@ export function PromptCustomizationStep({ stepIndex, isActive = true, data, setD
|
|
|
95
194
|
};
|
|
96
195
|
const isSame = currentServiceData?.enableCustomPrompt === true &&
|
|
97
196
|
(currentServiceData.customPrompt || "") === trimmedCustomPrompt &&
|
|
98
|
-
(currentServiceData.promptCustomizationType || "extend") ===
|
|
99
|
-
|
|
197
|
+
(currentServiceData.promptCustomizationType || "extend") ===
|
|
198
|
+
currentCustomizationType;
|
|
199
|
+
if (isSame)
|
|
100
200
|
return;
|
|
101
|
-
}
|
|
102
201
|
newServiceCustomData.set(data.translationProvider, nextServiceData);
|
|
103
202
|
}
|
|
104
203
|
else {
|
|
105
|
-
if (!currentServiceData)
|
|
204
|
+
if (!currentServiceData)
|
|
106
205
|
return;
|
|
107
|
-
}
|
|
108
206
|
newServiceCustomData.delete(data.translationProvider);
|
|
109
207
|
}
|
|
110
|
-
|
|
208
|
+
setData({
|
|
111
209
|
...data,
|
|
112
210
|
serviceCustomData: newServiceCustomData,
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
}, [enableCustomPrompt, data, setData]);
|
|
116
|
-
// Update wizard data when settings change - debounced to prevent rapid fire
|
|
211
|
+
});
|
|
212
|
+
}, [data, enableCustomPrompt, setData]);
|
|
117
213
|
useEffect(() => {
|
|
118
|
-
if (!isActive)
|
|
214
|
+
if (!isActive || !supportsPromptCustomization)
|
|
119
215
|
return;
|
|
120
|
-
// Clear any pending update
|
|
121
216
|
if (updateTimerRef.current) {
|
|
122
217
|
clearTimeout(updateTimerRef.current);
|
|
123
218
|
}
|
|
124
|
-
// Debounce the update to parent (100ms delay)
|
|
125
219
|
updateTimerRef.current = setTimeout(() => {
|
|
126
220
|
updateParentData();
|
|
127
221
|
}, 100);
|
|
@@ -131,24 +225,33 @@ export function PromptCustomizationStep({ stepIndex, isActive = true, data, setD
|
|
|
131
225
|
}
|
|
132
226
|
};
|
|
133
227
|
}, [
|
|
134
|
-
enableCustomPrompt,
|
|
135
228
|
customPrompt,
|
|
136
229
|
customizationType,
|
|
137
230
|
isActive,
|
|
231
|
+
supportsPromptCustomization,
|
|
138
232
|
updateParentData,
|
|
139
233
|
]);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
234
|
+
return (_jsx(WizardStepShell, { fillHeight: true, testId: "prompt-customization-step", children: _jsxs("div", { className: "mx-auto flex min-h-0 w-full max-w-3xl flex-1 flex-col gap-6 overflow-y-auto", children: [_jsxs("div", { children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("label", { htmlFor: "translation-batch-name", className: "text-[11px] font-bold tracking-wider text-neutral-grey-50 ", children: "Name" }), isSuggesting && (_jsx("div", { role: "status", "aria-label": "Suggesting name", className: "text-muted-foreground flex h-3.5 w-3.5 items-center justify-center", children: _jsx(Spinner, { size: "xs" }) }))] }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: "A short label for this translation batch." }), _jsx(Input, { id: "translation-batch-name", type: "text", value: batchName, placeholder: "Translation batch", onChange: (e) => {
|
|
235
|
+
userTouchedNameRef.current = true;
|
|
236
|
+
setBatchName(e.target.value);
|
|
237
|
+
}, className: "mt-2", "data-testid": "translation-batch-name-input" })] }), _jsxs("div", { children: [_jsx("label", { className: "text-[11px] font-bold tracking-wider text-neutral-grey-50 ", children: "Translation provider" }), _jsx("p", { className: "text-muted-foreground mt-1 text-xs", children: "\"Create Versions\" will create new language versions without automatic translation." }), _jsx(Select, { value: data.translationProvider || "", onValueChange: (value) => handleProviderChange({
|
|
238
|
+
target: { value },
|
|
239
|
+
}), options: data.translationProviders.map((provider) => ({
|
|
240
|
+
value: provider.name,
|
|
241
|
+
label: provider.displayName || provider.name,
|
|
242
|
+
})), placeholder: "Select a provider\u2026", size: "sm", className: "mt-2 w-full", "data-testid": "translation-provider-select" })] }), supportsPromptCustomization && (_jsxs(_Fragment, { children: [_jsxs("label", { className: "-mb-2 flex cursor-pointer items-start gap-2.5", children: [_jsx("input", { type: "checkbox", checked: enableCustomPrompt, onChange: (e) => handleCustomPromptToggle(e.target.checked), className: "mt-0.5 h-3.5 w-3.5 rounded border-border-default text-[var(--color-highlight-100)] accent-[var(--color-highlight-100)] focus:ring-[var(--color-highlight-100)]", "data-testid": "enable-custom-prompt-checkbox" }), _jsxs("div", { className: "min-w-0", children: [_jsx("span", { className: "block text-[13px] font-medium text-neutral-grey-100", children: "Customize translation prompt" }), _jsx("span", { className: "text-muted-foreground mt-0.5 block text-xs", children: "Override or extend the provider's default instructions." })] })] }), enableCustomPrompt && (_jsxs(_Fragment, { children: [hasDefaultPrompt && (_jsxs(_Fragment, { children: [_jsxs("div", { children: [_jsx("span", { className: "text-[11px] font-bold tracking-wider text-neutral-grey-50 ", children: "Default prompt" }), _jsx("pre", { className: "mt-2 max-h-40 overflow-y-auto rounded-md bg-neutral-grey-5/70 p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap text-neutral-grey-100", children: defaultPrompt })] }), _jsxs("div", { children: [_jsx("span", { className: "text-[11px] font-bold tracking-wider text-neutral-grey-50 ", children: "Customization type" }), _jsx("div", { className: "mt-2 grid gap-2 sm:grid-cols-2", children: [
|
|
243
|
+
{
|
|
244
|
+
value: "extend",
|
|
245
|
+
title: "Extend",
|
|
246
|
+
hint: "Append to the default prompt.",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
value: "replace",
|
|
250
|
+
title: "Replace",
|
|
251
|
+
hint: "Use the custom prompt only.",
|
|
252
|
+
},
|
|
253
|
+
].map((opt) => {
|
|
254
|
+
const selected = customizationType === opt.value;
|
|
255
|
+
return (_jsxs("label", { className: `group flex cursor-pointer items-start gap-2.5 rounded-lg p-2.5 transition-colors ${selected ? "bg-primary/5" : "hover:bg-neutral-grey-5"}`, children: [_jsx("input", { type: "radio", name: "customizationType", value: opt.value, checked: selected, onChange: () => setCustomizationType(opt.value), className: "mt-0.5 h-3.5 w-3.5 border-border-default text-[var(--color-highlight-100)] accent-[var(--color-highlight-100)] focus:ring-[var(--color-highlight-100)]", "data-testid": `customization-type-${opt.value}` }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "text-xs font-medium text-neutral-grey-100", children: opt.title }), _jsx("div", { className: "text-muted-foreground mt-0.5 text-[11px]", children: opt.hint })] })] }, opt.value));
|
|
256
|
+
}) })] })] })), _jsxs("div", { children: [_jsx("span", { className: "text-[11px] font-bold tracking-wider text-neutral-grey-50 ", children: "Custom prompt" }), _jsx(Textarea, { value: customPrompt, onChange: (e) => setCustomPrompt(e.target.value), className: "mt-2 font-mono", rows: 6, placeholder: "Enter your custom prompt instructions here\u2026", "data-testid": "custom-prompt-textarea" })] }), hasDefaultPrompt && (_jsxs("div", { children: [_jsx("span", { className: "text-[11px] font-bold tracking-wider text-neutral-grey-50 ", children: "Preview" }), _jsx("pre", { className: "mt-2 max-h-52 overflow-y-auto rounded-md bg-neutral-grey-5/70 p-3 font-mono text-xs leading-relaxed whitespace-pre-wrap text-neutral-grey-100", children: previewPrompt })] }))] }))] }))] }) }));
|
|
154
257
|
}
|