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