@pie-players/pie-players-shared 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.
Files changed (233) hide show
  1. package/dist/config/profile.d.ts +15 -0
  2. package/dist/config/profile.d.ts.map +1 -0
  3. package/dist/config/profile.js +27 -0
  4. package/dist/config/profile.js.map +1 -0
  5. package/dist/i18n/index.d.ts +13 -0
  6. package/dist/i18n/index.d.ts.map +1 -0
  7. package/dist/i18n/index.js +12 -0
  8. package/dist/i18n/index.js.map +1 -0
  9. package/dist/i18n/loader.d.ts +36 -0
  10. package/dist/i18n/loader.d.ts.map +1 -0
  11. package/dist/i18n/loader.js +133 -0
  12. package/dist/i18n/loader.js.map +1 -0
  13. package/dist/i18n/scripts/check-coverage.d.ts +16 -0
  14. package/dist/i18n/scripts/check-coverage.d.ts.map +1 -0
  15. package/dist/i18n/scripts/check-coverage.js +262 -0
  16. package/dist/i18n/scripts/check-coverage.js.map +1 -0
  17. package/dist/i18n/scripts/scan-hardcoded.d.ts +16 -0
  18. package/dist/i18n/scripts/scan-hardcoded.d.ts.map +1 -0
  19. package/dist/i18n/scripts/scan-hardcoded.js +266 -0
  20. package/dist/i18n/scripts/scan-hardcoded.js.map +1 -0
  21. package/dist/i18n/simple-i18n.d.ts +69 -0
  22. package/dist/i18n/simple-i18n.d.ts.map +1 -0
  23. package/dist/i18n/simple-i18n.js +199 -0
  24. package/dist/i18n/simple-i18n.js.map +1 -0
  25. package/dist/i18n/translations/ar/common.json +36 -0
  26. package/dist/i18n/translations/ar/toolkit.json +48 -0
  27. package/dist/i18n/translations/ar/tools.json +109 -0
  28. package/dist/i18n/translations/en/common.json +36 -0
  29. package/dist/i18n/translations/en/toolkit.json +48 -0
  30. package/dist/i18n/translations/en/tools.json +109 -0
  31. package/dist/i18n/translations/es/common.json +36 -0
  32. package/dist/i18n/translations/es/toolkit.json +48 -0
  33. package/dist/i18n/translations/es/tools.json +109 -0
  34. package/dist/i18n/translations/zh/common.json +36 -0
  35. package/dist/i18n/translations/zh/toolkit.json +48 -0
  36. package/dist/i18n/translations/zh/tools.json +109 -0
  37. package/dist/i18n/types.d.ts +58 -0
  38. package/dist/i18n/types.d.ts.map +1 -0
  39. package/dist/i18n/types.js +8 -0
  40. package/dist/i18n/types.js.map +1 -0
  41. package/dist/i18n/use-i18n-standalone.svelte.d.ts +87 -0
  42. package/dist/i18n/use-i18n-standalone.svelte.d.ts.map +1 -0
  43. package/dist/i18n/use-i18n-standalone.svelte.js +151 -0
  44. package/dist/i18n/use-i18n-standalone.svelte.js.map +1 -0
  45. package/dist/i18n/use-i18n.svelte.d.ts +67 -0
  46. package/dist/i18n/use-i18n.svelte.d.ts.map +1 -0
  47. package/dist/i18n/use-i18n.svelte.js +144 -0
  48. package/dist/i18n/use-i18n.svelte.js.map +1 -0
  49. package/dist/index.d.ts +11 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +11 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/instrumentation/index.d.ts +53 -0
  54. package/dist/instrumentation/index.d.ts.map +1 -0
  55. package/dist/instrumentation/index.js +53 -0
  56. package/dist/instrumentation/index.js.map +1 -0
  57. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts +197 -0
  58. package/dist/instrumentation/providers/BaseInstrumentationProvider.d.ts.map +1 -0
  59. package/dist/instrumentation/providers/BaseInstrumentationProvider.js +267 -0
  60. package/dist/instrumentation/providers/BaseInstrumentationProvider.js.map +1 -0
  61. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts +106 -0
  62. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.d.ts.map +1 -0
  63. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js +182 -0
  64. package/dist/instrumentation/providers/ConsoleInstrumentationProvider.js.map +1 -0
  65. package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts +170 -0
  66. package/dist/instrumentation/providers/DataDogInstrumentationProvider.d.ts.map +1 -0
  67. package/dist/instrumentation/providers/DataDogInstrumentationProvider.js +183 -0
  68. package/dist/instrumentation/providers/DataDogInstrumentationProvider.js.map +1 -0
  69. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts +86 -0
  70. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.d.ts.map +1 -0
  71. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js +135 -0
  72. package/dist/instrumentation/providers/NewRelicInstrumentationProvider.js.map +1 -0
  73. package/dist/instrumentation/providers/index.d.ts +12 -0
  74. package/dist/instrumentation/providers/index.d.ts.map +1 -0
  75. package/dist/instrumentation/providers/index.js +12 -0
  76. package/dist/instrumentation/providers/index.js.map +1 -0
  77. package/dist/instrumentation/types.d.ts +348 -0
  78. package/dist/instrumentation/types.d.ts.map +1 -0
  79. package/dist/instrumentation/types.js +9 -0
  80. package/dist/instrumentation/types.js.map +1 -0
  81. package/dist/loader-config.d.ts +76 -0
  82. package/dist/loader-config.d.ts.map +1 -0
  83. package/dist/loader-config.js +12 -0
  84. package/dist/loader-config.js.map +1 -0
  85. package/dist/loaders/ElementLoader.d.ts +72 -0
  86. package/dist/loaders/ElementLoader.d.ts.map +1 -0
  87. package/dist/loaders/ElementLoader.js +52 -0
  88. package/dist/loaders/ElementLoader.js.map +1 -0
  89. package/dist/loaders/EsmElementLoader.d.ts +67 -0
  90. package/dist/loaders/EsmElementLoader.d.ts.map +1 -0
  91. package/dist/loaders/EsmElementLoader.js +71 -0
  92. package/dist/loaders/EsmElementLoader.js.map +1 -0
  93. package/dist/loaders/IifeElementLoader.d.ts +61 -0
  94. package/dist/loaders/IifeElementLoader.d.ts.map +1 -0
  95. package/dist/loaders/IifeElementLoader.js +63 -0
  96. package/dist/loaders/IifeElementLoader.js.map +1 -0
  97. package/dist/loaders/index.d.ts +28 -0
  98. package/dist/loaders/index.d.ts.map +1 -0
  99. package/dist/loaders/index.js +25 -0
  100. package/dist/loaders/index.js.map +1 -0
  101. package/dist/object/index.d.ts +12 -0
  102. package/dist/object/index.d.ts.map +1 -0
  103. package/dist/object/index.js +40 -0
  104. package/dist/object/index.js.map +1 -0
  105. package/dist/pie/asset-handler.d.ts +64 -0
  106. package/dist/pie/asset-handler.d.ts.map +1 -0
  107. package/dist/pie/asset-handler.js +238 -0
  108. package/dist/pie/asset-handler.js.map +1 -0
  109. package/dist/pie/component-context.d.ts +22 -0
  110. package/dist/pie/component-context.d.ts.map +1 -0
  111. package/dist/pie/component-context.js +30 -0
  112. package/dist/pie/component-context.js.map +1 -0
  113. package/dist/pie/config.d.ts +39 -0
  114. package/dist/pie/config.d.ts.map +1 -0
  115. package/dist/pie/config.js +174 -0
  116. package/dist/pie/config.js.map +1 -0
  117. package/dist/pie/configure-initialization.d.ts +35 -0
  118. package/dist/pie/configure-initialization.d.ts.map +1 -0
  119. package/dist/pie/configure-initialization.js +141 -0
  120. package/dist/pie/configure-initialization.js.map +1 -0
  121. package/dist/pie/esm-loader.d.ts +93 -0
  122. package/dist/pie/esm-loader.d.ts.map +1 -0
  123. package/dist/pie/esm-loader.js +308 -0
  124. package/dist/pie/esm-loader.js.map +1 -0
  125. package/dist/pie/iife-loader.d.ts +76 -0
  126. package/dist/pie/iife-loader.d.ts.map +1 -0
  127. package/dist/pie/iife-loader.js +303 -0
  128. package/dist/pie/iife-loader.js.map +1 -0
  129. package/dist/pie/index.d.ts +31 -0
  130. package/dist/pie/index.d.ts.map +1 -0
  131. package/dist/pie/index.js +34 -0
  132. package/dist/pie/index.js.map +1 -0
  133. package/dist/pie/initialization.d.ts +40 -0
  134. package/dist/pie/initialization.d.ts.map +1 -0
  135. package/dist/pie/initialization.js +349 -0
  136. package/dist/pie/initialization.js.map +1 -0
  137. package/dist/pie/logger.d.ts +64 -0
  138. package/dist/pie/logger.d.ts.map +1 -0
  139. package/dist/pie/logger.js +45 -0
  140. package/dist/pie/logger.js.map +1 -0
  141. package/dist/pie/math-rendering.d.ts +69 -0
  142. package/dist/pie/math-rendering.d.ts.map +1 -0
  143. package/dist/pie/math-rendering.js +98 -0
  144. package/dist/pie/math-rendering.js.map +1 -0
  145. package/dist/pie/overrides.d.ts +43 -0
  146. package/dist/pie/overrides.d.ts.map +1 -0
  147. package/dist/pie/overrides.js +146 -0
  148. package/dist/pie/overrides.js.map +1 -0
  149. package/dist/pie/player-initializer.d.ts +55 -0
  150. package/dist/pie/player-initializer.d.ts.map +1 -0
  151. package/dist/pie/player-initializer.js +123 -0
  152. package/dist/pie/player-initializer.js.map +1 -0
  153. package/dist/pie/registry.d.ts +11 -0
  154. package/dist/pie/registry.d.ts.map +1 -0
  155. package/dist/pie/registry.js +21 -0
  156. package/dist/pie/registry.js.map +1 -0
  157. package/dist/pie/resource-monitor.d.ts +208 -0
  158. package/dist/pie/resource-monitor.d.ts.map +1 -0
  159. package/dist/pie/resource-monitor.js +969 -0
  160. package/dist/pie/resource-monitor.js.map +1 -0
  161. package/dist/pie/scoring.d.ts +17 -0
  162. package/dist/pie/scoring.d.ts.map +1 -0
  163. package/dist/pie/scoring.js +84 -0
  164. package/dist/pie/scoring.js.map +1 -0
  165. package/dist/pie/types.d.ts +136 -0
  166. package/dist/pie/types.d.ts.map +1 -0
  167. package/dist/pie/types.js +52 -0
  168. package/dist/pie/types.js.map +1 -0
  169. package/dist/pie/updates.d.ts +20 -0
  170. package/dist/pie/updates.d.ts.map +1 -0
  171. package/dist/pie/updates.js +175 -0
  172. package/dist/pie/updates.js.map +1 -0
  173. package/dist/pie/use-resource-monitor.svelte.d.ts +56 -0
  174. package/dist/pie/use-resource-monitor.svelte.d.ts.map +1 -0
  175. package/dist/pie/use-resource-monitor.svelte.js +117 -0
  176. package/dist/pie/use-resource-monitor.svelte.js.map +1 -0
  177. package/dist/pie/utils.d.ts +44 -0
  178. package/dist/pie/utils.d.ts.map +1 -0
  179. package/dist/pie/utils.js +74 -0
  180. package/dist/pie/utils.js.map +1 -0
  181. package/dist/types/custom-elements.d.ts +183 -0
  182. package/dist/types/custom-elements.d.ts.map +1 -0
  183. package/dist/types/custom-elements.js +8 -0
  184. package/dist/types/custom-elements.js.map +1 -0
  185. package/dist/types/index.d.ts +761 -0
  186. package/dist/types/index.d.ts.map +1 -0
  187. package/dist/types/index.js +120 -0
  188. package/dist/types/index.js.map +1 -0
  189. package/dist/types/search.d.ts +105 -0
  190. package/dist/types/search.d.ts.map +1 -0
  191. package/dist/types/search.js +12 -0
  192. package/dist/types/search.js.map +1 -0
  193. package/dist/types/transform.d.ts +48 -0
  194. package/dist/types/transform.d.ts.map +1 -0
  195. package/dist/types/transform.js +21 -0
  196. package/dist/types/transform.js.map +1 -0
  197. package/dist/ui/focus-trap.d.ts +10 -0
  198. package/dist/ui/focus-trap.d.ts.map +1 -0
  199. package/dist/ui/focus-trap.js +30 -0
  200. package/dist/ui/focus-trap.js.map +1 -0
  201. package/dist/ui/safe-storage.d.ts +3 -0
  202. package/dist/ui/safe-storage.d.ts.map +1 -0
  203. package/dist/ui/safe-storage.js +21 -0
  204. package/dist/ui/safe-storage.js.map +1 -0
  205. package/package.json +118 -0
  206. package/src/components/PieItemPlayer.svelte +604 -0
  207. package/src/components/PiePreviewLayout.svelte +144 -0
  208. package/src/components/PiePreviewToggle.svelte +110 -0
  209. package/src/components/PieSpinner.svelte +85 -0
  210. package/src/components/ToolSettingsButton.svelte +31 -0
  211. package/src/components/ToolSettingsPanel.svelte +90 -0
  212. package/src/components/index.ts +6 -0
  213. package/src/i18n/README.md +223 -0
  214. package/src/i18n/index.ts +26 -0
  215. package/src/i18n/loader.ts +156 -0
  216. package/src/i18n/scripts/check-coverage.ts +345 -0
  217. package/src/i18n/scripts/scan-hardcoded.ts +342 -0
  218. package/src/i18n/simple-i18n.ts +236 -0
  219. package/src/i18n/translations/ar/common.json +36 -0
  220. package/src/i18n/translations/ar/toolkit.json +48 -0
  221. package/src/i18n/translations/ar/tools.json +109 -0
  222. package/src/i18n/translations/en/common.json +36 -0
  223. package/src/i18n/translations/en/toolkit.json +48 -0
  224. package/src/i18n/translations/en/tools.json +109 -0
  225. package/src/i18n/translations/es/common.json +36 -0
  226. package/src/i18n/translations/es/toolkit.json +48 -0
  227. package/src/i18n/translations/es/tools.json +109 -0
  228. package/src/i18n/translations/zh/common.json +36 -0
  229. package/src/i18n/translations/zh/toolkit.json +48 -0
  230. package/src/i18n/translations/zh/tools.json +109 -0
  231. package/src/i18n/types.ts +66 -0
  232. package/src/i18n/use-i18n-standalone.svelte.ts +184 -0
  233. package/src/i18n/use-i18n.svelte.ts +163 -0
