@case-framework/survey-core 0.1.0 → 0.2.0

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/build/editor.mjs CHANGED
@@ -1,5 +1,307 @@
1
- import { D as structuredCloneMethod, T as generateId, n as GroupItemCore, r as SurveyItemTranslations, s as ReservedSurveyItemTypes, t as Survey } from "./survey-C3ZHI-5z.mjs";
1
+ import { F as structuredCloneMethod, N as generateId, l as GroupItemCore, m as ReservedSurveyItemTypes, n as SurveyItemTranslations, t as Survey } from "./survey-DQmpzihl.mjs";
2
2
 
3
+ //#region src/editor/ai-context.ts
4
+ const DEFAULT_SCOPE_LIMITS = {
5
+ tiny: 40,
6
+ focused: 150,
7
+ full: 500
8
+ };
9
+ const DEFAULT_FOCUS_TREE_LIMIT = 120;
10
+ const DEFAULT_TRANSLATION_SNIPPETS_PER_ITEM = 8;
11
+ const DEFAULT_TRANSLATION_SNIPPET_TEXT_LIMIT = 120;
12
+ const PURPOSE_DEFAULTS = {
13
+ generic: {
14
+ scope: "tiny",
15
+ includeRawSurvey: false,
16
+ includeIndexes: false,
17
+ includeFocusItemTree: false
18
+ },
19
+ "key-suggestion": {
20
+ scope: "tiny",
21
+ includeRawSurvey: false,
22
+ includeIndexes: false,
23
+ includeFocusItemTree: true
24
+ },
25
+ "label-suggestion": {
26
+ scope: "tiny",
27
+ includeRawSurvey: false,
28
+ includeIndexes: false,
29
+ includeFocusItemTree: true
30
+ },
31
+ "item-generation": {
32
+ scope: "focused",
33
+ includeRawSurvey: false,
34
+ includeIndexes: true,
35
+ includeFocusItemTree: true
36
+ },
37
+ translation: {
38
+ scope: "focused",
39
+ includeRawSurvey: false,
40
+ includeIndexes: true,
41
+ includeFocusItemTree: true
42
+ },
43
+ "condition-management": {
44
+ scope: "full",
45
+ includeRawSurvey: true,
46
+ includeIndexes: true,
47
+ includeFocusItemTree: true
48
+ }
49
+ };
50
+ const sortByItemId = (items) => [...items].sort((a, b) => a.itemId.localeCompare(b.itemId));
51
+ function getChildIds(survey, itemId) {
52
+ const item = survey.surveyItems.get(itemId);
53
+ if (!(item instanceof GroupItemCore)) return [];
54
+ return [...item.items ?? []];
55
+ }
56
+ function buildParentLookup(survey) {
57
+ const lookup = /* @__PURE__ */ new Map();
58
+ for (const item of survey.surveyItems.values()) {
59
+ if (!(item instanceof GroupItemCore)) continue;
60
+ for (const childId of item.items ?? []) lookup.set(childId, item.id);
61
+ }
62
+ return lookup;
63
+ }
64
+ function createFullKeyGetter(survey, parentLookup) {
65
+ const fullKeyCache = /* @__PURE__ */ new Map();
66
+ const getFullKey = (itemId, stack = /* @__PURE__ */ new Set()) => {
67
+ const cached = fullKeyCache.get(itemId);
68
+ if (cached) return cached;
69
+ const item = survey.surveyItems.get(itemId);
70
+ if (!item) return itemId;
71
+ if (stack.has(itemId)) return item.key;
72
+ const parentId = parentLookup.get(itemId);
73
+ if (!parentId) {
74
+ fullKeyCache.set(itemId, item.key);
75
+ return item.key;
76
+ }
77
+ stack.add(itemId);
78
+ const parentFullKey = getFullKey(parentId, stack);
79
+ stack.delete(itemId);
80
+ const fullKey = `${parentFullKey}.${item.key}`;
81
+ fullKeyCache.set(itemId, fullKey);
82
+ return fullKey;
83
+ };
84
+ return getFullKey;
85
+ }
86
+ function buildFullOutline(survey, parentLookup, getFullKey) {
87
+ const outline = [];
88
+ const visited = /* @__PURE__ */ new Set();
89
+ const toNode = (itemId, depth) => {
90
+ const item = survey.surveyItems.get(itemId);
91
+ if (!item) return;
92
+ const childCount = item instanceof GroupItemCore ? item.items.length : 0;
93
+ return {
94
+ itemId,
95
+ parentId: parentLookup.get(itemId),
96
+ depth,
97
+ itemType: item.type,
98
+ key: item.key,
99
+ fullKey: getFullKey(itemId),
100
+ itemLabel: item.metadata?.itemLabel,
101
+ childCount
102
+ };
103
+ };
104
+ const visit = (itemId, depth) => {
105
+ if (visited.has(itemId)) return;
106
+ visited.add(itemId);
107
+ const node = toNode(itemId, depth);
108
+ if (node) outline.push(node);
109
+ for (const childId of getChildIds(survey, itemId)) visit(childId, depth + 1);
110
+ };
111
+ const root = survey.rootItem;
112
+ if (root) visit(root.id, 0);
113
+ const remaining = sortByItemId(Array.from(survey.surveyItems.values()).filter((item) => !visited.has(item.id)).map((item) => ({ itemId: item.id })));
114
+ for (const item of remaining) visit(item.itemId, 0);
115
+ return outline;
116
+ }
117
+ function collectTinyScopeIds(survey, focusItemId) {
118
+ const ids = /* @__PURE__ */ new Set();
119
+ const rootItem = survey.rootItem;
120
+ if (rootItem) ids.add(rootItem.id);
121
+ ids.add(focusItemId);
122
+ for (const id of survey.getItemPath(focusItemId)) ids.add(id);
123
+ for (const sibling of survey.getSiblings(focusItemId)) ids.add(sibling.id);
124
+ for (const childId of getChildIds(survey, focusItemId)) ids.add(childId);
125
+ return ids;
126
+ }
127
+ function addDescendants(survey, itemId, set, maxDepth) {
128
+ const queue = [{
129
+ id: itemId,
130
+ depth: 0
131
+ }];
132
+ while (queue.length > 0) {
133
+ const current = queue.shift();
134
+ if (!current) continue;
135
+ if (current.depth >= maxDepth) continue;
136
+ for (const childId of getChildIds(survey, current.id)) {
137
+ set.add(childId);
138
+ queue.push({
139
+ id: childId,
140
+ depth: current.depth + 1
141
+ });
142
+ }
143
+ }
144
+ }
145
+ function collectFocusedScopeIds(survey, focusItemId) {
146
+ const ids = collectTinyScopeIds(survey, focusItemId);
147
+ const ancestors = survey.getItemPath(focusItemId);
148
+ addDescendants(survey, focusItemId, ids, 2);
149
+ for (const sibling of survey.getSiblings(focusItemId)) {
150
+ if (sibling.id === focusItemId) continue;
151
+ ids.add(sibling.id);
152
+ addDescendants(survey, sibling.id, ids, 1);
153
+ }
154
+ for (const ancestorId of ancestors) {
155
+ const siblingIds = (survey.getParentItem(ancestorId)?.items ?? []).filter((id) => id !== ancestorId);
156
+ for (const siblingId of siblingIds) {
157
+ ids.add(siblingId);
158
+ addDescendants(survey, siblingId, ids, 1);
159
+ }
160
+ }
161
+ return ids;
162
+ }
163
+ function buildFocusInfo(survey, focusItemId) {
164
+ if (!survey.surveyItems.has(focusItemId)) return;
165
+ const pathItemIds = [...survey.getItemPath(focusItemId), focusItemId];
166
+ return {
167
+ focusItemId,
168
+ parentItemId: survey.getParentItem(focusItemId)?.id,
169
+ pathItemIds,
170
+ siblingItemIds: survey.getSiblings(focusItemId).filter((item) => item.id !== focusItemId).map((item) => item.id),
171
+ childItemIds: getChildIds(survey, focusItemId)
172
+ };
173
+ }
174
+ const normalizeSnippetText = (text, maxChars) => {
175
+ return text.trim().replace(/\s+/g, " ").slice(0, maxChars);
176
+ };
177
+ const getContentText = (content, maxChars) => {
178
+ if (!content) return null;
179
+ const maybeText = content.content;
180
+ if (typeof maybeText !== "string") return null;
181
+ const normalized = normalizeSnippetText(maybeText, maxChars);
182
+ return normalized.length > 0 ? normalized : null;
183
+ };
184
+ function getItemTranslationSnippets(survey, itemId, maxSnippets, textLimit) {
185
+ const snippets = [];
186
+ const translations = survey.getItemTranslations(itemId);
187
+ if (!translations) return snippets;
188
+ for (const locale of translations.locales) {
189
+ const localeContent = translations.getAllForLocale(locale);
190
+ if (!localeContent) continue;
191
+ for (const [contentKey, content] of Object.entries(localeContent)) {
192
+ const text = getContentText(content, textLimit);
193
+ if (!text) continue;
194
+ snippets.push({
195
+ locale,
196
+ contentKey,
197
+ text
198
+ });
199
+ if (snippets.length >= maxSnippets) return snippets;
200
+ }
201
+ }
202
+ return snippets;
203
+ }
204
+ function buildFocusItemTree(survey, focusItemId, getFullKey, options) {
205
+ if (!survey.surveyItems.has(focusItemId)) return { truncated: false };
206
+ let remainingBudget = options.treeLimit;
207
+ let truncated = false;
208
+ const visit = (itemId) => {
209
+ if (remainingBudget <= 0) {
210
+ truncated = true;
211
+ return;
212
+ }
213
+ const item = survey.surveyItems.get(itemId);
214
+ if (!item) return;
215
+ remainingBudget -= 1;
216
+ const siblingKeys = survey.getSiblings(itemId).filter((sibling) => sibling.id !== itemId).map((sibling) => sibling.key);
217
+ const descendants = [];
218
+ if (item instanceof GroupItemCore) for (const childId of item.items) {
219
+ const childNode = visit(childId);
220
+ if (childNode) descendants.push(childNode);
221
+ }
222
+ return {
223
+ itemId: item.id,
224
+ itemType: item.type,
225
+ itemKey: item.key,
226
+ fullKey: getFullKey(item.id),
227
+ itemLabel: item.metadata?.itemLabel,
228
+ siblingKeys,
229
+ translations: getItemTranslationSnippets(survey, item.id, options.snippetsPerItem, options.snippetTextLimit),
230
+ descendants
231
+ };
232
+ };
233
+ return {
234
+ focusItem: visit(focusItemId),
235
+ truncated
236
+ };
237
+ }
238
+ function buildContextIndexes(survey, fullOutline, purpose) {
239
+ const indexes = { keyIndex: fullOutline.map((node) => ({
240
+ itemId: node.itemId,
241
+ itemType: node.itemType,
242
+ key: node.key,
243
+ fullKey: node.fullKey,
244
+ itemLabel: node.itemLabel,
245
+ path: node.fullKey.split(".").slice(0, -1)
246
+ })) };
247
+ if (purpose === "condition-management") indexes.responseSlots = Object.keys(survey.getAvailableResponseValueSlots());
248
+ return indexes;
249
+ }
250
+ function buildSurveyAIContextPack(survey, options = {}) {
251
+ const purpose = options.purpose ?? "generic";
252
+ const purposeDefaults = PURPOSE_DEFAULTS[purpose];
253
+ const scope = options.scope ?? purposeDefaults.scope;
254
+ const defaultLimit = DEFAULT_SCOPE_LIMITS[scope];
255
+ const outlineLimit = Math.max(1, options.outlineLimit ?? defaultLimit);
256
+ const includeRawSurvey = options.includeRawSurvey ?? purposeDefaults.includeRawSurvey;
257
+ const includeIndexes = options.includeIndexes ?? purposeDefaults.includeIndexes;
258
+ const includeFocusItemTree = options.includeFocusItemTree ?? purposeDefaults.includeFocusItemTree;
259
+ const focusItemId = options.focusItemId;
260
+ const focusExists = Boolean(focusItemId && survey.surveyItems.has(focusItemId));
261
+ const parentLookup = buildParentLookup(survey);
262
+ const getFullKey = createFullKeyGetter(survey, parentLookup);
263
+ const fullOutline = buildFullOutline(survey, parentLookup, getFullKey);
264
+ let scopedOutline = fullOutline;
265
+ if (scope === "tiny" && focusItemId && focusExists) {
266
+ const includedIds = collectTinyScopeIds(survey, focusItemId);
267
+ scopedOutline = fullOutline.filter((node) => includedIds.has(node.itemId));
268
+ } else if (scope === "focused" && focusItemId && focusExists) {
269
+ const includedIds = collectFocusedScopeIds(survey, focusItemId);
270
+ scopedOutline = fullOutline.filter((node) => includedIds.has(node.itemId));
271
+ }
272
+ const outlineTruncated = scopedOutline.length > outlineLimit;
273
+ const outline = outlineTruncated ? scopedOutline.slice(0, outlineLimit) : scopedOutline;
274
+ let focusItemTreeTruncated = false;
275
+ let focusItem;
276
+ if (includeFocusItemTree && focusItemId && focusExists) {
277
+ const focusTreeResult = buildFocusItemTree(survey, focusItemId, getFullKey, {
278
+ treeLimit: Math.max(1, options.focusItemTreeLimit ?? DEFAULT_FOCUS_TREE_LIMIT),
279
+ snippetsPerItem: Math.max(1, options.translationSnippetsPerItemLimit ?? DEFAULT_TRANSLATION_SNIPPETS_PER_ITEM),
280
+ snippetTextLimit: Math.max(1, options.translationSnippetTextLimit ?? DEFAULT_TRANSLATION_SNIPPET_TEXT_LIMIT)
281
+ });
282
+ focusItemTreeTruncated = focusTreeResult.truncated;
283
+ focusItem = focusTreeResult.focusItem;
284
+ }
285
+ return {
286
+ purpose,
287
+ scope,
288
+ surveyKey: survey.surveyKey,
289
+ locales: [...survey.locales],
290
+ itemCount: survey.surveyItems.size,
291
+ outline,
292
+ focus: focusItemId ? buildFocusInfo(survey, focusItemId) : void 0,
293
+ focusItem,
294
+ indexes: includeIndexes ? buildContextIndexes(survey, fullOutline, purpose) : void 0,
295
+ flags: {
296
+ outlineTruncated,
297
+ rawSurveyIncluded: includeRawSurvey,
298
+ focusItemTreeTruncated
299
+ },
300
+ rawSurvey: includeRawSurvey ? survey.serialize() : void 0
301
+ };
302
+ }
303
+
304
+ //#endregion
3
305
  //#region src/editor/item-copy-paste.ts
