@cognior/iap-sdk 0.1.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.

Potentially problematic release.


This version of @cognior/iap-sdk might be problematic. Click here for more details.

Files changed (60) hide show
  1. package/.github/copilot-instructions.md +95 -0
  2. package/README.md +79 -0
  3. package/TRACKING.md +105 -0
  4. package/USER_CONTEXT_README.md +284 -0
  5. package/package.json +154 -0
  6. package/src/config.ts +25 -0
  7. package/src/core/flowEngine.ts +1833 -0
  8. package/src/core/triggerManager.ts +1011 -0
  9. package/src/experiences/banner.ts +366 -0
  10. package/src/experiences/beacon.ts +668 -0
  11. package/src/experiences/hotspotTour.ts +654 -0
  12. package/src/experiences/hotspots.ts +566 -0
  13. package/src/experiences/modal.ts +1337 -0
  14. package/src/experiences/modalSequence.ts +1247 -0
  15. package/src/experiences/popover.ts +652 -0
  16. package/src/experiences/registry.ts +21 -0
  17. package/src/experiences/survey.ts +1639 -0
  18. package/src/experiences/taskList.ts +625 -0
  19. package/src/experiences/tooltip.ts +740 -0
  20. package/src/experiences/types.ts +395 -0
  21. package/src/experiences/walkthrough.ts +670 -0
  22. package/src/flow-sequence.ts +177 -0
  23. package/src/flows.ts +512 -0
  24. package/src/http.ts +61 -0
  25. package/src/index.ts +355 -0
  26. package/src/services/flowManager.ts +905 -0
  27. package/src/services/flowNormalizer.ts +74 -0
  28. package/src/services/locationContextService.ts +189 -0
  29. package/src/services/pageContextService.ts +221 -0
  30. package/src/services/userContextService.ts +286 -0
  31. package/src/state/appState.ts +0 -0
  32. package/src/state/hooks.ts +0 -0
  33. package/src/state/index.ts +0 -0
  34. package/src/state/migration.ts +0 -0
  35. package/src/state/store.ts +0 -0
  36. package/src/styles/banner.css.ts +0 -0
  37. package/src/styles/hotspot.css.ts +0 -0
  38. package/src/styles/hotspotTour.css.ts +0 -0
  39. package/src/styles/modal.css.ts +564 -0
  40. package/src/styles/survey.css.ts +1013 -0
  41. package/src/styles/taskList.css.ts +0 -0
  42. package/src/styles/tooltip.css.ts +149 -0
  43. package/src/styles/walkthrough.css.ts +0 -0
  44. package/src/tourUtils.ts +0 -0
  45. package/src/tracking.ts +223 -0
  46. package/src/utils/debounce.ts +66 -0
  47. package/src/utils/eventSequenceValidator.ts +124 -0
  48. package/src/utils/flowTrackingSystem.ts +524 -0
  49. package/src/utils/idGenerator.ts +155 -0
  50. package/src/utils/immediateValidationPrevention.ts +184 -0
  51. package/src/utils/normalize.ts +50 -0
  52. package/src/utils/privacyManager.ts +166 -0
  53. package/src/utils/ruleEvaluator.ts +199 -0
  54. package/src/utils/sanitize.ts +79 -0
  55. package/src/utils/selectors.ts +107 -0
  56. package/src/utils/stepExecutor.ts +345 -0
  57. package/src/utils/triggerNormalizer.ts +149 -0
  58. package/src/utils/validationInterceptor.ts +650 -0
  59. package/tsconfig.json +13 -0
  60. package/tsup.config.ts +13 -0