@@ -0,0 +1,604 @@
1
+ <!--
2
+ PieItemPlayer - Pure PIE Item Renderer
3
+
4
+ This component renders a PIE item using direct PIE elements (no pie-player wrapper).
5
+ It assumes PIE bundles are already loaded via window.pie.
6
+
7
+ Uses the same reactive pattern as PieItemPreview.svelte (proven to work).
8
+ -->
9
+ <script lang="ts">
10
+ import { onDestroy, tick, untrack } from "svelte";
11
+ import type { LoaderConfig } from "../loader-config";
12
+ import { DEFAULT_LOADER_CONFIG } from "../loader-config";
13
+ import { AssetEventManager } from "../pie/asset-handler";
14
+ import { initializeConfiguresFromLoadedBundle } from "../pie/configure-initialization";
15
+ import { initializePiesFromLoadedBundle } from "../pie/initialization";
16
+ import { createPieLogger, isGlobalDebugEnabled } from "../pie/logger";
17
+ import { findPieController } from "../pie/scoring";
18
+ import type { AuthoringEnv } from "../pie/types";
19
+ import { BundleType } from "../pie/types";
20
+ import { updatePieElements } from "../pie/updates";
21
+ import { useResourceMonitor } from "../pie/use-resource-monitor.svelte";
22
+ import type {
23
+ ConfigEntity,
24
+ Env,
25
+ ImageHandler,
26
+ ModelUpdatedEvent,
27
+ SoundHandler,
28
+ } from "../types";
29
+
30
+ // Create logger (respects global debug flag - pass function for dynamic checking)
31
+ const logger = createPieLogger("pie-item-player", () =>
32
+ isGlobalDebugEnabled()
33
+ );
34
+
35
+ // Use Svelte 5 runes for props
36
+ let {
37
+ itemConfig,
38
+ passageConfig = null,
39
+ env = { mode: "gather", role: "student" } as Env,
40
+ session = [] as any[],
41
+ addCorrectResponse = false,
42
+ customClassname = "",
43
+ passageContainerClass = "",
44
+ containerClass = "",
45
+ bundleType = BundleType.player, // Default to player.js (server-processed models)
46
+ loaderConfig = DEFAULT_LOADER_CONFIG as LoaderConfig,
47
+ // Authoring mode props
48
+ mode = "view" as "view" | "author",
49
+ configuration = {} as Record<string, any>,
50
+ // Toolkit service integration (optional)
51
+ ttsService = null,
52
+ toolCoordinator = null,
53
+ highlightCoordinator = null,
54
+ // Asset handler callbacks
55
+ onInsertImage,
56
+ onDeleteImage,
57
+ onInsertSound,
58
+ onDeleteSound,
59
+ // Event callbacks (Svelte 5 pattern)
60
+ onLoadComplete,
61
+ onPlayerError,
62
+ onSessionChanged,
63
+ onModelUpdated,
64
+ }: {
65
+ itemConfig: ConfigEntity;
66
+ passageConfig?: ConfigEntity | null;
67
+ env?: Env;
68
+ session?: any[];
69
+ addCorrectResponse?: boolean;
70
+ customClassname?: string;
71
+ passageContainerClass?: string;
72
+ containerClass?: string;
73
+ bundleType?: BundleType;
74
+ loaderConfig?: LoaderConfig;
75
+ // Authoring mode props
76
+ mode?: "view" | "author";
77
+ configuration?: Record<string, any>;
78
+ // Toolkit service integration
79
+ ttsService?: any;
80
+ toolCoordinator?: any;
81
+ highlightCoordinator?: any;
82
+ // Asset handlers
83
+ onInsertImage?: (handler: ImageHandler) => void;
84
+ onDeleteImage?: (src: string, done: (err?: Error) => void) => void;
85
+ onInsertSound?: (handler: SoundHandler) => void;
86
+ onDeleteSound?: (src: string, done: (err?: Error) => void) => void;
87
+ // Event callbacks
88
+ onLoadComplete?: (detail?: any) => void;
89
+ onPlayerError?: (detail?: any) => void;
90
+ onSessionChanged?: (detail?: any) => void;
91
+ onModelUpdated?: (detail?: any) => void;
92
+ } = $props();
93
+
94
+ // Track if correct responses have been added
95
+ let correctResponsesAdded = $state(false);
96
+
97
+ // Asset event manager for authoring mode
98
+ let assetEventManager: AssetEventManager | null = $state(null);
99
+
100
+ // Transform markup for authoring mode (append -config suffix)
101
+ function transformMarkupForAuthoring(
102
+ markup: string,
103
+ elements: Record<string, string>
104
+ ): string {
105
+ let result = markup;
106
+ for (const elementTag of Object.keys(elements)) {
107
+ // Replace opening tags
108
+ const openRegex = new RegExp(`<${elementTag}(\\s|>)`, "g");
109
+ result = result.replace(openRegex, `<${elementTag}-config$1`);
110
+ // Replace closing tags
111
+ const closeRegex = new RegExp(`</${elementTag}>`, "g");
112
+ result = result.replace(closeRegex, `</${elementTag}-config>`);
113
+ }
114
+ return result;
115
+ }
116
+
117
+ // Get appropriate markup based on mode
118
+ const itemMarkup = $derived.by(() => {
119
+ if (!itemConfig?.markup) return "";
120
+ if (mode === "author" && itemConfig.elements) {
121
+ return transformMarkupForAuthoring(
122
+ itemConfig.markup,
123
+ itemConfig.elements
124
+ );
125
+ }
126
+ return itemConfig.markup;
127
+ });
128
+
129
+ const passageMarkup = $derived.by(() => {
130
+ if (!passageConfig?.markup) return "";
131
+ if (mode === "author" && passageConfig.elements) {
132
+ return transformMarkupForAuthoring(
133
+ passageConfig.markup,
134
+ passageConfig.elements
135
+ );
136
+ }
137
+ return passageConfig.markup;
138
+ });
139
+
140
+ // Dispatch events (will add more as needed)
141
+ const dispatch = (type: string, detail?: any) => {
142
+ // Call callback prop if provided (Svelte 5 pattern)
143
+ if (type === "load-complete" && onLoadComplete) {
144
+ onLoadComplete(detail);
145
+ } else if (type === "player-error" && onPlayerError) {
146
+ onPlayerError(detail);
147
+ } else if (type === "session-changed" && onSessionChanged) {
148
+ onSessionChanged(detail);
149
+ } else if (type === "model-updated" && onModelUpdated) {
150
+ onModelUpdated(detail);
151
+ }
152
+
153
+ // Also dispatch DOM event for backward compatibility
154
+ const event = new CustomEvent(type, {
155
+ detail,
156
+ bubbles: true,
157
+ composed: true, // Allow events to cross shadow DOM boundaries
158
+ });
159
+ dispatchEvent(event);
160
+ };
161
+
162
+ // Populate session with correct responses when addCorrectResponse is true
163
+ async function populateCorrectResponses(force = false) {
164
+ // Early return checks
165
+ if (!addCorrectResponse || !itemConfig || (correctResponsesAdded && !force))
166
+ return;
167
+
168
+ // CRITICAL: Only populate if env has instructor role (required for createCorrectResponseSession)
169
+ if (env.role !== "instructor" || env.mode === "evaluate") {
170
+ logger.debug(
171
+ "[PieItemPlayer] Skipping populateCorrectResponses - env not suitable (role=%s, mode=%s)",
172
+ env.role,
173
+ env.mode
174
+ );
175
+ return;
176
+ }
177
+ const newSession: any[] = [];
178
+
179
+ for (const model of itemConfig.models) {
180
+ const controller = findPieController(model.element);
181
+ logger.debug(
182
+ "[PieItemPlayer] Controller lookup for %s: %s (createCorrectResponseSession=%s)",
183
+ model.element,
184
+ controller ? "FOUND" : "NOT FOUND",
185
+ controller && controller.createCorrectResponseSession ? "YES" : "NO"
186
+ );
187
+
188
+ if (controller && controller.createCorrectResponseSession) {
189
+ try {
190
+ const correctResponse =
191
+ (await controller.createCorrectResponseSession(model, env)) as any;
192
+
193
+ // Check if we got a valid response
194
+ if (!correctResponse) {
195
+ logger.debug(
196
+ "[PieItemPlayer] createCorrectResponseSession returned null for %s (env=%j)",
197
+ model.element,
198
+ env
199
+ );
200
+ continue;
201
+ }
202
+
203
+ const { id: _ignoredId, ...sessionData } = correctResponse;
204
+ newSession.push({
205
+ id: model.id,
206
+ element: model.element,
207
+ ...sessionData,
208
+ });
209
+ } catch (e) {
210
+ logger.warn(
211
+ "[PieItemPlayer] Failed to create correct response for %s",
212
+ model.element,
213
+ e
214
+ );
215
+ }
216
+ }
217
+ }
218
+
219
+ // Clear existing session entries first by dispatching clear events for each existing entry
220
+ // This ensures the parent component clears its session state before we populate new responses
221
+ const existingIds = new Set(session.map((s: any) => s.id));
222
+ for (const id of existingIds) {
223
+ // Dispatch a session-changed event with null/empty to signal clearing
224
+ // The parent should handle this by removing the entry
225
+ dispatch("session-changed", { id, clear: true });
226
+ }
227
+
228
+ // Update session with correct responses
229
+ session.length = 0;
230
+ session.push(...newSession);
231
+
232
+ // Only mark as added if we actually got responses
233
+ if (newSession.length > 0) {
234
+ correctResponsesAdded = true;
235
+
236
+ // IMPORTANT: `session` is a plain array prop that we mutate in place.
237
+ // Svelte reactivity won't necessarily re-run effects on in-place mutation,
238
+ // so we must push the updated session into the PIE elements explicitly.
239
+ try {
240
+ updatePieElements(itemConfig, session, env, rootElement);
241
+ if (passageConfig) {
242
+ updatePieElements(passageConfig, session, env, rootElement);
243
+ }
244
+ } catch (e) {
245
+ logger.warn(
246
+ "[PieItemPlayer] Failed to update PIE elements after populating correct responses",
247
+ e
248
+ );
249
+ }
250
+
251
+ logger.debug(
252
+ "[PieItemPlayer] Correct responses added to session:",
253
+ session
254
+ );
255
+
256
+ // Dispatch session-changed events for each populated response
257
+ // This ensures the parent component can sync its session state
258
+ for (const sessionEntry of newSession) {
259
+ dispatch("session-changed", sessionEntry);
260
+ }
261
+ } else {
262
+ logger.debug(
263
+ "[PieItemPlayer] No correct responses returned (likely wrong env). Will retry if env/addCorrectResponse changes."
264
+ );
265
+ }
266
+ }
267
+
268
+ // Build CSS classes for containers using $derived
269
+ const passageContainerClassFinal = $derived(
270
+ ["pie-passage-container", customClassname, passageContainerClass]
271
+ .filter(Boolean)
272
+ .join(" ")
273
+ );
274
+
275
+ const itemContainerClassFinal = $derived(
276
+ ["pie-item-container", customClassname, containerClass]
277
+ .filter(Boolean)
278
+ .join(" ")
279
+ );
280
+
281
+ // Track if we've initialized (to avoid double-initialization)
282
+ let initialized = $state(false);
283
+
284
+ // Set up session-changed listener after DOM is ready
285
+ let sessionListenerAttached = $state(false);
286
+ let detachSessionChangedListener: (() => void) | null = $state(null);
287
+
288
+ // Flag to prevent infinite loop when re-dispatching events
289
+ let isDispatching = $state(false);
290
+
291
+ // Root element reference for resource monitor
292
+ let rootElement: HTMLElement | null = $state(null);
293
+
294
+ // Resource monitor (handles initialization and cleanup automatically)
295
+ const resourceMonitor = useResourceMonitor(
296
+ () => rootElement,
297
+ () => loaderConfig,
298
+ () => isGlobalDebugEnabled(),
299
+ "pie-item-player"
300
+ );
301
+
302
+ // Initialize PIE elements AFTER markup is rendered (reactive pattern like PieItemPreview)
303
+ $effect(() => {
304
+ if (!itemConfig || initialized) return;
305
+ logger.debug(
306
+ "[PieItemPlayer] Item config received, initializing after DOM renders..."
307
+ );
308
+ logger.debug("[PieItemPlayer] Mode:", mode);
309
+
310
+ // Wait for DOM to update (markup to render)
311
+ tick().then(async () => {
312
+ try {
313
+ logger.debug("[PieItemPlayer] DOM ready, initializing PIE elements");
314
+ logger.debug("[PieItemPlayer] Config:", {
315
+ itemElements: Object.keys(itemConfig.elements || {}),
316
+ itemModels: (itemConfig.models || []).length,
317
+ passageElements: passageConfig
318
+ ? Object.keys(passageConfig.elements || {})
319
+ : [],
320
+ passageModels: passageConfig
321
+ ? (passageConfig.models || []).length
322
+ : 0,
323
+ sessionLength: session.length,
324
+ addCorrectResponse,
325
+ env,
326
+ mode,
327
+ });
328
+
329
+ if (mode === "author") {
330
+ // AUTHORING MODE: Initialize configure elements
331
+ logger.debug("[PieItemPlayer] Initializing in authoring mode");
332
+
333
+ const authoringEnv: AuthoringEnv = {
334
+ ...env,
335
+ mode: "author",
336
+ configuration,
337
+ };
338
+
339
+ initializeConfiguresFromLoadedBundle(itemConfig, configuration, {
340
+ env: authoringEnv,
341
+ });
342
+ logger.debug("[PieItemPlayer] Configure elements initialized");
343
+
344
+ if (passageConfig) {
345
+ initializeConfiguresFromLoadedBundle(passageConfig, configuration, {
346
+ env: authoringEnv,
347
+ });
348
+ logger.debug(
349
+ "[PieItemPlayer] Passage configure elements initialized"
350
+ );
351
+ }
352
+
353
+ // Set up asset event manager if handlers provided
354
+ if (
355
+ rootElement &&
356
+ (onInsertImage || onDeleteImage || onInsertSound || onDeleteSound)
357
+ ) {
358
+ assetEventManager = new AssetEventManager(
359
+ rootElement,
360
+ onInsertImage,
361
+ onDeleteImage,
362
+ onInsertSound,
363
+ onDeleteSound
364
+ );
365
+ assetEventManager.attach();
366
+ logger.debug("[PieItemPlayer] Asset event manager attached");
367
+ }
368
+ } else {
369
+ // VIEW MODE: Initialize regular player elements
370
+ logger.debug("[PieItemPlayer] Initializing in view mode");
371
+
372
+ // STEP 1: Initialize bundles and register controllers (don't pass session yet)
373
+ // This registers controllers in the registry so we can call createCorrectResponseSession
374
+ initializePiesFromLoadedBundle(itemConfig, [], { env, bundleType, container: rootElement });
375
+ logger.debug(
376
+ "[PieItemPlayer] Item bundle initialized (bundle type: %s)",
377
+ bundleType
378
+ );
379
+
380
+ if (passageConfig) {
381
+ initializePiesFromLoadedBundle(passageConfig, [], {
382
+ env,
383
+ bundleType,
384
+ container: rootElement,
385
+ });
386
+ logger.debug(
387
+ "[PieItemPlayer] Passage bundle initialized (bundle type: %s)",
388
+ bundleType
389
+ );
390
+ }
391
+
392
+ // STEP 2: Don't populate correct responses during initialization
393
+ // Let the reactive effect handle it after initialization when env is ready
394
+
395
+ // STEP 3: Update elements with the correct session
396
+ logger.debug(
397
+ "[PieItemPlayer] Updating elements with session (length=" +
398
+ session.length +
399
+ ")"
400
+ );
401
+ updatePieElements(itemConfig, session, env, rootElement);
402
+
403
+ if (passageConfig) {
404
+ updatePieElements(passageConfig, session, env, rootElement);
405
+ }
406
+ }
407
+
408
+ initialized = true;
409
+
410
+ // Set up event listeners
411
+ if (!sessionListenerAttached) {
412
+ if (mode === "author") {
413
+ // AUTHORING MODE: Listen for model-updated events
414
+ const handleModelUpdated = (event: Event) => {
415
+ if (isDispatching) return;
416
+
417
+ const customEvent = event as ModelUpdatedEvent;
418
+ logger.debug(
419
+ "[PieItemPlayer] model-updated event received from configure element"
420
+ );
421
+
422
+ isDispatching = true;
423
+ try {
424
+ dispatch("model-updated", customEvent.detail);
425
+ } finally {
426
+ setTimeout(() => {
427
+ isDispatching = false;
428
+ }, 0);
429
+ }
430
+ };
431
+
432
+ if (rootElement) {
433
+ rootElement.addEventListener("model.updated", handleModelUpdated);
434
+ sessionListenerAttached = true;
435
+ detachSessionChangedListener = () => {
436
+ try {
437
+ rootElement?.removeEventListener(
438
+ "model.updated",
439
+ handleModelUpdated
440
+ );
441
+ } catch {}
442
+ };
443
+ logger.debug(
444
+ "[PieItemPlayer] model-updated listener attached to root element"
445
+ );
446
+ }
447
+ } else {
448
+ // VIEW MODE: Listen for session-changed events from PIE elements
449
+ const handleSessionChanged = (event: Event) => {
450
+ // CRITICAL: Prevent infinite loop
451
+ // When we dispatch, it triggers this listener again
452
+ // Use flag to detect and break the loop
453
+ if (isDispatching) {
454
+ return;
455
+ }
456
+
457
+ const customEvent = event as CustomEvent;
458
+ logger.debug(
459
+ "[PieItemPlayer] session-changed event received from PIE element",
460
+ customEvent.detail
461
+ );
462
+
463
+ // Set flag before dispatching
464
+ isDispatching = true;
465
+ try {
466
+ // Re-dispatch to parent with full session data (not just event detail)
467
+ // The event detail only contains metadata like {complete: boolean, component: string}
468
+ // The actual session data is in the session array prop (it's already the data array)
469
+ dispatch("session-changed", {
470
+ ...customEvent.detail,
471
+ session: { id: "", data: session },
472
+ });
473
+ } finally {
474
+ // Reset flag after dispatch (use setTimeout to ensure it happens after event propagation)
475
+ setTimeout(() => {
476
+ isDispatching = false;
477
+ }, 0);
478
+ }
479
+ };
480
+
481
+ // Attach to THIS component instance's root element (critical for stimulus layouts)
482
+ // Using document.querySelector would only attach to the first instance on the page.
483
+ if (rootElement) {
484
+ rootElement.addEventListener(
485
+ "session-changed",
486
+ handleSessionChanged
487
+ );
488
+ sessionListenerAttached = true;
489
+ detachSessionChangedListener = () => {
490
+ try {
491
+ rootElement?.removeEventListener(
492
+ "session-changed",
493
+ handleSessionChanged
494
+ );
495
+ } catch {}
496
+ };
497
+ logger.debug(
498
+ "[PieItemPlayer] session-changed listener attached to root element"
499
+ );
500
+ }
501
+ }
502
+ }
503
+
504
+ // Note: Resource monitor starts automatically via useResourceMonitor when rootElement is set
505
+
506
+ logger.debug(
507
+ "[PieItemPlayer] Initialization complete, dispatching load-complete event"
508
+ );
509
+ dispatch("load-complete");
510
+ } catch (e: any) {
511
+ logger.error("[PieItemPlayer] Error initializing:", e);
512
+ dispatch("player-error", e.message);
513
+ }
514
+ });
515
+ });
516
+
517
+ onDestroy(() => {
518
+ try {
519
+ detachSessionChangedListener?.();
520
+ assetEventManager?.detach();
521
+ } catch {}
522
+ });
523
+
524
+ // React to addCorrectResponse and env changes to populate/clear correct responses
525
+ $effect(() => {
526
+ if (!initialized) return;
527
+
528
+ // Read env to ensure effect tracks it (so it re-runs when env changes)
529
+ const currentEnv = env;
530
+
531
+ if (addCorrectResponse && !correctResponsesAdded) {
532
+ // Need to populate correct responses
533
+ // populateCorrectResponses will check if env is suitable (role === 'instructor')
534
+ untrack(async () => {
535
+ await populateCorrectResponses();
536
+ // Elements will be updated by the env/session effect below
537
+ });
538
+ } else if (!addCorrectResponse && correctResponsesAdded) {
539
+ // Switching FROM browse mode - clear correct responses
540
+ untrack(() => {
541
+ session.length = 0;
542
+ correctResponsesAdded = false;
543
+ // Elements will be updated by the env/session effect below
544
+ });
545
+ }
546
+ });
547
+
548
+ // Update PIE elements when env or session changes (after initialization) - using $effect
549
+ let isUpdating = false;
550
+ $effect(() => {
551
+ if (!initialized || !env || !itemConfig || !session) return;
552
+ if (isUpdating) return; // Prevent re-entry
553
+
554
+ // Log changes
555
+ logger.debug("[PieItemPlayer] Dependencies changed, updating elements");
556
+ logger.debug("[PieItemPlayer] Env:", env);
557
+ logger.debug(
558
+ "[PieItemPlayer] Session (length=" + session.length + "):",
559
+ session
560
+ );
561
+
562
+ isUpdating = true;
563
+ untrack(() => {
564
+ try {
565
+ updatePieElements(itemConfig, session, env, rootElement);
566
+
567
+ if (passageConfig) {
568
+ updatePieElements(passageConfig, session, env, rootElement);
569
+ }
570
+ } finally {
571
+ isUpdating = false;
572
+ }
573
+ });
574
+ });
575
+
576
+ // Note: Resource monitor cleanup is handled automatically by useResourceMonitor's onDestroy
577
+ </script>
578
+
579
+ <div class="pie-item-player" bind:this={rootElement}>
580
+ {#if passageMarkup}
581
+ <div class={passageContainerClassFinal}>
582
+ {@html passageMarkup}
583
+ </div>
584
+ {/if}
585
+
586
+ {#if itemMarkup}
587
+ <div class={itemContainerClassFinal}>
588
+ {@html itemMarkup}
589
+ </div>
590
+ {/if}
591
+ </div>
592
+
593
+ <style>
594
+ .pie-item-player {
595
+ display: block;
596
+ width: 100%;
597
+ }
598
+
599
+ .pie-passage-container,
600
+ .pie-item-container {
601
+ display: block;
602
+ width: 100%;
603
+ }
604
+ </style>