4
306
  var ItemCopyPaste = class ItemCopyPaste {
5
307
  survey;
@@ -399,8 +701,11 @@ var SurveyEditor = class SurveyEditor {
399
701
  this._pluginRegistry = pluginRegistry;
400
702
  this._undoRedo = new SurveyEditorUndoRedo(survey.serialize(), undoRedoConfig, meta);
401
703
  }
704
+ /** Returns an immutable copy of the current survey state. */
402
705
  get survey() {
403
- return this._survey;
706
+ const serialized = this._survey.serialize();
707
+ const registry = this._pluginRegistry ?? this._survey.getPluginRegistry();
708
+ return Survey.fromJson(serialized, registry);
404
709
  }
405
710
  get hasUncommittedChanges() {
406
711
  return this._hasUncommittedChanges;
@@ -535,7 +840,7 @@ var SurveyEditor = class SurveyEditor {
535
840
  if (target?.index !== void 0) insertIndex = Math.min(target.index, parentGroup.items.length);
536
841
  else insertIndex = parentGroup.items.length;
537
842
  this._survey.surveyItems.set(item.id, item);
538
- parentGroup.items.splice(insertIndex, 0, item.id);
843
+ parentGroup.addChild(item.id, insertIndex);
539
844
  if (content) this._survey.translations.setItemTranslations(item.id, content);
540
845
  }
541
846
  removeItem(itemId, nested = false) {
@@ -572,6 +877,51 @@ var SurveyEditor = class SurveyEditor {
572
877
  targetGroup.items.splice(insertIndex, 0, itemId);
573
878
  return true;
574
879
  }
880
+ /**
881
+ * Get a deep copy of an item's raw data.
882
+ * The returned object is independent of the survey—mutating it will not affect the survey.
883
+ * Use this when you need to edit an item in a form or pass it to updateItem after modifications.
884
+ *
885
+ * @param itemId - The ID of the item to copy
886
+ * @returns A deep copy of the item's RawSurveyItem data
887
+ * @throws Error if the item is not found
888
+ */
889
+ getItemDataCopy(itemId) {
890
+ const item = this._survey.surveyItems.get(itemId);
891
+ if (!item) throw new Error(`Item with id '${itemId}' not found`);
892
+ return JSON.parse(JSON.stringify(item.rawItem));
893
+ }
894
+ /**
895
+ * Update an item's data in the survey.
896
+ * Can update common attributes (displayConditions, disabledConditions, validations, etc.)
897
+ * as well as type-specific config.
898
+ *
899
+ * Use getItemDataCopy to obtain an editable copy, modify it, then pass it here.
900
+ * Alternatively, pass a Partial to merge specific fields (top-level keys only;
901
+ * e.g. config replaces the entire config object).
902
+ *
903
+ * @param itemId - The ID of the item to update (must match data.id if provided)
904
+ * @param rawItemData - Full RawSurveyItem or Partial to merge. The id cannot be changed.
905
+ */
906
+ updateItem(itemId, rawItemData) {
907
+ const existingItem = this._survey.surveyItems.get(itemId);
908
+ if (!existingItem) throw new Error(`Item with id '${itemId}' not found`);
909
+ if ("id" in rawItemData && rawItemData.id !== void 0 && rawItemData.id !== itemId) throw new Error(`Cannot change item id from '${itemId}' to '${rawItemData.id}'`);
910
+ const merged = {
911
+ ...existingItem.rawItem,
912
+ ...rawItemData,
913
+ id: itemId
914
+ };
915
+ const newKey = merged.key ?? existingItem.key;
916
+ const parentGroup = this._survey.getParentItem(itemId);
917
+ if (parentGroup) {
918
+ const siblingWithSameKey = (parentGroup.items ?? []).filter((id) => id !== itemId).map((id) => this._survey.surveyItems.get(id)).find((s) => s !== void 0 && s.key === newKey);
919
+ if (siblingWithSameKey) throw new Error(`Key '${newKey}' is already in use by sibling item '${siblingWithSameKey.id}'`);
920
+ }
921
+ const newItem = this._survey.createItemFromRaw(merged);
922
+ this._survey.surveyItems.set(itemId, newItem);
923
+ this.markAsModified();
924
+ }
575
925
  updateItemTranslations(itemId, updatedContent) {
576
926
  this.markAsModified();
577
927
  if (!this._survey.surveyItems.get(itemId)) throw new Error(`Item with id '${itemId}' not found`);
@@ -579,18 +929,89 @@ var SurveyEditor = class SurveyEditor {
579
929
  return true;
580
930
  }
581
931
  /**
932
+ * Update survey-level translations that are not tied to specific items.
933
+ * Use these for survey card, navigation, validation messages, etc.
934
+ */
935
+ updateSurveyTranslations(updates) {
936
+ this.markAsModified();
937
+ if (updates.surveyCard) this._survey.translations.setSurveyCardContent(updates.surveyCard.locale, updates.surveyCard.content);
938
+ if (updates.navigation) this._survey.translations.setNavigationContent(updates.navigation.locale, updates.navigation.content);
939
+ if (updates.validationMessages) this._survey.translations.setValidationMessages(updates.validationMessages.locale, updates.validationMessages.content);
940
+ }
941
+ /**
942
+ * Get maxItemsPerPage immutably (returns a copy).
943
+ */
944
+ getMaxItemsPerPage() {
945
+ const val = this._survey.maxItemsPerPage;
946
+ return val ? { ...val } : void 0;
947
+ }
948
+ /**
949
+ * Update maxItemsPerPage.
950
+ */
951
+ updateMaxItemsPerPage(value) {
952
+ this.markAsModified();
953
+ this._survey.maxItemsPerPage = value ? { ...value } : void 0;
954
+ }
955
+ /**
956
+ * Get metadata immutably (returns a copy).
957
+ */
958
+ getMetadata() {
959
+ const val = this._survey.metadata;
960
+ return val ? { ...val } : void 0;
961
+ }
962
+ /**
963
+ * Update metadata.
964
+ */
965
+ updateMetadata(metadata) {
966
+ this.markAsModified();
967
+ this._survey.metadata = metadata ? { ...metadata } : void 0;
968
+ }
969
+ /**
970
+ * Get a template value immutably (returns a deep copy).
971
+ */
972
+ getTemplateValue(key) {
973
+ const val = this._survey.getTemplateValue(key);
974
+ return val ? structuredCloneMethod(val) : void 0;
975
+ }
976
+ /**
977
+ * Get all template values immutably (returns a new Map with deep-copied values).
978
+ */
979
+ getTemplateValues() {
980
+ const keys = this._survey.getTemplateValueKeys();
981
+ const result = /* @__PURE__ */ new Map();
982
+ for (const key of keys) {
983
+ const val = this._survey.getTemplateValue(key);
984
+ if (val) result.set(key, structuredCloneMethod(val));
985
+ }
986
+ return result;
987
+ }
988
+ /**
989
+ * Add or replace a template value.
990
+ */
991
+ setTemplateValue(key, templateValue) {
992
+ this.markAsModified();
993
+ this._survey.setTemplateValue(key, structuredCloneMethod(templateValue));
994
+ }
995
+ /**
996
+ * Remove a template value.
997
+ */
998
+ removeTemplateValue(key) {
999
+ this.markAsModified();
1000
+ this._survey.deleteTemplateValue(key);
1001
+ }
1002
+ /**
582
1003
  * Copy a survey item and all its data to clipboard format
583
- * @param itemKey - The full key of the item to copy
1004
+ * @param itemId - The ID of the item to copy
584
1005
  * @returns Clipboard data that can be serialized to JSON for clipboard
585
1006
  */
586
- copyItem(itemKey) {
587
- return new ItemCopyPaste(this._survey).copyItem(itemKey);
1007
+ copyItem(itemId) {
1008
+ return new ItemCopyPaste(this._survey).copyItem(itemId);
588
1009
  }
589
1010
  /**
590
1011
  * Paste a survey item from clipboard data to a target location
591
1012
  * @param clipboardData - The clipboard data containing the item to paste
592
1013
  * @param target - Target location where to paste the item
593
- * @returns The full key of the pasted item
1014
+ * @returns The ID of the pasted item
594
1015
  */
595
1016
  pasteItem(clipboardData, target) {
596
1017
  this.markAsModified();
@@ -599,5 +1020,5 @@ var SurveyEditor = class SurveyEditor {
599
1020
  };
600
1021
 
601
1022
  //#endregion
602
- export { CommitSource, ItemCopyPaste, SurveyEditor, SurveyEditorUndoRedo };
1023
+ export { CommitSource, ItemCopyPaste, SurveyEditor, SurveyEditorUndoRedo, buildSurveyAIContextPack };
603
1024
  //# sourceMappingURL=editor.mjs.map