@@ -0,0 +1,1337 @@
1
+ // src/experiences/modal.ts
2
+ // Simple modal experience renderer
3
+
4
+ import { sanitizeHtml } from "../utils/sanitize";
5
+ import { register } from "./registry";
6
+ import { modalCssText } from "../styles/modal.css";
7
+ import type { ModalPayload, ModalContent, ModalSize } from "./types";
8
+
9
+ type ModalFlow = { id: string; type: "modal"; payload: ModalPayload };
10
+
11
+ export function registerModal() {
12
+ register("modal", renderModal);
13
+ }
14
+
15
+ export async function renderModal(flow: ModalFlow): Promise<void> {
16
+ const { payload, id } = flow;
17
+
18
+ console.debug("[DAP] renderModal called with payload:", payload);
19
+ console.debug("[DAP] Modal flow ID:", id);
20
+
21
+ // Extract completion tracker
22
+ const completionTracker = payload._completionTracker;
23
+
24
+ // Ensure CSS is injected
25
+ ensureStyles();
26
+
27
+ // Create modal elements
28
+ const { overlay, modal, header, body, footer } = createModalElements(payload);
29
+
30
+ // Add to DOM
31
+ document.documentElement.appendChild(overlay);
32
+
33
+ // Focus management
34
+ const prevActive = document.activeElement as HTMLElement | null;
35
+ modal.setAttribute("role", "dialog");
36
+ modal.setAttribute("aria-modal", "true");
37
+ modal.setAttribute("aria-labelledby", "modal-title");
38
+
39
+ // Enhanced accessibility setup
40
+ setupModalAccessibility(modal, overlay);
41
+
42
+ // Event handlers
43
+ function closeModal() {
44
+ // Cleanup accessibility handlers
45
+ if ((modal as any)._accessibilityCleanup) {
46
+ (modal as any)._accessibilityCleanup();
47
+ }
48
+
49
+ overlay.style.animation = "modalFadeOut 0.2s ease-in";
50
+ modal.style.animation = "modalSlideOut 0.2s ease-in";
51
+
52
+ setTimeout(() => {
53
+ overlay.remove();
54
+ prevActive?.focus();
55
+
56
+ // Signal completion
57
+ if (completionTracker?.onComplete) {
58
+ console.debug(`[DAP] Completing modal flow: ${id}`);
59
+ completionTracker.onComplete();
60
+ }
61
+ }, 200);
62
+ }
63
+
64
+ // Close on overlay click
65
+ overlay.addEventListener("click", (e) => {
66
+ if (e.target === overlay) {
67
+ closeModal();
68
+ }
69
+ });
70
+
71
+ // Close button
72
+ const closeBtn = modal.querySelector(".dap-modal-close") as HTMLButtonElement;
73
+ if (closeBtn) {
74
+ closeBtn.addEventListener("click", closeModal);
75
+ }
76
+
77
+ // Escape key
78
+ function handleKeyboard(e: KeyboardEvent) {
79
+ if (e.key === "Escape") {
80
+ closeModal();
81
+ document.removeEventListener("keydown", handleKeyboard);
82
+ }
83
+ }
84
+ document.addEventListener("keydown", handleKeyboard);
85
+
86
+ // Add exit animations
87
+ if (!document.getElementById("dap-modal-exit-styles")) {
88
+ const style = document.createElement("style");
89
+ style.id = "dap-modal-exit-styles";
90
+ style.textContent = `
91
+ @keyframes modalFadeOut {
92
+ from { opacity: 1; backdrop-filter: blur(4px); }
93
+ to { opacity: 0; backdrop-filter: blur(0px); }
94
+ }
95
+ @keyframes modalSlideOut {
96
+ from { opacity: 1; transform: scale(1) translateY(0); }
97
+ to { opacity: 0; transform: scale(0.95) translateY(-10px); }
98
+ }
99
+ `;
100
+ document.head.appendChild(style);
101
+ }
102
+
103
+ // Enterprise dragging functionality
104
+ setupModalDragging(modal, header, overlay);
105
+
106
+ // Media lifecycle management
107
+ setupMediaHandling(modal, overlay);
108
+ }
109
+
110
+ function setupModalDragging(modal: HTMLElement, header: HTMLElement, overlay: HTMLElement): void {
111
+ let isDragging = false;
112
+ let dragStartX = 0;
113
+ let dragStartY = 0;
114
+ let modalStartX = 0;
115
+ let modalStartY = 0;
116
+
117
+ // Make header the drag handle
118
+ header.style.cursor = "move";
119
+
120
+ const startDrag = (e: MouseEvent) => {
121
+ isDragging = true;
122
+ dragStartX = e.clientX;
123
+ dragStartY = e.clientY;
124
+
125
+ const rect = modal.getBoundingClientRect();
126
+ modalStartX = rect.left;
127
+ modalStartY = rect.top;
128
+
129
+ header.classList.add("dragging");
130
+ overlay.classList.add("dragging");
131
+
132
+ document.addEventListener("mousemove", drag);
133
+ document.addEventListener("mouseup", endDrag);
134
+ e.preventDefault();
135
+ };
136
+
137
+ const drag = (e: MouseEvent) => {
138
+ if (!isDragging) return;
139
+
140
+ const deltaX = e.clientX - dragStartX;
141
+ const deltaY = e.clientY - dragStartY;
142
+
143
+ let newX = modalStartX + deltaX;
144
+ let newY = modalStartY + deltaY;
145
+
146
+ // Constrain to viewport with 10px margin
147
+ const modalRect = modal.getBoundingClientRect();
148
+ const viewport = {
149
+ width: window.innerWidth,
150
+ height: window.innerHeight
151
+ };
152
+
153
+ newX = Math.max(10, Math.min(newX, viewport.width - modalRect.width - 10));
154
+ newY = Math.max(10, Math.min(newY, viewport.height - modalRect.height - 10));
155
+
156
+ // Position modal
157
+ modal.style.position = "fixed";
158
+ modal.style.left = `${newX}px`;
159
+ modal.style.top = `${newY}px`;
160
+ modal.style.transform = "none";
161
+ };
162
+
163
+ const endDrag = () => {
164
+ isDragging = false;
165
+ header.classList.remove("dragging");
166
+ overlay.classList.remove("dragging");
167
+
168
+ document.removeEventListener("mousemove", drag);
169
+ document.removeEventListener("mouseup", endDrag);
170
+ };
171
+
172
+ header.addEventListener("mousedown", startDrag);
173
+ }
174
+
175
+ function setupModalAccessibility(modal: HTMLElement, overlay: HTMLElement): void {
176
+ // Enhanced focus management - Focus trap
177
+ const focusableElements = modal.querySelectorAll(
178
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
179
+ ) as NodeListOf<HTMLElement>;
180
+
181
+ const firstFocusable = focusableElements[0];
182
+ const lastFocusable = focusableElements[focusableElements.length - 1];
183
+
184
+ // Initial focus to first focusable element
185
+ if (firstFocusable) {
186
+ firstFocusable.focus();
187
+ }
188
+
189
+ // Focus trap on Tab/Shift+Tab
190
+ const handleTabKey = (e: KeyboardEvent) => {
191
+ if (e.key === "Tab") {
192
+ if (e.shiftKey) {
193
+ // Shift + Tab - focus previous
194
+ if (document.activeElement === firstFocusable) {
195
+ e.preventDefault();
196
+ lastFocusable?.focus();
197
+ }
198
+ } else {
199
+ // Tab - focus next
200
+ if (document.activeElement === lastFocusable) {
201
+ e.preventDefault();
202
+ firstFocusable?.focus();
203
+ }
204
+ }
205
+ }
206
+ };
207
+
208
+ modal.addEventListener("keydown", handleTabKey);
209
+
210
+ // Store cleanup function
211
+ (modal as any)._accessibilityCleanup = () => {
212
+ modal.removeEventListener("keydown", handleTabKey);
213
+ };
214
+ }
215
+
216
+ function setupMediaHandling(modal: HTMLElement, overlay: HTMLElement): void {
217
+ const videos = modal.querySelectorAll("video") as NodeListOf<HTMLVideoElement>;
218
+ const pausedVideos: HTMLVideoElement[] = [];
219
+
220
+ // Store original overlay.remove to intercept it
221
+ const originalRemove = overlay.remove.bind(overlay);
222
+
223
+ overlay.remove = () => {
224
+ // Pause all videos and track which ones were playing
225
+ videos.forEach((video) => {
226
+ if (!video.paused) {
227
+ video.pause();
228
+ pausedVideos.push(video);
229
+ }
230
+ });
231
+
232
+ // Call original remove
233
+ originalRemove();
234
+
235
+ // Resume videos after a short delay (if they're still in DOM)
236
+ setTimeout(() => {
237
+ pausedVideos.forEach((video) => {
238
+ if (document.contains(video)) {
239
+ video.play().catch(() => {
240
+ // Ignore autoplay policy errors
241
+ });
242
+ }
243
+ });
244
+ }, 100);
245
+ };
246
+ }
247
+
248
+ function createModalElements(payload: ModalPayload) {
249
+ console.log("[DAP] Creating modal elements with payload:", payload);
250
+ const overlay = document.createElement("div");
251
+ overlay.className = "dap-modal-overlay";
252
+
253
+ const modal = document.createElement("div");
254
+ modal.className = "dap-modal";
255
+
256
+ // Apply size class if specified
257
+ if (payload.size) {
258
+ modal.classList.add(`dap-modal-${payload.size}`);
259
+ } else {
260
+ modal.classList.add("dap-modal-medium"); // Default to medium size
261
+ }
262
+
263
+ // Header - always include header for consistent layout
264
+ const header = document.createElement("div");
265
+ header.className = "dap-modal-header";
266
+
267
+ // Title (if provided)
268
+ if (payload.title) {
269
+ const title = document.createElement("h2");
270
+ title.className = "dap-modal-title";
271
+ title.id = "modal-title";
272
+ title.textContent = payload.title;
273
+ header.appendChild(title);
274
+ } else {
275
+ // Add empty space to maintain header layout
276
+ const title = document.createElement("h2");
277
+ title.className = "dap-modal-title";
278
+ title.id = "modal-title";
279
+ title.style.visibility = "hidden";
280
+ title.innerHTML = "&nbsp;";
281
+ header.appendChild(title);
282
+ }
283
+
284
+ // Close button - always present
285
+ const closeBtn = document.createElement("button");
286
+ closeBtn.className = "dap-modal-close";
287
+ closeBtn.setAttribute("aria-label", "Close modal");
288
+ closeBtn.innerHTML = "×";
289
+ header.appendChild(closeBtn);
290
+
291
+ // Body
292
+ const body = document.createElement("div");
293
+ body.className = "dap-modal-body";
294
+
295
+ console.debug("[DAP] Processing modal body:", payload.body);
296
+ console.debug("[DAP] Body type:", typeof payload.body);
297
+ console.debug("[DAP] Is body array:", Array.isArray(payload.body));
298
+
299
+ if (payload.body && Array.isArray(payload.body)) {
300
+ payload.body.forEach((content, index) => {
301
+ console.debug(`[DAP] Processing body content ${index}:`, content);
302
+ const contentEl = renderModalContent(content);
303
+ if (contentEl) body.appendChild(contentEl);
304
+ });
305
+ } else if (payload.body) {
306
+ console.warn("[DAP] Body is not an array:", payload.body);
307
+ // Handle non-array body content
308
+ const textEl = document.createElement("div");
309
+ textEl.className = "dap-content-text";
310
+ textEl.textContent = String(payload.body);
311
+ body.appendChild(textEl);
312
+ }
313
+
314
+ // Footer - always include for consistent layout
315
+ const footer = document.createElement("div");
316
+ footer.className = "dap-modal-footer";
317
+
318
+ if (payload.footerText) {
319
+ const footerText = document.createElement("p");
320
+ footerText.className = "dap-footer-text";
321
+ footerText.innerHTML = sanitizeHtml(payload.footerText);
322
+ footer.appendChild(footerText);
323
+ }
324
+
325
+ // Assemble modal - always include header and footer
326
+ modal.appendChild(header);
327
+ modal.appendChild(body);
328
+ modal.appendChild(footer);
329
+ overlay.appendChild(modal);
330
+
331
+ return { overlay, modal, header, body, footer };
332
+ }
333
+
334
+ function renderModalContent(content: ModalContent): HTMLElement | null {
335
+ console.log("[DAP] Rendering modal content with kind:", content.kind);
336
+ switch (content.kind) {
337
+ case "text":
338
+ const textEl = document.createElement("div");
339
+ textEl.className = "dap-content-text";
340
+ textEl.innerHTML = sanitizeHtml(content.html);
341
+ return textEl;
342
+
343
+ case "link":
344
+ const linkEl = document.createElement("a");
345
+ linkEl.className = "dap-content-link";
346
+ linkEl.href = content.href;
347
+ linkEl.textContent = content.label || content.href;
348
+ linkEl.target = "_blank";
349
+ linkEl.rel = "noopener noreferrer";
350
+ return linkEl;
351
+
352
+ case "image":
353
+ const imgEl = document.createElement("img");
354
+ imgEl.className = "dap-content-image";
355
+ imgEl.src = content.url;
356
+ imgEl.alt = content.alt || "";
357
+ return imgEl;
358
+
359
+ case "video":
360
+ if (content.sources && content.sources.length > 0) {
361
+ const videoEl = document.createElement("video");
362
+ videoEl.className = "dap-content-video";
363
+ videoEl.controls = true;
364
+
365
+ content.sources.forEach(source => {
366
+ const sourceEl = document.createElement("source");
367
+ sourceEl.src = source.src;
368
+ if (source.type) sourceEl.type = source.type;
369
+ videoEl.appendChild(sourceEl);
370
+ });
371
+
372
+ return videoEl;
373
+ }
374
+ return null;
375
+
376
+ case "youtube":
377
+ const iframeEl = document.createElement("iframe");
378
+ iframeEl.className = "dap-content-youtube";
379
+ iframeEl.src = content.href;
380
+ iframeEl.setAttribute("frameborder", "0");
381
+ iframeEl.setAttribute("allowfullscreen", "true");
382
+ return iframeEl;
383
+
384
+ case "kb":
385
+ // Knowledge base rendering with in-modal viewing
386
+ console.debug("[DAP] Rendering KB content:", content);
387
+ return renderKnowledgeBase(content);
388
+
389
+ case "kb-item-viewer":
390
+ // Knowledge base item viewer
391
+ console.debug("[DAP] Rendering KB item viewer:", content);
392
+ return renderKBItemViewer(content);
393
+
394
+ case "article":
395
+ // Article content rendering with inline viewing
396
+ console.debug("[DAP] Rendering Article content:", content);
397
+ console.debug("[DAP] Detected MIME type:", content.mime);
398
+ return createArticleViewer(content);
399
+
400
+ default:
401
+ console.warn("[DAP] Unknown content kind:", (content as any)?.kind);
402
+ return null;
403
+ }
404
+ }
405
+
406
+ function renderKnowledgeBase(content: any): HTMLElement {
407
+ const kbEl = document.createElement("div");
408
+ kbEl.className = "dap-content-kb";
409
+
410
+ // Initialize KB state when rendering list for the first time
411
+ if (!kbState || kbState.view === "item") {
412
+ console.debug("[DAP] Initializing KB state with items:", content.items);
413
+ kbState = {
414
+ view: "list",
415
+ items: content.items || [],
416
+ selectedItem: null,
417
+ title: content.title || "Knowledge Base",
418
+ modalBodyRef: null
419
+ };
420
+ console.debug("[DAP] KB state initialized, items count:", kbState.items.length);
421
+ }
422
+
423
+ if (content.title) {
424
+ const title = document.createElement("h3");
425
+ title.textContent = content.title;
426
+ kbEl.appendChild(title);
427
+ }
428
+
429
+ if (content.items && Array.isArray(content.items)) {
430
+ console.debug("[DAP] Processing KB items:", content.items);
431
+
432
+ content.items.forEach((item: any, index: number) => {
433
+ console.debug(`[DAP] Processing KB item ${index}:`, item);
434
+
435
+ const itemEl = document.createElement("div");
436
+ itemEl.className = "dap-kb-item";
437
+
438
+ // Handle KBItem structure from flows.ts
439
+ let itemUrl = "";
440
+ let itemTitle = "";
441
+ let itemDescription = "";
442
+ let itemType = "";
443
+
444
+ if (typeof item === "string") {
445
+ // If item is just a string, treat it as URL
446
+ itemUrl = item;
447
+ itemTitle = item;
448
+ itemType = "link";
449
+ } else if (item && typeof item === "object") {
450
+ // Handle KBItem structure with proper property access
451
+ const kbItem = item as any;
452
+ itemUrl = kbItem.url || "";
453
+ itemTitle = kbItem.title || "";
454
+ itemDescription = kbItem.description || "";
455
+ itemType = kbItem.itemType || detectContentType(itemUrl, kbItem.fileName);
456
+
457
+ console.debug(`[DAP] Extracted: url=${itemUrl}, title=${itemTitle}, description=${itemDescription}, type=${itemType}`);
458
+ } else {
459
+ console.warn("[DAP] Invalid KB item structure:", item);
460
+ return; // Skip this item
461
+ }
462
+
463
+ if (!itemUrl || !itemTitle) {
464
+ console.warn("[DAP] KB item missing required fields (url or title), skipping:", item);
465
+ return; // Skip items without required fields
466
+ }
467
+
468
+ // Create clickable item (NO external navigation)
469
+ const button = document.createElement("button");
470
+ button.className = "dap-kb-item-button dap-modal-button primary";
471
+ button.textContent = itemTitle;
472
+ button.title = itemDescription || itemTitle;
473
+
474
+ // Add content type icon
475
+ const icon = document.createElement("span");
476
+ icon.className = `dap-kb-icon dap-kb-icon-${itemType}`;
477
+ button.insertBefore(icon, button.firstChild);
478
+
479
+ // In-modal viewing click handler
480
+ button.addEventListener("click", (e) => {
481
+ e.preventDefault();
482
+ e.stopPropagation();
483
+ openKBItemInModal(item, content.title || "Knowledge Base");
484
+ });
485
+
486
+ itemEl.appendChild(button);
487
+
488
+ // Add description if available
489
+ if (itemDescription) {
490
+ const desc = document.createElement("p");
491
+ desc.className = "dap-kb-description";
492
+ desc.textContent = itemDescription;
493
+ itemEl.appendChild(desc);
494
+ }
495
+
496
+ kbEl.appendChild(itemEl);
497
+ });
498
+ } else {
499
+ console.warn("[DAP] KB content has no items or items is not an array:", content);
500
+
501
+ // Add fallback message
502
+ const noItemsMsg = document.createElement("p");
503
+ noItemsMsg.className = "dap-kb-no-items";
504
+ noItemsMsg.textContent = "No knowledge base items available.";
505
+ kbEl.appendChild(noItemsMsg);
506
+ }
507
+
508
+ return kbEl;
509
+ }
510
+
511
+ function renderKBItemViewer(content: any): HTMLElement {
512
+ const viewerEl = document.createElement("div");
513
+ viewerEl.className = "dap-kb-item-viewer";
514
+
515
+ // Back navigation header
516
+ const headerEl = document.createElement("div");
517
+ headerEl.className = "dap-kb-viewer-header";
518
+
519
+ const backBtn = document.createElement("button");
520
+ backBtn.className = "dap-kb-back-button dap-modal-button outline";
521
+ backBtn.innerHTML = "← Back to " + (content.kbTitle || "Knowledge Base");
522
+ backBtn.addEventListener("click", () => {
523
+ goBackToKBList();
524
+ });
525
+ headerEl.appendChild(backBtn);
526
+
527
+ // File type badge
528
+ const itemType = content.item.itemType || detectContentType(content.item.url, content.item.fileName);
529
+ const badge = createFileTypeBadge(itemType, content.item.fileName);
530
+ headerEl.appendChild(badge);
531
+
532
+ // Item title
533
+ const title = document.createElement("h3");
534
+ title.className = "dap-kb-item-title";
535
+ title.textContent = content.item.title || "Content";
536
+ headerEl.appendChild(title);
537
+
538
+ // File metadata
539
+ if (content.item.fileName) {
540
+ const fileInfo = document.createElement("p");
541
+ fileInfo.className = "dap-file-metadata";
542
+ fileInfo.innerHTML = `<strong>File:</strong> ${content.item.fileName}`;
543
+ headerEl.appendChild(fileInfo);
544
+ }
545
+
546
+ viewerEl.appendChild(headerEl);
547
+
548
+ // Content viewer
549
+ const contentEl = renderKBItemContent(content.item);
550
+ if (contentEl) {
551
+ viewerEl.appendChild(contentEl);
552
+ }
553
+
554
+ return viewerEl;
555
+ }
556
+
557
+ function renderKBItemContent(item: any): HTMLElement | null {
558
+ const itemType = item.itemType || detectContentType(item.url, item.fileName);
559
+ console.debug("[DAP] Rendering KB item content, type:", itemType, "url:", item.url);
560
+
561
+ switch (itemType) {
562
+ case "video":
563
+ return createVideoViewer(item.url);
564
+
565
+ case "image":
566
+ return createImageViewer(item.url, item.title);
567
+
568
+ case "pdf":
569
+ return createPDFViewer(item.url, item.fileName);
570
+
571
+ case "article":
572
+ return createArticleViewer(item);
573
+
574
+ case "doc":
575
+ case "docx":
576
+ return createDocumentViewer(item.url, item.fileName, itemType);
577
+
578
+ case "youtube":
579
+ return createYouTubeViewer(item.url);
580
+
581
+ case "link":
582
+ default:
583
+ return createLinkViewer(item.url, item.title, item.description);
584
+ }
585
+ }
586
+
587
+ function createVideoViewer(url: string): HTMLElement {
588
+ const videoEl = document.createElement("video");
589
+ videoEl.className = "dap-kb-video";
590
+ videoEl.controls = true;
591
+ videoEl.preload = "metadata";
592
+ videoEl.style.width = "100%";
593
+ videoEl.style.maxHeight = "400px";
594
+
595
+ const source = document.createElement("source");
596
+ source.src = url;
597
+ videoEl.appendChild(source);
598
+
599
+ return videoEl;
600
+ }
601
+
602
+ function createImageViewer(url: string, alt?: string): HTMLElement {
603
+ const imgEl = document.createElement("img");
604
+ imgEl.className = "dap-kb-image";
605
+ imgEl.src = url;
606
+ imgEl.alt = alt || "";
607
+ imgEl.style.width = "100%";
608
+ imgEl.style.maxHeight = "500px";
609
+ imgEl.style.objectFit = "contain";
610
+
611
+ return imgEl;
612
+ }
613
+
614
+ function createPDFViewer(url: string, fileName?: string): HTMLElement {
615
+ const container = document.createElement("div");
616
+ container.className = "dap-kb-pdf-container";
617
+
618
+ // Try embedded PDF viewer first
619
+ const iframe = document.createElement("iframe");
620
+ iframe.className = "dap-kb-pdf-iframe";
621
+ iframe.src = url;
622
+ iframe.style.width = "100%";
623
+ iframe.style.height = "500px";
624
+ iframe.style.border = "1px solid #ddd";
625
+
626
+ // Fallback controls
627
+ const fallback = document.createElement("div");
628
+ fallback.className = "dap-kb-pdf-fallback";
629
+ fallback.innerHTML = `
630
+ <p>PDF preview not available in this browser.</p>
631
+ <button class="dap-kb-download-btn dap-modal-button secondary" onclick="window.open('${url}', '_blank')">
632
+ Open PDF in New Tab
633
+ </button>
634
+ `;
635
+
636
+ // Error handling
637
+ iframe.addEventListener("error", () => {
638
+ iframe.style.display = "none";
639
+ fallback.style.display = "block";
640
+ });
641
+
642
+ container.appendChild(iframe);
643
+ container.appendChild(fallback);
644
+
645
+ return container;
646
+ }
647
+
648
+ // Centralized Article Viewer Resolver - determines appropriate viewer based on MIME type and file extension
649
+ function resolveArticleViewer(articleContent: any): { viewer: string; mimeType: string | null } {
650
+ console.debug("[DAP] Resolving Article viewer for:", articleContent);
651
+
652
+ const url = articleContent.url || articleContent.presignedUrl || "";
653
+ const mimeType = articleContent.mime || articleContent.mimeType || null;
654
+ const fileName = articleContent.fileName || "";
655
+
656
+ console.debug("[DAP] Detected MIME type:", mimeType || "none");
657
+ console.debug("[DAP] File URL:", url);
658
+ console.debug("[DAP] File name:", fileName);
659
+
660
+ // Check MIME type first (preferred)
661
+ if (mimeType) {
662
+ if (mimeType === "application/pdf") {
663
+ console.debug("[DAP] Selected viewer: pdf");
664
+ return { viewer: "pdf", mimeType };
665
+ }
666
+ if (mimeType.includes("word") || mimeType.includes("msword") ||
667
+ mimeType === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
668
+ console.debug("[DAP] Selected viewer: document");
669
+ return { viewer: "document", mimeType };
670
+ }
671
+ if (mimeType.includes("presentation") || mimeType.includes("powerpoint") ||
672
+ mimeType === "application/vnd.openxmlformats-officedocument.presentationml.presentation") {
673
+ console.debug("[DAP] Selected viewer: presentation");
674
+ return { viewer: "presentation", mimeType };
675
+ }
676
+ if (mimeType === "text/html" || mimeType.includes("text/")) {
677
+ console.debug("[DAP] Selected viewer: web");
678
+ return { viewer: "web", mimeType };
679
+ }
680
+ }
681
+
682
+ // Fallback to file extension detection
683
+ const urlLower = url.toLowerCase();
684
+ const fileNameLower = fileName.toLowerCase();
685
+
686
+ if (urlLower.includes(".pdf") || fileNameLower.endsWith(".pdf")) {
687
+ console.debug("[DAP] Selected viewer: pdf (by extension)");
688
+ return { viewer: "pdf", mimeType: "application/pdf" };
689
+ }
690
+ if (urlLower.match(/\.(doc|docx)/) || fileNameLower.match(/\.(doc|docx)$/)) {
691
+ console.debug("[DAP] Selected viewer: document (by extension)");
692
+ return { viewer: "document", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" };
693
+ }
694
+ if (urlLower.match(/\.(ppt|pptx)/) || fileNameLower.match(/\.(ppt|pptx)$/)) {
695
+ console.debug("[DAP] Selected viewer: presentation (by extension)");
696
+ return { viewer: "presentation", mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation" };
697
+ }
698
+ if (urlLower.match(/\.(html?|htm)/) || fileNameLower.match(/\.(html?|htm)$/)) {
699
+ console.debug("[DAP] Selected viewer: web (by extension)");
700
+ return { viewer: "web", mimeType: "text/html" };
701
+ }
702
+
703
+ // Check URL patterns for web content
704
+ if (url && (url.startsWith('http://') || url.startsWith('https://'))) {
705
+ // If it's a web URL and doesn't have a specific file extension, treat as web content
706
+ if (!urlLower.match(/\.(pdf|doc|docx|ppt|pptx|xls|xlsx|zip|rar)$/)) {
707
+ console.debug("[DAP] Selected viewer: web (by URL pattern)");
708
+ return { viewer: "web", mimeType: "text/html" };
709
+ }
710
+ }
711
+
712
+ console.debug("[DAP] Selected viewer: fallback");
713
+ return { viewer: "fallback", mimeType: mimeType };
714
+ }
715
+
716
+ // Create Article Viewer - handles all document-based assets with inline preview and fallback
717
+ function createArticleViewer(articleContent: any): HTMLElement {
718
+ console.debug("[DAP] Rendering Article");
719
+
720
+ const container = document.createElement("div");
721
+ container.className = "dap-kb-article-viewer";
722
+
723
+ const url = articleContent.url || articleContent.presignedUrl || "";
724
+ const fileName = articleContent.fileName || "Document";
725
+ const title = articleContent.title || fileName;
726
+ const description = articleContent.description || "";
727
+ const content = articleContent.content || "";
728
+
729
+ // Add title
730
+ const titleEl = document.createElement("h4");
731
+ titleEl.className = "dap-article-title";
732
+ titleEl.textContent = title;
733
+ container.appendChild(titleEl);
734
+
735
+ // Add description if available
736
+ if (description) {
737
+ const descEl = document.createElement("p");
738
+ descEl.className = "dap-article-description";
739
+ descEl.textContent = description;
740
+ container.appendChild(descEl);
741
+ }
742
+
743
+ // Handle different content scenarios
744
+ if (content && content.trim()) {
745
+ // Rich HTML content provided directly
746
+ console.debug("[DAP] Rendering direct HTML content");
747
+ const contentEl = document.createElement("div");
748
+ contentEl.className = "dap-article-content";
749
+ contentEl.innerHTML = content;
750
+ container.appendChild(contentEl);
751
+
752
+ // Add action buttons if URL is also provided
753
+ if (url) {
754
+ const actions = createDocumentActions(url, fileName);
755
+ container.appendChild(actions);
756
+ }
757
+ } else if (url) {
758
+ // URL-based content - create loading state first
759
+ const loadingEl = document.createElement("div");
760
+ loadingEl.className = "dap-article-loading";
761
+ loadingEl.innerHTML = `
762
+ <div class="dap-loading-spinner"></div>
763
+ <p>Loading article content...</p>
764
+ `;
765
+ container.appendChild(loadingEl);
766
+
767
+ // Resolve viewer type
768
+ const { viewer, mimeType } = resolveArticleViewer(articleContent);
769
+ console.debug("[DAP] Selected viewer type:", viewer, "for URL:", url);
770
+
771
+ // Remove loading state after a brief delay to show it
772
+ setTimeout(() => {
773
+ loadingEl.remove();
774
+
775
+ // Create appropriate viewer
776
+ switch (viewer) {
777
+ case "pdf":
778
+ const pdfViewer = createInlinePDFViewer(url, fileName);
779
+ container.appendChild(pdfViewer);
780
+ break;
781
+
782
+ case "document":
783
+ const docViewer = createInlineDocumentViewer(url, fileName, mimeType);
784
+ container.appendChild(docViewer);
785
+ break;
786
+
787
+ case "presentation":
788
+ const pptViewer = createInlinePresentationViewer(url, fileName, mimeType);
789
+ container.appendChild(pptViewer);
790
+ break;
791
+
792
+ case "web":
793
+ const webViewer = createWebContentViewer(url, title);
794
+ container.appendChild(webViewer);
795
+ break;
796
+
797
+ case "fallback":
798
+ default:
799
+ console.debug("[DAP] Fallback activated for URL:", url);
800
+ const fallbackViewer = createEnhancedFallbackViewer(articleContent, "This document cannot be previewed inline.");
801
+ container.appendChild(fallbackViewer);
802
+ break;
803
+ }
804
+ }, 300);
805
+ } else {
806
+ // No content or URL provided
807
+ console.error("[DAP] No content or URL provided for Article");
808
+ const errorViewer = createEnhancedFallbackViewer(articleContent, "No article content available to display.");
809
+ container.appendChild(errorViewer);
810
+ }
811
+
812
+ return container;
813
+ }
814
+
815
+ // Inline PDF Viewer - uses iframe for PDF preview
816
+ function createInlinePDFViewer(url: string, fileName: string): HTMLElement {
817
+ const container = document.createElement("div");
818
+ container.className = "dap-pdf-viewer-container";
819
+
820
+ // Try iframe first
821
+ const iframe = document.createElement("iframe");
822
+ iframe.className = "dap-pdf-iframe";
823
+ iframe.src = url;
824
+ iframe.style.width = "100%";
825
+ iframe.style.height = "500px";
826
+ iframe.style.border = "1px solid #ddd";
827
+ iframe.setAttribute("frameborder", "0");
828
+
829
+ // Add fallback handling
830
+ iframe.onerror = () => {
831
+ console.warn("[DAP] PDF iframe failed, showing fallback");
832
+ container.innerHTML = "";
833
+ const fallback = createFallbackViewer({ url, fileName, title: fileName },
834
+ "PDF preview failed. Please download or open in a new tab.");
835
+ container.appendChild(fallback);
836
+ };
837
+
838
+ container.appendChild(iframe);
839
+
840
+ // Add action buttons below PDF
841
+ const actions = createDocumentActions(url, fileName);
842
+ container.appendChild(actions);
843
+
844
+ return container;
845
+ }
846
+
847
+ // Inline Document Viewer - attempts preview with Office Online, falls back gracefully
848
+ function createInlineDocumentViewer(url: string, fileName: string, mimeType?: string | null): HTMLElement {
849
+ const container = document.createElement("div");
850
+ container.className = "dap-document-viewer-container";
851
+
852
+ // Try Office Online viewer for DOC/DOCX
853
+ if (mimeType && (mimeType.includes("word") || mimeType.includes("msword"))) {
854
+ const iframe = document.createElement("iframe");
855
+ iframe.className = "dap-document-iframe";
856
+ iframe.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
857
+ iframe.style.width = "100%";
858
+ iframe.style.height = "500px";
859
+ iframe.style.border = "1px solid #ddd";
860
+ iframe.setAttribute("frameborder", "0");
861
+
862
+ // Add fallback handling
863
+ iframe.onerror = () => {
864
+ console.warn("[DAP] Office Online viewer failed, showing fallback");
865
+ container.innerHTML = "";
866
+ const fallback = createFallbackViewer({ url, fileName, title: fileName },
867
+ "Document preview is not available. Please download or open in a new tab.");
868
+ container.appendChild(fallback);
869
+ };
870
+
871
+ container.appendChild(iframe);
872
+ } else {
873
+ // Direct fallback for unsupported document types
874
+ const fallback = createFallbackViewer({ url, fileName, title: fileName },
875
+ "Document preview is not supported for this file type.");
876
+ container.appendChild(fallback);
877
+ }
878
+
879
+ // Add action buttons
880
+ const actions = createDocumentActions(url, fileName);
881
+ container.appendChild(actions);
882
+
883
+ return container;
884
+ }
885
+
886
+ // Inline Presentation Viewer - attempts preview for PPT/PPTX
887
+ function createInlinePresentationViewer(url: string, fileName: string, mimeType?: string | null): HTMLElement {
888
+ const container = document.createElement("div");
889
+ container.className = "dap-presentation-viewer-container";
890
+
891
+ // Try Office Online viewer for PPT/PPTX
892
+ if (mimeType && mimeType.includes("presentation")) {
893
+ const iframe = document.createElement("iframe");
894
+ iframe.className = "dap-presentation-iframe";
895
+ iframe.src = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
896
+ iframe.style.width = "100%";
897
+ iframe.style.height = "500px";
898
+ iframe.style.border = "1px solid #ddd";
899
+ iframe.setAttribute("frameborder", "0");
900
+
901
+ // Add fallback handling
902
+ iframe.onerror = () => {
903
+ console.warn("[DAP] Office Online presentation viewer failed, showing fallback");
904
+ container.innerHTML = "";
905
+ const fallback = createFallbackViewer({ url, fileName, title: fileName },
906
+ "Presentation preview is not available. Please download or open in a new tab.");
907
+ container.appendChild(fallback);
908
+ };
909
+
910
+ container.appendChild(iframe);
911
+ } else {
912
+ // Direct fallback for unsupported presentation types
913
+ const fallback = createFallbackViewer({ url, fileName, title: fileName },
914
+ "Presentation preview is not supported for this file type.");
915
+ container.appendChild(fallback);
916
+ }
917
+
918
+ // Add action buttons
919
+ const actions = createDocumentActions(url, fileName);
920
+ container.appendChild(actions);
921
+
922
+ return container;
923
+ }
924
+
925
+ // Fallback Viewer - shows clear message and action buttons when inline preview is not possible
926
+ function createFallbackViewer(articleContent: any, message: string): HTMLElement {
927
+ const container = document.createElement("div");
928
+ container.className = "dap-fallback-viewer";
929
+
930
+ const url = articleContent.url || articleContent.presignedUrl || "";
931
+ const fileName = articleContent.fileName || "Document";
932
+
933
+ // Message
934
+ const messageEl = document.createElement("div");
935
+ messageEl.className = "dap-fallback-message";
936
+ messageEl.innerHTML = `
937
+ <p><strong>${message}</strong></p>
938
+ <p>File: ${fileName}</p>
939
+ `;
940
+ container.appendChild(messageEl);
941
+
942
+ // Action buttons
943
+ const actions = createDocumentActions(url, fileName);
944
+ container.appendChild(actions);
945
+
946
+ return container;
947
+ }
948
+
949
+ // Enhanced Fallback Viewer - improved version with better UX and error handling
950
+ function createEnhancedFallbackViewer(articleContent: any, message: string): HTMLElement {
951
+ const container = document.createElement("div");
952
+ container.className = "dap-enhanced-fallback-viewer";
953
+
954
+ const url = articleContent.url || articleContent.presignedUrl || "";
955
+ const fileName = articleContent.fileName || "Document";
956
+ const title = articleContent.title || fileName;
957
+ const fileExtension = fileName.split('.').pop()?.toUpperCase() || "";
958
+
959
+ // Icon based on file type
960
+ const iconEl = document.createElement("div");
961
+ iconEl.className = "dap-fallback-icon";
962
+ if (fileExtension.includes('PDF')) {
963
+ iconEl.innerHTML = "📄";
964
+ } else if (['DOC', 'DOCX'].includes(fileExtension)) {
965
+ iconEl.innerHTML = "📝";
966
+ } else if (['PPT', 'PPTX'].includes(fileExtension)) {
967
+ iconEl.innerHTML = "📊";
968
+ } else if (['XLS', 'XLSX'].includes(fileExtension)) {
969
+ iconEl.innerHTML = "📈";
970
+ } else {
971
+ iconEl.innerHTML = "📰";
972
+ }
973
+ container.appendChild(iconEl);
974
+
975
+ // Message with better formatting
976
+ const messageEl = document.createElement("div");
977
+ messageEl.className = "dap-enhanced-fallback-message";
978
+ messageEl.innerHTML = `
979
+ <h4>${title}</h4>
980
+ <p class="dap-fallback-primary">${message}</p>
981
+ ${fileName !== title ? `<p class="dap-fallback-filename">File: ${fileName}</p>` : ''}
982
+ ${fileExtension ? `<p class="dap-fallback-type">Type: ${fileExtension} Document</p>` : ''}
983
+ `;
984
+ container.appendChild(messageEl);
985
+
986
+ // Action buttons with better styling
987
+ if (url) {
988
+ const actions = createEnhancedDocumentActions(url, fileName);
989
+ container.appendChild(actions);
990
+ } else {
991
+ const noUrlMessage = document.createElement("p");
992
+ noUrlMessage.className = "dap-fallback-no-url";
993
+ noUrlMessage.textContent = "No document link available.";
994
+ container.appendChild(noUrlMessage);
995
+ }
996
+
997
+ return container;
998
+ }
999
+
1000
+ // Web Content Viewer - for displaying web pages in an iframe
1001
+ function createWebContentViewer(url: string, title: string): HTMLElement {
1002
+ const container = document.createElement("div");
1003
+ container.className = "dap-web-viewer-container";
1004
+
1005
+ // Create iframe for web content
1006
+ const iframe = document.createElement("iframe");
1007
+ iframe.className = "dap-web-iframe";
1008
+ iframe.src = url;
1009
+ iframe.style.width = "100%";
1010
+ iframe.style.height = "600px";
1011
+ iframe.style.border = "1px solid var(--dap-border)";
1012
+ iframe.style.borderRadius = "4px";
1013
+ iframe.setAttribute("frameborder", "0");
1014
+ iframe.setAttribute("loading", "lazy");
1015
+
1016
+ // Add error handling
1017
+ iframe.onerror = () => {
1018
+ console.warn("[DAP] Web content iframe failed, showing fallback");
1019
+ container.innerHTML = "";
1020
+ const fallback = createEnhancedFallbackViewer({ url, title, fileName: title },
1021
+ "Web content could not be loaded. Please open in a new tab.");
1022
+ container.appendChild(fallback);
1023
+ };
1024
+
1025
+ // Add iframe to container
1026
+ container.appendChild(iframe);
1027
+
1028
+ // Add action buttons
1029
+ const actions = document.createElement("div");
1030
+ actions.className = "dap-web-actions";
1031
+ const openBtn = document.createElement("button");
1032
+ openBtn.className = "dap-action-btn dap-open-btn";
1033
+ openBtn.textContent = "Open in New Tab";
1034
+ openBtn.addEventListener("click", (e) => {
1035
+ e.preventDefault();
1036
+ window.open(url, "_blank", "noopener,noreferrer");
1037
+ });
1038
+ actions.appendChild(openBtn);
1039
+ container.appendChild(actions);
1040
+
1041
+ return container;
1042
+ }
1043
+
1044
+ // Enhanced Document Actions - improved version with better UX
1045
+ function createEnhancedDocumentActions(url: string, fileName: string): HTMLElement {
1046
+ const actions = document.createElement("div");
1047
+ actions.className = "dap-enhanced-document-actions";
1048
+
1049
+ // Download button with icon
1050
+ const downloadBtn = document.createElement("button");
1051
+ downloadBtn.className = "dap-action-btn dap-download-btn dap-primary-btn";
1052
+ downloadBtn.innerHTML = `
1053
+ <span class="dap-btn-icon">⬇️</span>
1054
+ <span class="dap-btn-text">Download</span>
1055
+ `;
1056
+ downloadBtn.addEventListener("click", (e) => {
1057
+ e.preventDefault();
1058
+ e.stopPropagation();
1059
+
1060
+ // Create temporary download link
1061
+ const link = document.createElement("a");
1062
+ link.href = url;
1063
+ link.download = fileName || "download";
1064
+ link.target = "_blank";
1065
+ document.body.appendChild(link);
1066
+ link.click();
1067
+ document.body.removeChild(link);
1068
+
1069
+ // Show feedback
1070
+ downloadBtn.innerHTML = `<span class="dap-btn-icon">✅</span><span class="dap-btn-text">Downloaded</span>`;
1071
+ setTimeout(() => {
1072
+ downloadBtn.innerHTML = `<span class="dap-btn-icon">⬇️</span><span class="dap-btn-text">Download</span>`;
1073
+ }, 2000);
1074
+ });
1075
+
1076
+ // Open in new tab button with icon
1077
+ const openBtn = document.createElement("button");
1078
+ openBtn.className = "dap-action-btn dap-open-btn dap-secondary-btn";
1079
+ openBtn.innerHTML = `
1080
+ <span class="dap-btn-icon">🔗</span>
1081
+ <span class="dap-btn-text">Open in New Tab</span>
1082
+ `;
1083
+ openBtn.addEventListener("click", (e) => {
1084
+ e.preventDefault();
1085
+ e.stopPropagation();
1086
+ window.open(url, "_blank", "noopener,noreferrer");
1087
+ });
1088
+
1089
+ actions.appendChild(downloadBtn);
1090
+ actions.appendChild(openBtn);
1091
+
1092
+ return actions;
1093
+ }
1094
+
1095
+ // Document Actions - reusable download and open buttons with proper event listeners
1096
+ function createDocumentActions(url: string, fileName: string): HTMLElement {
1097
+ const actions = document.createElement("div");
1098
+ actions.className = "dap-document-actions dap-modal-buttons";
1099
+
1100
+ // Download button
1101
+ const downloadBtn = document.createElement("button");
1102
+ downloadBtn.className = "dap-action-btn dap-download-btn dap-modal-button primary";
1103
+ downloadBtn.textContent = "Download";
1104
+ downloadBtn.addEventListener("click", (e) => {
1105
+ e.preventDefault();
1106
+ e.stopPropagation();
1107
+
1108
+ // Create temporary download link
1109
+ const link = document.createElement("a");
1110
+ link.href = url;
1111
+ link.download = fileName || "download";
1112
+ link.target = "_blank";
1113
+ document.body.appendChild(link);
1114
+ link.click();
1115
+ document.body.removeChild(link);
1116
+ });
1117
+
1118
+ // Open in new tab button
1119
+ const openBtn = document.createElement("button");
1120
+ openBtn.className = "dap-action-btn dap-open-btn dap-modal-button secondary";
1121
+ openBtn.textContent = "Open in New Tab";
1122
+ openBtn.addEventListener("click", (e) => {
1123
+ e.preventDefault();
1124
+ e.stopPropagation();
1125
+ window.open(url, "_blank", "noopener,noreferrer");
1126
+ });
1127
+
1128
+ actions.appendChild(downloadBtn);
1129
+ actions.appendChild(openBtn);
1130
+
1131
+ return actions;
1132
+ }
1133
+
1134
+ function createDocumentViewer(url: string, fileName?: string, type?: string): HTMLElement {
1135
+ const container = document.createElement("div");
1136
+ container.className = "dap-kb-document-container";
1137
+
1138
+ // For docs, provide download/open options since inline viewing is limited
1139
+ container.innerHTML = `
1140
+ <div class="dap-kb-document-info">
1141
+ <h4>${fileName || "Document"}</h4>
1142
+ <p>Document type: ${type?.toUpperCase()}</p>
1143
+ <div class="dap-kb-document-actions">
1144
+ <button class="dap-kb-download-btn" onclick="window.open('${url}', '_blank')">
1145
+ Open in New Tab
1146
+ </button>
1147
+ <button class="dap-kb-download-btn" onclick="downloadFile('${url}', '${fileName}')">
1148
+ Download
1149
+ </button>
1150
+ </div>
1151
+ </div>
1152
+ `;
1153
+
1154
+ return container;
1155
+ }
1156
+
1157
+ function createYouTubeViewer(url: string): HTMLElement {
1158
+ const iframe = document.createElement("iframe");
1159
+ iframe.className = "dap-kb-youtube";
1160
+ iframe.src = url;
1161
+ iframe.style.width = "100%";
1162
+ iframe.style.height = "315px";
1163
+ iframe.setAttribute("frameborder", "0");
1164
+ iframe.setAttribute("allowfullscreen", "true");
1165
+
1166
+ return iframe;
1167
+ }
1168
+
1169
+ function createLinkViewer(url: string, title?: string, description?: string): HTMLElement {
1170
+ const container = document.createElement("div");
1171
+ container.className = "dap-kb-link-container";
1172
+
1173
+ container.innerHTML = `
1174
+ <div class="dap-kb-link-info">
1175
+ <h4>${title || "External Link"}</h4>
1176
+ ${description ? `<p>${description}</p>` : ''}
1177
+ <p><strong>URL:</strong> ${url}</p>
1178
+ <button class="dap-kb-external-btn" onclick="window.open('${url}', '_blank')">
1179
+ Open Link in New Tab
1180
+ </button>
1181
+ </div>
1182
+ `;
1183
+
1184
+ return container;
1185
+ }
1186
+
1187
+ function detectContentType(url: string, fileName?: string): string {
1188
+ const path = fileName || url;
1189
+ const ext = path.split('.').pop()?.toLowerCase();
1190
+
1191
+ if (ext === 'pdf') return 'pdf';
1192
+ if (['doc', 'docx'].includes(ext || '')) return ext || 'doc';
1193
+ if (['mp4', 'webm', 'ogg', 'avi', 'mov'].includes(ext || '')) return 'video';
1194
+ if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext || '')) return 'image';
1195
+ if (url.includes('youtube.com') || url.includes('youtu.be')) return 'youtube';
1196
+
1197
+ return 'link';
1198
+ }
1199
+
1200
+ // KB State Management - maintains explicit state across view transitions
1201
+ let kbState: {
1202
+ view: "list" | "item";
1203
+ items: any[];
1204
+ selectedItem: any | null;
1205
+ title: string;
1206
+ modalBodyRef: HTMLElement | null;
1207
+ } | null = null;
1208
+
1209
+ // Helper function to create file type badge
1210
+ function createFileTypeBadge(itemType: string, fileName?: string): HTMLElement {
1211
+ const badge = document.createElement("div");
1212
+ badge.className = `dap-file-type-badge ${itemType}`;
1213
+
1214
+ let icon = "";
1215
+ let label = "";
1216
+
1217
+ switch (itemType) {
1218
+ case "video":
1219
+ icon = "🎥";
1220
+ label = "Video";
1221
+ break;
1222
+ case "image":
1223
+ icon = "🖼️";
1224
+ label = "Image";
1225
+ break;
1226
+ case "pdf":
1227
+ icon = "📄";
1228
+ label = "PDF";
1229
+ break;
1230
+ case "docx":
1231
+ case "doc":
1232
+ icon = "📝";
1233
+ label = "Document";
1234
+ break;
1235
+ case "pptx":
1236
+ case "ppt":
1237
+ icon = "📊";
1238
+ label = "Presentation";
1239
+ break;
1240
+ case "xlsx":
1241
+ case "xls":
1242
+ icon = "📈";
1243
+ label = "Spreadsheet";
1244
+ break;
1245
+ case "article":
1246
+ default:
1247
+ icon = "📰";
1248
+ label = "Article";
1249
+ break;
1250
+ }
1251
+
1252
+ badge.innerHTML = `<span>${icon}</span> ${label}`;
1253
+
1254
+ return badge;
1255
+ }
1256
+
1257
+ function openKBItemInModal(item: any, kbTitle?: string) {
1258
+ console.debug("[DAP] Opening KB item in modal:", item);
1259
+ console.debug("[DAP] KB view changed to item");
1260
+
1261
+ // Find the current modal body
1262
+ const modalBody = document.querySelector('.dap-modal-body') as HTMLElement;
1263
+ if (!modalBody) {
1264
+ console.error("[DAP] Could not find modal body for KB item viewing");
1265
+ return;
1266
+ }
1267
+
1268
+ // Update KB state for item view
1269
+ if (kbState) {
1270
+ kbState.view = "item";
1271
+ kbState.selectedItem = item;
1272
+ kbState.modalBodyRef = modalBody;
1273
+
1274
+ console.debug("[DAP] KB items count:", kbState.items.length);
1275
+ } else {
1276
+ console.error("[DAP] KB state not initialized when opening item");
1277
+ return;
1278
+ }
1279
+
1280
+ // Create KB item viewer content
1281
+ const viewerContent = {
1282
+ kind: "kb-item-viewer",
1283
+ item: item,
1284
+ kbTitle: kbTitle
1285
+ };
1286
+
1287
+ // Clear modal body and add viewer
1288
+ modalBody.innerHTML = '';
1289
+ const viewerEl = renderKBItemViewer(viewerContent);
1290
+ modalBody.appendChild(viewerEl);
1291
+ }
1292
+
1293
+ function goBackToKBList() {
1294
+ console.debug("[DAP] Going back to KB list");
1295
+ console.debug("[DAP] KB view changed to list");
1296
+
1297
+ if (!kbState || !kbState.modalBodyRef) {
1298
+ console.error("[DAP] Cannot go back: missing KB state or modal reference");
1299
+ return;
1300
+ }
1301
+
1302
+ console.debug("[DAP] KB items count:", kbState.items.length);
1303
+
1304
+ // Update KB state for list view
1305
+ kbState.view = "list";
1306
+ kbState.selectedItem = null;
1307
+
1308
+ // Prepare KB content for rendering
1309
+ const kbContent = {
1310
+ title: kbState.title,
1311
+ items: kbState.items
1312
+ };
1313
+
1314
+ // Clear modal body and restore KB list using preserved state
1315
+ kbState.modalBodyRef.innerHTML = '';
1316
+ const kbListEl = renderKnowledgeBase(kbContent);
1317
+ kbState.modalBodyRef.appendChild(kbListEl);
1318
+ }
1319
+
1320
+ // Utility function for file downloads
1321
+ function downloadFile(url: string, fileName?: string) {
1322
+ const a = document.createElement('a');
1323
+ a.href = url;
1324
+ a.download = fileName || 'download';
1325
+ document.body.appendChild(a);
1326
+ a.click();
1327
+ document.body.removeChild(a);
1328
+ }
1329
+
1330
+ function ensureStyles() {
1331
+ if (!document.getElementById("dap-modal-style")) {
1332
+ const style = document.createElement("style");
1333
+ style.id = "dap-modal-style";
1334
+ style.textContent = modalCssText;
1335
+ document.head.appendChild(style);
1336
+ }
1337
+ }