@memberjunction/ng-artifacts 5.39.0 → 5.40.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 (52) hide show
  1. package/dist/lib/artifacts.module.d.ts +26 -13
  2. package/dist/lib/artifacts.module.d.ts.map +1 -1
  3. package/dist/lib/artifacts.module.js +65 -2
  4. package/dist/lib/artifacts.module.js.map +1 -1
  5. package/dist/lib/components/artifact-message-card.component.d.ts +17 -1
  6. package/dist/lib/components/artifact-message-card.component.d.ts.map +1 -1
  7. package/dist/lib/components/artifact-message-card.component.js +118 -27
  8. package/dist/lib/components/artifact-message-card.component.js.map +1 -1
  9. package/dist/lib/components/base-artifact-viewer.component.d.ts +11 -0
  10. package/dist/lib/components/base-artifact-viewer.component.d.ts.map +1 -1
  11. package/dist/lib/components/base-artifact-viewer.component.js +11 -0
  12. package/dist/lib/components/base-artifact-viewer.component.js.map +1 -1
  13. package/dist/lib/components/plugins/audio-artifact-viewer.component.d.ts +32 -0
  14. package/dist/lib/components/plugins/audio-artifact-viewer.component.d.ts.map +1 -0
  15. package/dist/lib/components/plugins/audio-artifact-viewer.component.js +146 -0
  16. package/dist/lib/components/plugins/audio-artifact-viewer.component.js.map +1 -0
  17. package/dist/lib/components/plugins/image-artifact-viewer.component.d.ts +11 -1
  18. package/dist/lib/components/plugins/image-artifact-viewer.component.d.ts.map +1 -1
  19. package/dist/lib/components/plugins/image-artifact-viewer.component.js +17 -1
  20. package/dist/lib/components/plugins/image-artifact-viewer.component.js.map +1 -1
  21. package/dist/lib/components/plugins/video-artifact-viewer.component.d.ts +34 -0
  22. package/dist/lib/components/plugins/video-artifact-viewer.component.d.ts.map +1 -0
  23. package/dist/lib/components/plugins/video-artifact-viewer.component.js +148 -0
  24. package/dist/lib/components/plugins/video-artifact-viewer.component.js.map +1 -0
  25. package/dist/lib/components/previews/audio-artifact-preview.component.d.ts +20 -0
  26. package/dist/lib/components/previews/audio-artifact-preview.component.d.ts.map +1 -0
  27. package/dist/lib/components/previews/audio-artifact-preview.component.js +83 -0
  28. package/dist/lib/components/previews/audio-artifact-preview.component.js.map +1 -0
  29. package/dist/lib/components/previews/base-artifact-preview.component.d.ts +33 -0
  30. package/dist/lib/components/previews/base-artifact-preview.component.d.ts.map +1 -0
  31. package/dist/lib/components/previews/base-artifact-preview.component.js +52 -0
  32. package/dist/lib/components/previews/base-artifact-preview.component.js.map +1 -0
  33. package/dist/lib/components/previews/image-artifact-preview.component.d.ts +28 -0
  34. package/dist/lib/components/previews/image-artifact-preview.component.d.ts.map +1 -0
  35. package/dist/lib/components/previews/image-artifact-preview.component.js +91 -0
  36. package/dist/lib/components/previews/image-artifact-preview.component.js.map +1 -0
  37. package/dist/lib/components/previews/video-artifact-preview.component.d.ts +21 -0
  38. package/dist/lib/components/previews/video-artifact-preview.component.d.ts.map +1 -0
  39. package/dist/lib/components/previews/video-artifact-preview.component.js +84 -0
  40. package/dist/lib/components/previews/video-artifact-preview.component.js.map +1 -0
  41. package/dist/lib/interfaces/artifact-viewer-plugin.interface.d.ts +53 -10
  42. package/dist/lib/interfaces/artifact-viewer-plugin.interface.d.ts.map +1 -1
  43. package/dist/lib/interfaces/artifact-viewer-plugin.interface.js.map +1 -1
  44. package/dist/lib/services/artifact-preview-resolver.service.d.ts +39 -0
  45. package/dist/lib/services/artifact-preview-resolver.service.d.ts.map +1 -0
  46. package/dist/lib/services/artifact-preview-resolver.service.js +78 -0
  47. package/dist/lib/services/artifact-preview-resolver.service.js.map +1 -0
  48. package/dist/public-api.d.ts +7 -0
  49. package/dist/public-api.d.ts.map +1 -1
  50. package/dist/public-api.js +8 -0
  51. package/dist/public-api.js.map +1 -1
  52. package/package.json +18 -18
@@ -1 +1 @@
1
- {"version":3,"file":"image-artifact-viewer.component.js","sourceRoot":"","sources":["../../../../src/lib/components/plugins/image-artifact-viewer.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';\nimport { DataSnapshot } from '@memberjunction/core';\nimport { RegisterClass } from '@memberjunction/global';\nimport { BaseArtifactViewerPluginComponent } from '../base-artifact-viewer.component';\nimport { ArtifactFileService } from '../../services/artifact-file.service';\n\n/**\n * Lookup table for synthesizing a download filename when the artifact version\n * has no `FileName` set. Covers the MIME types declared by the seed `Image`\n * artifact type (`image/*`); anything not listed falls back to `img`.\n *\n * Exported so the unit test can assert the mapping without instantiating an\n * Angular component (the package's vitest config uses the `node` environment).\n */\nexport const IMAGE_MIME_EXTENSION_MAP: Readonly<Record<string, string>> = Object.freeze({\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/gif': 'gif',\n 'image/webp': 'webp',\n 'image/bmp': 'bmp',\n 'image/tiff': 'tif',\n 'image/avif': 'avif',\n 'image/heic': 'heic',\n 'image/heif': 'heif',\n});\n\n/**\n * Pure helper that synthesizes a safe download filename from an artifact's\n * name + MIME type. Pulled out as a top-level function (not a class method) so\n * the vitest `node`-environment unit test can exercise it directly.\n *\n * - Strips characters illegal on common filesystems (anything not\n * `[A-Za-z0-9._-]`) and replaces them with underscores.\n * - Picks the extension from {@link IMAGE_MIME_EXTENSION_MAP}, defaulting to\n * `img` if the MIME type is missing or unrecognized.\n * - Falls back to the literal `image` when no name is supplied.\n *\n * @param name Artifact name (e.g. `\"Generated image 1\"`). Optional.\n * @param mime MIME type from the artifact version (e.g. `\"image/jpeg\"`). Optional.\n * @returns Sanitized filename with extension (e.g. `\"Generated_image_1.jpg\"`).\n */\nexport function buildImageDownloadFileName(name: string | null | undefined, mime: string | null | undefined): string {\n // `??` falls back on null/undefined but NOT empty / whitespace-only names —\n // those should also use the default, so trim first and check for length.\n const trimmed = name?.trim();\n const baseName = trimmed && trimmed.length > 0 ? trimmed : 'image';\n const safeBase = baseName.replace(/[^A-Za-z0-9._-]/g, '_');\n const normalizedMime = (mime ?? '').toLowerCase();\n const extension = IMAGE_MIME_EXTENSION_MAP[normalizedMime] ?? 'img';\n // If the name already ends with the right extension, don't append it again\n // (e.g. metadata round-tripped through a file upload).\n return safeBase.toLowerCase().endsWith(`.${extension}`) ? safeBase : `${safeBase}.${extension}`;\n}\n\n/**\n * Viewer plugin for raster image artifact versions (`image/png`, `image/jpeg`,\n * `image/webp`, etc.). Vector SVG is handled by `SvgArtifactViewerPlugin`.\n *\n * Storage modes both supported (same contract as `PdfArtifactViewerPlugin`):\n * - `ContentMode === 'Text'` — `artifactVersion.Content` is a `data:image/<mime>;base64,…` URI.\n * We bind it directly to `<img [src]>`; the browser decodes inline.\n * - `ContentMode === 'File'` — bytes live in MJStorage. We fetch a\n * pre-authenticated URL via `ArtifactFileService.getDownloadUrl()` and\n * bind that to `<img [src]>`.\n *\n * UX highlights:\n * - Click the image to toggle **fit** (default — `object-fit: contain` inside\n * the panel) ↔ **actual** size (1:1 pixels, scrolls within the panel body).\n * - Toolbar provides Download (reuses base class `triggerBrowserDownload`) and\n * Print (opens in a new tab so the browser's native print dialog handles it).\n * - Loading and error states match the PDF plugin's vocabulary, so behavior\n * across all file-style viewers is consistent.\n * - Image dimensions are surfaced as a small chip once the browser has decoded\n * the image (`naturalWidth × naturalHeight`).\n *\n * Design tokens are used for every color so the plugin adapts to light / dark\n * themes — see root `CLAUDE.md` \"Design Token System\".\n *\n * @see PdfArtifactViewerComponent — the dual-storage reference implementation.\n * @see plans/artifact-attachment-unification.md — the broader artifact-as-canonical-storage design.\n * @see docs/chat-ui-image-display-bugs.md — context for why this plugin exists (Bug B).\n */\n@Component({\n standalone: false,\n selector: 'mj-image-artifact-viewer',\n template: `\n <div class=\"image-viewer\" [ngClass]=\"cssClass\">\n <mj-file-artifact-toolbar\n [fileName]=\"toolbarFileName\"\n [isDownloading]=\"isDownloading\"\n [showPrint]=\"true\"\n (download)=\"onDownload()\"\n (print)=\"onPrint()\"\n >\n </mj-file-artifact-toolbar>\n\n <div\n class=\"image-viewer__body\"\n [class.image-viewer__body--actual]=\"displayMode === 'actual'\"\n >\n @if (errorMessage) {\n <div class=\"image-viewer__state image-viewer__state--error\">\n <i class=\"fas fa-exclamation-triangle\"></i>\n <span>{{ errorMessage }}</span>\n </div>\n } @else {\n @if (isLoading) {\n <div class=\"image-viewer__state\">\n <i class=\"fas fa-spinner fa-spin\"></i>\n <span>Loading image…</span>\n </div>\n }\n @if (imageUrl) {\n <!-- The img is rendered even while loading=true (with opacity 0)\n so the browser starts decoding immediately. The (load) handler\n flips isLoading=false once dimensions are known. -->\n <img\n class=\"image-viewer__img\"\n [class.image-viewer__img--actual]=\"displayMode === 'actual'\"\n [class.image-viewer__img--hidden]=\"isLoading\"\n [src]=\"imageUrl\"\n [alt]=\"imageAltText\"\n [title]=\"\n displayMode === 'fit'\n ? 'Click to view at actual size'\n : 'Click to fit to window'\n \"\n (load)=\"onImageLoad($event)\"\n (error)=\"onImageError()\"\n (click)=\"toggleDisplayMode()\"\n />\n }\n }\n </div>\n\n @if (naturalWidth && naturalHeight && !isLoading && !errorMessage) {\n <div class=\"image-viewer__dimensions\">{{ naturalWidth }} × {{ naturalHeight }}</div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: block;\n height: 100%;\n }\n\n .image-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface-sunken);\n position: relative;\n }\n\n .image-viewer__body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n }\n\n /* \"Actual size\" mode: allow scrolling from the top-left so large images\n don't get re-centered as the user pans. The browser handles overflow. */\n .image-viewer__body--actual {\n align-items: flex-start;\n justify-content: flex-start;\n }\n\n .image-viewer__state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n color: var(--mj-text-muted);\n font-size: 14px;\n }\n\n .image-viewer__state--error {\n color: var(--mj-status-error-text);\n }\n\n .image-viewer__img {\n /* Fit mode (default): never exceeds the panel, preserves aspect ratio. */\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n object-fit: contain;\n cursor: zoom-in;\n background: var(--mj-bg-surface);\n border-radius: 4px;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n transition: opacity 0.18s ease-out;\n }\n\n /* Actual-size mode: render at the image's intrinsic dimensions so the body\n scroll-area handles anything larger than the panel. */\n .image-viewer__img--actual {\n max-width: none;\n max-height: none;\n width: auto;\n height: auto;\n cursor: zoom-out;\n }\n\n /* Pre-load: keep the <img> in the DOM (so the browser decodes it) but\n invisible and out of layout flow so the spinner sits centered. */\n .image-viewer__img--hidden {\n opacity: 0;\n position: absolute;\n pointer-events: none;\n }\n\n .image-viewer__dimensions {\n position: absolute;\n bottom: 8px;\n right: 12px;\n padding: 2px 8px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 90%, transparent);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n color: var(--mj-text-muted);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n pointer-events: none;\n /* Don't intercept clicks meant for the image-toggle. */\n }\n `,\n ],\n})\n@RegisterClass(BaseArtifactViewerPluginComponent, 'ImageArtifactViewerPlugin')\nexport class ImageArtifactViewerComponent extends BaseArtifactViewerPluginComponent implements OnInit, OnDestroy {\n /** Current resolved URL to bind to the `<img src>` — either a data URI (inline) or a pre-auth URL (file). */\n public imageUrl = '';\n\n /** True from `ngOnInit` until the `<img>` `(load)` event fires (or an error happens). */\n public isLoading = true;\n\n /** Set while the download is in flight, to drive the toolbar spinner. */\n public isDownloading = false;\n\n /** User-facing error message; non-empty hides the image and shows the error block. */\n public errorMessage = '';\n\n /** Click-to-toggle display mode. `'fit'` = contained in the panel, `'actual'` = 1:1 with scroll. */\n public displayMode: 'fit' | 'actual' = 'fit';\n\n /** Native image dimensions, populated by the `(load)` event. Null until then. */\n public naturalWidth: number | null = null;\n public naturalHeight: number | null = null;\n\n /**\n * Object URL we lazily build from inline data so Download / Print have a real\n * URL to anchor on. Tracked here so `ngOnDestroy` can release it via\n * `URL.revokeObjectURL`. Null until the user clicks Download or Print.\n */\n private inlineObjectUrl: string | null = null;\n\n constructor(\n private fileService: ArtifactFileService,\n private cdr: ChangeDetectorRef,\n ) {\n super();\n }\n\n /** Opt in to the wrapper's \"Display\" tab — without this the panel only shows Details/JSON. */\n public override get hasDisplayContent(): boolean {\n return true;\n }\n\n /** Filename surfaced by the toolbar in the center of the bar. */\n public get toolbarFileName(): string {\n return this.artifactVersion?.FileName || this.artifactVersion?.Name || 'image';\n }\n\n /** Accessible alt text — prefers a descriptive name over a raw filename. */\n public get imageAltText(): string {\n return this.artifactVersion?.Name || this.artifactVersion?.FileName || 'Image artifact';\n }\n\n async ngOnInit(): Promise<void> {\n await this.resolveImageUrl();\n }\n\n ngOnDestroy(): void {\n // Release any blob URL we built for Download / Print so the browser\n // doesn't hold the raw bytes in memory after the panel closes.\n if (this.inlineObjectUrl) {\n URL.revokeObjectURL(this.inlineObjectUrl);\n this.inlineObjectUrl = null;\n }\n }\n\n // ─── Loading ────────────────────────────────────────────────────────────────\n\n /**\n * Resolve the image URL based on the artifact version's `ContentMode`.\n * Sets `imageUrl` (which the template binds to `<img src>`) or `errorMessage`.\n * Loading state remains `true` until the browser fires `(load)` on the `<img>`.\n */\n private async resolveImageUrl(): Promise<void> {\n if (!this.artifactVersion?.ID) {\n this.showError('No artifact version provided.');\n return;\n }\n\n // Defensive MIME check — refuse to render non-image artifacts. If we get here\n // it's a metadata bug somewhere (the ArtifactType registry routed something\n // non-image to the image plugin), and silently showing a broken-image icon\n // would mask the real issue.\n const mime = (this.artifactVersion.MimeType ?? '').toLowerCase();\n if (mime && !mime.startsWith('image/')) {\n this.showError(`This viewer cannot display content of type \"${this.artifactVersion.MimeType}\".`);\n return;\n }\n\n try {\n if (this.artifactVersion.ContentMode === 'File') {\n // File-backed: fetch a pre-auth URL from MJStorage via the shared service.\n // ArtifactFileService caches the URL so reopening the same artifact is instant.\n this.imageUrl = await this.fileService.getDownloadUrl(this.artifactVersion.ID);\n } else {\n // Inline ('Text' mode): Content is a data URI ready to bind directly.\n const content = this.artifactVersion.Content;\n if (!content) {\n this.showError('This artifact has no image content.');\n return;\n }\n this.imageUrl = content;\n }\n this.cdr.markForCheck();\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n this.showError(`Could not load image: ${detail}`);\n }\n }\n\n public onImageLoad(event: Event): void {\n const img = event.target as HTMLImageElement;\n this.naturalWidth = img.naturalWidth || null;\n this.naturalHeight = img.naturalHeight || null;\n this.isLoading = false;\n this.cdr.markForCheck();\n }\n\n public onImageError(): void {\n this.showError('The image could not be displayed. It may be corrupt or in an unsupported format.');\n }\n\n private showError(message: string): void {\n this.isLoading = false;\n this.errorMessage = message;\n this.cdr.markForCheck();\n }\n\n // ─── User actions ───────────────────────────────────────────────────────────\n\n public toggleDisplayMode(): void {\n // Don't toggle while loading or errored — the user has nothing to react to yet.\n if (this.isLoading || this.errorMessage) {\n return;\n }\n this.displayMode = this.displayMode === 'fit' ? 'actual' : 'fit';\n this.cdr.markForCheck();\n }\n\n /**\n * Download the image to disk. Uses the inherited `triggerBrowserDownload`\n * helper so the fetch → blob → object-URL → anchor pattern is shared across\n * every file-backed viewer (PDF, XLSX, DOCX, and now Image).\n */\n public async onDownload(): Promise<void> {\n if (this.isDownloading || !this.imageUrl) {\n return;\n }\n this.isDownloading = true;\n this.cdr.markForCheck();\n try {\n const sourceUrl = this.ensureDownloadableUrl();\n const fileName = buildImageDownloadFileName(\n this.artifactVersion?.FileName ?? this.artifactVersion?.Name ?? null,\n this.artifactVersion?.MimeType ?? null,\n );\n await this.triggerBrowserDownload(sourceUrl, fileName);\n } finally {\n this.isDownloading = false;\n this.cdr.markForCheck();\n }\n }\n\n /**\n * Open the image in a new browser tab so the user can use the browser's\n * native print dialog. We deliberately don't manage a print pipeline\n * ourselves — same approach as `PdfArtifactViewerComponent.onPrint()`.\n */\n public onPrint(): void {\n if (!this.imageUrl) {\n return;\n }\n const sourceUrl = this.ensureDownloadableUrl();\n window.open(sourceUrl, '_blank');\n }\n\n /**\n * Returns a URL suitable for `<a download>` / `window.open` use.\n * - File mode: `imageUrl` is already a pre-auth blob URL — use it directly.\n * - Inline mode: `imageUrl` is a data URI; we lazily turn it into a blob URL\n * (cached on the instance) for nicer download UX. Some browsers truncate\n * long data URIs in the downloads tray; blob URLs preserve the filename.\n */\n private ensureDownloadableUrl(): string {\n if (this.artifactVersion?.ContentMode === 'File') {\n return this.imageUrl;\n }\n if (this.inlineObjectUrl) {\n return this.inlineObjectUrl;\n }\n const mime = this.artifactVersion?.MimeType || 'application/octet-stream';\n this.inlineObjectUrl = this.fileService.dataUrlToObjectUrl(this.imageUrl, mime);\n return this.inlineObjectUrl;\n }\n\n // ─── Snapshot ───────────────────────────────────────────────────────────────\n\n /**\n * Required by the base class. Returns a structured summary of the artifact\n * in its current display state — used by snapshot-consuming features\n * (e.g. agent context capture, share / export flows).\n */\n public override GetCurrentStateSnapshot(): DataSnapshot | null {\n if (!this.artifactVersion) {\n return null;\n }\n const snap = new DataSnapshot();\n snap.title = this.getDisplayTitle() ?? undefined;\n\n const dimensionPart =\n this.naturalWidth && this.naturalHeight ? ` ${this.naturalWidth} × ${this.naturalHeight}` : '';\n const mimeLabel = this.artifactVersion.MimeType ?? 'image';\n snap.interpretation = `Image (${mimeLabel}${dimensionPart}).`;\n\n snap.custom = {\n mimeType: this.artifactVersion.MimeType ?? undefined,\n fileName: this.artifactVersion.FileName ?? undefined,\n contentMode: this.artifactVersion.ContentMode,\n width: this.naturalWidth ?? undefined,\n height: this.naturalHeight ?? undefined,\n displayMode: this.displayMode,\n };\n return snap;\n }\n}\n"]}
1
+ {"version":3,"file":"image-artifact-viewer.component.js","sourceRoot":"","sources":["../../../../src/lib/components/plugins/image-artifact-viewer.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectorRef, Component, OnDestroy, OnInit, Type } from '@angular/core';\nimport { DataSnapshot } from '@memberjunction/core';\nimport { RegisterClass } from '@memberjunction/global';\nimport { BaseArtifactViewerPluginComponent } from '../base-artifact-viewer.component';\nimport { ArtifactFileService } from '../../services/artifact-file.service';\nimport { IArtifactPreviewComponent } from '../../interfaces/artifact-viewer-plugin.interface';\nimport { ImageArtifactPreviewComponent } from '../previews/image-artifact-preview.component';\n\n/**\n * Lookup table for synthesizing a download filename when the artifact version\n * has no `FileName` set. Covers the MIME types declared by the seed `Image`\n * artifact type (`image/*`); anything not listed falls back to `img`.\n *\n * Exported so the unit test can assert the mapping without instantiating an\n * Angular component (the package's vitest config uses the `node` environment).\n */\nexport const IMAGE_MIME_EXTENSION_MAP: Readonly<Record<string, string>> = Object.freeze({\n 'image/png': 'png',\n 'image/jpeg': 'jpg',\n 'image/jpg': 'jpg',\n 'image/gif': 'gif',\n 'image/webp': 'webp',\n 'image/bmp': 'bmp',\n 'image/tiff': 'tif',\n 'image/avif': 'avif',\n 'image/heic': 'heic',\n 'image/heif': 'heif',\n});\n\n/**\n * Pure helper that synthesizes a safe download filename from an artifact's\n * name + MIME type. Pulled out as a top-level function (not a class method) so\n * the vitest `node`-environment unit test can exercise it directly.\n *\n * - Strips characters illegal on common filesystems (anything not\n * `[A-Za-z0-9._-]`) and replaces them with underscores.\n * - Picks the extension from {@link IMAGE_MIME_EXTENSION_MAP}, defaulting to\n * `img` if the MIME type is missing or unrecognized.\n * - Falls back to the literal `image` when no name is supplied.\n *\n * @param name Artifact name (e.g. `\"Generated image 1\"`). Optional.\n * @param mime MIME type from the artifact version (e.g. `\"image/jpeg\"`). Optional.\n * @returns Sanitized filename with extension (e.g. `\"Generated_image_1.jpg\"`).\n */\nexport function buildImageDownloadFileName(name: string | null | undefined, mime: string | null | undefined): string {\n // `??` falls back on null/undefined but NOT empty / whitespace-only names —\n // those should also use the default, so trim first and check for length.\n const trimmed = name?.trim();\n const baseName = trimmed && trimmed.length > 0 ? trimmed : 'image';\n const safeBase = baseName.replace(/[^A-Za-z0-9._-]/g, '_');\n const normalizedMime = (mime ?? '').toLowerCase();\n const extension = IMAGE_MIME_EXTENSION_MAP[normalizedMime] ?? 'img';\n // If the name already ends with the right extension, don't append it again\n // (e.g. metadata round-tripped through a file upload).\n return safeBase.toLowerCase().endsWith(`.${extension}`) ? safeBase : `${safeBase}.${extension}`;\n}\n\n/**\n * Viewer plugin for raster image artifact versions (`image/png`, `image/jpeg`,\n * `image/webp`, etc.). Vector SVG is handled by `SvgArtifactViewerPlugin`.\n *\n * Storage modes both supported (same contract as `PdfArtifactViewerPlugin`):\n * - `ContentMode === 'Text'` — `artifactVersion.Content` is a `data:image/<mime>;base64,…` URI.\n * We bind it directly to `<img [src]>`; the browser decodes inline.\n * - `ContentMode === 'File'` — bytes live in MJStorage. We fetch a\n * pre-authenticated URL via `ArtifactFileService.getDownloadUrl()` and\n * bind that to `<img [src]>`.\n *\n * UX highlights:\n * - Click the image to toggle **fit** (default — `object-fit: contain` inside\n * the panel) ↔ **actual** size (1:1 pixels, scrolls within the panel body).\n * - Toolbar provides Download (reuses base class `triggerBrowserDownload`) and\n * Print (opens in a new tab so the browser's native print dialog handles it).\n * - Loading and error states match the PDF plugin's vocabulary, so behavior\n * across all file-style viewers is consistent.\n * - Image dimensions are surfaced as a small chip once the browser has decoded\n * the image (`naturalWidth × naturalHeight`).\n *\n * Design tokens are used for every color so the plugin adapts to light / dark\n * themes — see root `CLAUDE.md` \"Design Token System\".\n *\n * @see PdfArtifactViewerComponent — the dual-storage reference implementation.\n * @see plans/artifact-attachment-unification.md — the broader artifact-as-canonical-storage design.\n * @see docs/chat-ui-image-display-bugs.md — context for why this plugin exists (Bug B).\n */\n@Component({\n standalone: false,\n selector: 'mj-image-artifact-viewer',\n template: `\n <div class=\"image-viewer\" [ngClass]=\"cssClass\">\n <mj-file-artifact-toolbar\n [fileName]=\"toolbarFileName\"\n [isDownloading]=\"isDownloading\"\n [showPrint]=\"true\"\n (download)=\"onDownload()\"\n (print)=\"onPrint()\"\n >\n </mj-file-artifact-toolbar>\n\n <div\n class=\"image-viewer__body\"\n [class.image-viewer__body--actual]=\"displayMode === 'actual'\"\n >\n @if (errorMessage) {\n <div class=\"image-viewer__state image-viewer__state--error\">\n <i class=\"fas fa-exclamation-triangle\"></i>\n <span>{{ errorMessage }}</span>\n </div>\n } @else {\n @if (isLoading) {\n <div class=\"image-viewer__state\">\n <i class=\"fas fa-spinner fa-spin\"></i>\n <span>Loading image…</span>\n </div>\n }\n @if (imageUrl) {\n <!-- The img is rendered even while loading=true (with opacity 0)\n so the browser starts decoding immediately. The (load) handler\n flips isLoading=false once dimensions are known. -->\n <img\n class=\"image-viewer__img\"\n [class.image-viewer__img--actual]=\"displayMode === 'actual'\"\n [class.image-viewer__img--hidden]=\"isLoading\"\n [src]=\"imageUrl\"\n [alt]=\"imageAltText\"\n [title]=\"\n displayMode === 'fit'\n ? 'Click to view at actual size'\n : 'Click to fit to window'\n \"\n (load)=\"onImageLoad($event)\"\n (error)=\"onImageError()\"\n (click)=\"toggleDisplayMode()\"\n />\n }\n }\n </div>\n\n @if (naturalWidth && naturalHeight && !isLoading && !errorMessage) {\n <div class=\"image-viewer__dimensions\">{{ naturalWidth }} × {{ naturalHeight }}</div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: block;\n height: 100%;\n }\n\n .image-viewer {\n display: flex;\n flex-direction: column;\n height: 100%;\n background: var(--mj-bg-surface-sunken);\n position: relative;\n }\n\n .image-viewer__body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 16px;\n }\n\n /* \"Actual size\" mode: allow scrolling from the top-left so large images\n don't get re-centered as the user pans. The browser handles overflow. */\n .image-viewer__body--actual {\n align-items: flex-start;\n justify-content: flex-start;\n }\n\n .image-viewer__state {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n color: var(--mj-text-muted);\n font-size: 14px;\n }\n\n .image-viewer__state--error {\n color: var(--mj-status-error-text);\n }\n\n .image-viewer__img {\n /* Fit mode (default): never exceeds the panel, preserves aspect ratio. */\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n object-fit: contain;\n cursor: zoom-in;\n background: var(--mj-bg-surface);\n border-radius: 4px;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-text-primary) 12%, transparent);\n transition: opacity 0.18s ease-out;\n }\n\n /* Actual-size mode: render at the image's intrinsic dimensions so the body\n scroll-area handles anything larger than the panel. */\n .image-viewer__img--actual {\n max-width: none;\n max-height: none;\n width: auto;\n height: auto;\n cursor: zoom-out;\n }\n\n /* Pre-load: keep the <img> in the DOM (so the browser decodes it) but\n invisible and out of layout flow so the spinner sits centered. */\n .image-viewer__img--hidden {\n opacity: 0;\n position: absolute;\n pointer-events: none;\n }\n\n .image-viewer__dimensions {\n position: absolute;\n bottom: 8px;\n right: 12px;\n padding: 2px 8px;\n background: color-mix(in srgb, var(--mj-bg-surface-elevated) 90%, transparent);\n border: 1px solid var(--mj-border-default);\n border-radius: 12px;\n color: var(--mj-text-muted);\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n pointer-events: none;\n /* Don't intercept clicks meant for the image-toggle. */\n }\n `,\n ],\n})\n@RegisterClass(BaseArtifactViewerPluginComponent, 'ImageArtifactViewerPlugin')\nexport class ImageArtifactViewerComponent extends BaseArtifactViewerPluginComponent implements OnInit, OnDestroy {\n /** Current resolved URL to bind to the `<img src>` — either a data URI (inline) or a pre-auth URL (file). */\n public imageUrl = '';\n\n /** True from `ngOnInit` until the `<img>` `(load)` event fires (or an error happens). */\n public isLoading = true;\n\n /** Set while the download is in flight, to drive the toolbar spinner. */\n public isDownloading = false;\n\n /** User-facing error message; non-empty hides the image and shows the error block. */\n public errorMessage = '';\n\n /** Click-to-toggle display mode. `'fit'` = contained in the panel, `'actual'` = 1:1 with scroll. */\n public displayMode: 'fit' | 'actual' = 'fit';\n\n /** Native image dimensions, populated by the `(load)` event. Null until then. */\n public naturalWidth: number | null = null;\n public naturalHeight: number | null = null;\n\n /**\n * Object URL we lazily build from inline data so Download / Print have a real\n * URL to anchor on. Tracked here so `ngOnDestroy` can release it via\n * `URL.revokeObjectURL`. Null until the user clicks Download or Print.\n */\n private inlineObjectUrl: string | null = null;\n\n constructor(\n private fileService: ArtifactFileService,\n private cdr: ChangeDetectorRef,\n ) {\n super();\n }\n\n /**\n * Inline-preview component for image artifacts ({@link IArtifactViewerPluginPreviewStatics}).\n * STATIC so the resolver can read it off the registered constructor WITHOUT instantiating this\n * Angular component. Surfaces a contained thumbnail inside conversation message cards; the full\n * viewer (this class) remains the required `componentType`.\n */\n public static readonly PreviewComponentType: Type<IArtifactPreviewComponent> = ImageArtifactPreviewComponent;\n\n /** Matches raster image artifacts by type name or `image/*` MIME. STATIC — drives preview resolution. */\n public static CanHandlePreview(artifactTypeName: string, contentType?: string): boolean {\n const mime = (contentType ?? '').toLowerCase();\n if (mime.startsWith('image/')) {\n return true;\n }\n return (artifactTypeName ?? '').trim().toLowerCase() === 'image';\n }\n\n /** Opt in to the wrapper's \"Display\" tab — without this the panel only shows Details/JSON. */\n public override get hasDisplayContent(): boolean {\n return true;\n }\n\n /** Filename surfaced by the toolbar in the center of the bar. */\n public get toolbarFileName(): string {\n return this.artifactVersion?.FileName || this.artifactVersion?.Name || 'image';\n }\n\n /** Accessible alt text — prefers a descriptive name over a raw filename. */\n public get imageAltText(): string {\n return this.artifactVersion?.Name || this.artifactVersion?.FileName || 'Image artifact';\n }\n\n async ngOnInit(): Promise<void> {\n await this.resolveImageUrl();\n }\n\n ngOnDestroy(): void {\n // Release any blob URL we built for Download / Print so the browser\n // doesn't hold the raw bytes in memory after the panel closes.\n if (this.inlineObjectUrl) {\n URL.revokeObjectURL(this.inlineObjectUrl);\n this.inlineObjectUrl = null;\n }\n }\n\n // ─── Loading ────────────────────────────────────────────────────────────────\n\n /**\n * Resolve the image URL based on the artifact version's `ContentMode`.\n * Sets `imageUrl` (which the template binds to `<img src>`) or `errorMessage`.\n * Loading state remains `true` until the browser fires `(load)` on the `<img>`.\n */\n private async resolveImageUrl(): Promise<void> {\n if (!this.artifactVersion?.ID) {\n this.showError('No artifact version provided.');\n return;\n }\n\n // Defensive MIME check — refuse to render non-image artifacts. If we get here\n // it's a metadata bug somewhere (the ArtifactType registry routed something\n // non-image to the image plugin), and silently showing a broken-image icon\n // would mask the real issue.\n const mime = (this.artifactVersion.MimeType ?? '').toLowerCase();\n if (mime && !mime.startsWith('image/')) {\n this.showError(`This viewer cannot display content of type \"${this.artifactVersion.MimeType}\".`);\n return;\n }\n\n try {\n if (this.artifactVersion.ContentMode === 'File') {\n // File-backed: fetch a pre-auth URL from MJStorage via the shared service.\n // ArtifactFileService caches the URL so reopening the same artifact is instant.\n this.imageUrl = await this.fileService.getDownloadUrl(this.artifactVersion.ID);\n } else {\n // Inline ('Text' mode): Content is a data URI ready to bind directly.\n const content = this.artifactVersion.Content;\n if (!content) {\n this.showError('This artifact has no image content.');\n return;\n }\n this.imageUrl = content;\n }\n this.cdr.markForCheck();\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n this.showError(`Could not load image: ${detail}`);\n }\n }\n\n public onImageLoad(event: Event): void {\n const img = event.target as HTMLImageElement;\n this.naturalWidth = img.naturalWidth || null;\n this.naturalHeight = img.naturalHeight || null;\n this.isLoading = false;\n this.cdr.markForCheck();\n }\n\n public onImageError(): void {\n this.showError('The image could not be displayed. It may be corrupt or in an unsupported format.');\n }\n\n private showError(message: string): void {\n this.isLoading = false;\n this.errorMessage = message;\n this.cdr.markForCheck();\n }\n\n // ─── User actions ───────────────────────────────────────────────────────────\n\n public toggleDisplayMode(): void {\n // Don't toggle while loading or errored — the user has nothing to react to yet.\n if (this.isLoading || this.errorMessage) {\n return;\n }\n this.displayMode = this.displayMode === 'fit' ? 'actual' : 'fit';\n this.cdr.markForCheck();\n }\n\n /**\n * Download the image to disk. Uses the inherited `triggerBrowserDownload`\n * helper so the fetch → blob → object-URL → anchor pattern is shared across\n * every file-backed viewer (PDF, XLSX, DOCX, and now Image).\n */\n public async onDownload(): Promise<void> {\n if (this.isDownloading || !this.imageUrl) {\n return;\n }\n this.isDownloading = true;\n this.cdr.markForCheck();\n try {\n const sourceUrl = this.ensureDownloadableUrl();\n const fileName = buildImageDownloadFileName(\n this.artifactVersion?.FileName ?? this.artifactVersion?.Name ?? null,\n this.artifactVersion?.MimeType ?? null,\n );\n await this.triggerBrowserDownload(sourceUrl, fileName);\n } finally {\n this.isDownloading = false;\n this.cdr.markForCheck();\n }\n }\n\n /**\n * Open the image in a new browser tab so the user can use the browser's\n * native print dialog. We deliberately don't manage a print pipeline\n * ourselves — same approach as `PdfArtifactViewerComponent.onPrint()`.\n */\n public onPrint(): void {\n if (!this.imageUrl) {\n return;\n }\n const sourceUrl = this.ensureDownloadableUrl();\n window.open(sourceUrl, '_blank');\n }\n\n /**\n * Returns a URL suitable for `<a download>` / `window.open` use.\n * - File mode: `imageUrl` is already a pre-auth blob URL — use it directly.\n * - Inline mode: `imageUrl` is a data URI; we lazily turn it into a blob URL\n * (cached on the instance) for nicer download UX. Some browsers truncate\n * long data URIs in the downloads tray; blob URLs preserve the filename.\n */\n private ensureDownloadableUrl(): string {\n if (this.artifactVersion?.ContentMode === 'File') {\n return this.imageUrl;\n }\n if (this.inlineObjectUrl) {\n return this.inlineObjectUrl;\n }\n const mime = this.artifactVersion?.MimeType || 'application/octet-stream';\n this.inlineObjectUrl = this.fileService.dataUrlToObjectUrl(this.imageUrl, mime);\n return this.inlineObjectUrl;\n }\n\n // ─── Snapshot ───────────────────────────────────────────────────────────────\n\n /**\n * Required by the base class. Returns a structured summary of the artifact\n * in its current display state — used by snapshot-consuming features\n * (e.g. agent context capture, share / export flows).\n */\n public override GetCurrentStateSnapshot(): DataSnapshot | null {\n if (!this.artifactVersion) {\n return null;\n }\n const snap = new DataSnapshot();\n snap.title = this.getDisplayTitle() ?? undefined;\n\n const dimensionPart =\n this.naturalWidth && this.naturalHeight ? ` ${this.naturalWidth} × ${this.naturalHeight}` : '';\n const mimeLabel = this.artifactVersion.MimeType ?? 'image';\n snap.interpretation = `Image (${mimeLabel}${dimensionPart}).`;\n\n snap.custom = {\n mimeType: this.artifactVersion.MimeType ?? undefined,\n fileName: this.artifactVersion.FileName ?? undefined,\n contentMode: this.artifactVersion.ContentMode,\n width: this.naturalWidth ?? undefined,\n height: this.naturalHeight ?? undefined,\n displayMode: this.displayMode,\n };\n return snap;\n }\n}\n"]}
@@ -0,0 +1,34 @@
1
+ import { OnInit, Type } from '@angular/core';
2
+ import { DataSnapshot } from '@memberjunction/core';
3
+ import { BaseArtifactViewerPluginComponent } from '../base-artifact-viewer.component';
4
+ import { IArtifactPreviewComponent } from '../../interfaces/artifact-viewer-plugin.interface';
5
+ import * as i0 from "@angular/core";
6
+ /**
7
+ * Minimal full-size viewer plugin for video artifacts. Renders a `<video controls>` player that
8
+ * fills the panel. Supports both storage modes:
9
+ * - `ContentMode === 'File'` — fetch a pre-auth URL from MJStorage via {@link ArtifactFileService}.
10
+ * - `ContentMode === 'Text'` — `Content` is a `data:<mime>;base64,…` URI, bound directly.
11
+ *
12
+ * Deliberately minimal — the richer inline experience lives in {@link VideoArtifactPreviewComponent}
13
+ * (exposed via {@link previewComponentType}); this just needs to satisfy the required `componentType`.
14
+ */
15
+ export declare class VideoArtifactViewerComponent extends BaseArtifactViewerPluginComponent implements OnInit {
16
+ private readonly fileService;
17
+ private readonly cdr;
18
+ videoUrl: string;
19
+ errorMessage: string;
20
+ /**
21
+ * Inline-preview component for video artifacts ({@link IArtifactViewerPluginPreviewStatics}).
22
+ * STATIC so the resolver reads it off the constructor without instantiating this DI-using component.
23
+ */
24
+ static readonly PreviewComponentType: Type<IArtifactPreviewComponent>;
25
+ /** Matches video artifacts by type name or `video/*` MIME. STATIC — drives preview resolution. */
26
+ static CanHandlePreview(artifactTypeName: string, contentType?: string): boolean;
27
+ get hasDisplayContent(): boolean;
28
+ ngOnInit(): Promise<void>;
29
+ onMediaError(): void;
30
+ GetCurrentStateSnapshot(): DataSnapshot | null;
31
+ static ɵfac: i0.ɵɵFactoryDeclaration<VideoArtifactViewerComponent, never>;
32
+ static ɵcmp: i0.ɵɵComponentDeclaration<VideoArtifactViewerComponent, "mj-video-artifact-viewer", never, {}, {}, never, never, false, never>;
33
+ }
34
+ //# sourceMappingURL=video-artifact-viewer.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-artifact-viewer.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/plugins/video-artifact-viewer.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,MAAM,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEpD,OAAO,EAAE,iCAAiC,EAAE,MAAM,mCAAmC,CAAC;AAEtF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mDAAmD,CAAC;;AAG9F;;;;;;;;GAQG;AACH,qBA2Da,4BAA6B,SAAQ,iCAAkC,YAAW,MAAM;IACjG,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA+B;IAC3D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IAE1C,QAAQ,SAAM;IACd,YAAY,SAAM;IAEzB;;;OAGG;IACH,gBAAuB,oBAAoB,EAAE,IAAI,CAAC,yBAAyB,CAAC,CAAiC;IAE7G,kGAAkG;WACpF,gBAAgB,CAAC,gBAAgB,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO;IAQvF,IAAoB,iBAAiB,IAAI,OAAO,CAE/C;IAEK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBxB,YAAY,IAAI,IAAI;IAMX,uBAAuB,IAAI,YAAY,GAAG,IAAI;yCAnDrD,4BAA4B;2CAA5B,4BAA4B;CAiExC"}
@@ -0,0 +1,148 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { ChangeDetectorRef, Component, inject } from '@angular/core';
8
+ import { DataSnapshot } from '@memberjunction/core';
9
+ import { RegisterClass } from '@memberjunction/global';
10
+ import { BaseArtifactViewerPluginComponent } from '../base-artifact-viewer.component';
11
+ import { ArtifactFileService } from '../../services/artifact-file.service';
12
+ import { VideoArtifactPreviewComponent } from '../previews/video-artifact-preview.component';
13
+ import * as i0 from "@angular/core";
14
+ import * as i1 from "@angular/common";
15
+ function VideoArtifactViewerComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
16
+ i0.ɵɵelementStart(0, "div", 1);
17
+ i0.ɵɵelement(1, "i", 4);
18
+ i0.ɵɵelementStart(2, "span");
19
+ i0.ɵɵtext(3);
20
+ i0.ɵɵelementEnd()();
21
+ } if (rf & 2) {
22
+ const ctx_r0 = i0.ɵɵnextContext();
23
+ i0.ɵɵadvance(3);
24
+ i0.ɵɵtextInterpolate(ctx_r0.errorMessage);
25
+ } }
26
+ function VideoArtifactViewerComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
27
+ const _r2 = i0.ɵɵgetCurrentView();
28
+ i0.ɵɵelementStart(0, "video", 5);
29
+ i0.ɵɵlistener("error", function VideoArtifactViewerComponent_Conditional_2_Template_video_error_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onMediaError()); });
30
+ i0.ɵɵelementEnd();
31
+ } if (rf & 2) {
32
+ const ctx_r0 = i0.ɵɵnextContext();
33
+ i0.ɵɵproperty("src", ctx_r0.videoUrl, i0.ɵɵsanitizeUrl);
34
+ } }
35
+ function VideoArtifactViewerComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
36
+ i0.ɵɵelementStart(0, "div", 3);
37
+ i0.ɵɵelement(1, "i", 6);
38
+ i0.ɵɵelementStart(2, "span");
39
+ i0.ɵɵtext(3, "Loading video\u2026");
40
+ i0.ɵɵelementEnd()();
41
+ } }
42
+ /**
43
+ * Minimal full-size viewer plugin for video artifacts. Renders a `<video controls>` player that
44
+ * fills the panel. Supports both storage modes:
45
+ * - `ContentMode === 'File'` — fetch a pre-auth URL from MJStorage via {@link ArtifactFileService}.
46
+ * - `ContentMode === 'Text'` — `Content` is a `data:<mime>;base64,…` URI, bound directly.
47
+ *
48
+ * Deliberately minimal — the richer inline experience lives in {@link VideoArtifactPreviewComponent}
49
+ * (exposed via {@link previewComponentType}); this just needs to satisfy the required `componentType`.
50
+ */
51
+ let VideoArtifactViewerComponent = class VideoArtifactViewerComponent extends BaseArtifactViewerPluginComponent {
52
+ fileService = inject(ArtifactFileService);
53
+ cdr = inject(ChangeDetectorRef);
54
+ videoUrl = '';
55
+ errorMessage = '';
56
+ /**
57
+ * Inline-preview component for video artifacts ({@link IArtifactViewerPluginPreviewStatics}).
58
+ * STATIC so the resolver reads it off the constructor without instantiating this DI-using component.
59
+ */
60
+ static PreviewComponentType = VideoArtifactPreviewComponent;
61
+ /** Matches video artifacts by type name or `video/*` MIME. STATIC — drives preview resolution. */
62
+ static CanHandlePreview(artifactTypeName, contentType) {
63
+ const mime = (contentType ?? '').toLowerCase();
64
+ if (mime.startsWith('video/')) {
65
+ return true;
66
+ }
67
+ return (artifactTypeName ?? '').trim().toLowerCase() === 'video';
68
+ }
69
+ get hasDisplayContent() {
70
+ return true;
71
+ }
72
+ async ngOnInit() {
73
+ try {
74
+ if (this.artifactVersion?.ContentMode === 'File' && this.artifactVersion.ID) {
75
+ this.videoUrl = await this.fileService.getDownloadUrl(this.artifactVersion.ID);
76
+ }
77
+ else {
78
+ const content = this.artifactVersion?.Content;
79
+ if (content) {
80
+ this.videoUrl = content;
81
+ }
82
+ else {
83
+ this.errorMessage = 'This artifact has no video content.';
84
+ }
85
+ }
86
+ }
87
+ catch (err) {
88
+ const detail = err instanceof Error ? err.message : String(err);
89
+ this.errorMessage = `Could not load video: ${detail}`;
90
+ }
91
+ this.cdr.markForCheck();
92
+ }
93
+ onMediaError() {
94
+ this.errorMessage = 'The video could not be played. It may be corrupt or in an unsupported format.';
95
+ this.videoUrl = '';
96
+ this.cdr.markForCheck();
97
+ }
98
+ GetCurrentStateSnapshot() {
99
+ if (!this.artifactVersion) {
100
+ return null;
101
+ }
102
+ const snap = new DataSnapshot();
103
+ snap.title = this.getDisplayTitle() ?? undefined;
104
+ snap.interpretation = `Video (${this.artifactVersion.MimeType ?? 'video'}).`;
105
+ snap.custom = {
106
+ mimeType: this.artifactVersion.MimeType ?? undefined,
107
+ fileName: this.artifactVersion.FileName ?? undefined,
108
+ contentMode: this.artifactVersion.ContentMode,
109
+ };
110
+ return snap;
111
+ }
112
+ static ɵfac = /*@__PURE__*/ (() => { let ɵVideoArtifactViewerComponent_BaseFactory; return function VideoArtifactViewerComponent_Factory(__ngFactoryType__) { return (ɵVideoArtifactViewerComponent_BaseFactory || (ɵVideoArtifactViewerComponent_BaseFactory = i0.ɵɵgetInheritedFactory(VideoArtifactViewerComponent)))(__ngFactoryType__ || VideoArtifactViewerComponent); }; })();
113
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: VideoArtifactViewerComponent, selectors: [["mj-video-artifact-viewer"]], standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 4, vars: 2, consts: [[1, "video-viewer", 3, "ngClass"], [1, "video-viewer__state", "video-viewer__state--error"], ["controls", "", "preload", "metadata", 1, "video-viewer__video", 3, "src"], [1, "video-viewer__state"], [1, "fa-solid", "fa-exclamation-triangle"], ["controls", "", "preload", "metadata", 1, "video-viewer__video", 3, "error", "src"], [1, "fa-solid", "fa-spinner", "fa-spin"]], template: function VideoArtifactViewerComponent_Template(rf, ctx) { if (rf & 1) {
114
+ i0.ɵɵelementStart(0, "div", 0);
115
+ i0.ɵɵconditionalCreate(1, VideoArtifactViewerComponent_Conditional_1_Template, 4, 1, "div", 1)(2, VideoArtifactViewerComponent_Conditional_2_Template, 1, 1, "video", 2)(3, VideoArtifactViewerComponent_Conditional_3_Template, 4, 0, "div", 3);
116
+ i0.ɵɵelementEnd();
117
+ } if (rf & 2) {
118
+ i0.ɵɵproperty("ngClass", ctx.cssClass);
119
+ i0.ɵɵadvance();
120
+ i0.ɵɵconditional(ctx.errorMessage ? 1 : ctx.videoUrl ? 2 : 3);
121
+ } }, dependencies: [i1.NgClass], styles: ["[_nghost-%COMP%] {\n display: block;\n height: 100%;\n }\n\n .video-viewer[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 16px;\n background: var(--mj-bg-surface-sunken);\n }\n\n .video-viewer__video[_ngcontent-%COMP%] {\n max-width: 100%;\n max-height: 100%;\n border-radius: 4px;\n background: var(--mj-bg-surface);\n }\n\n .video-viewer__state[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n color: var(--mj-text-muted);\n font-size: 14px;\n }\n\n .video-viewer__state--error[_ngcontent-%COMP%] {\n color: var(--mj-status-error-text);\n }"] });
122
+ };
123
+ VideoArtifactViewerComponent = __decorate([
124
+ RegisterClass(BaseArtifactViewerPluginComponent, 'VideoArtifactViewerPlugin')
125
+ ], VideoArtifactViewerComponent);
126
+ export { VideoArtifactViewerComponent };
127
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(VideoArtifactViewerComponent, [{
128
+ type: Component,
129
+ args: [{ standalone: false, selector: 'mj-video-artifact-viewer', template: `
130
+ <div class="video-viewer" [ngClass]="cssClass">
131
+ @if (errorMessage) {
132
+ <div class="video-viewer__state video-viewer__state--error">
133
+ <i class="fa-solid fa-exclamation-triangle"></i>
134
+ <span>{{ errorMessage }}</span>
135
+ </div>
136
+ } @else if (videoUrl) {
137
+ <video class="video-viewer__video" controls preload="metadata" [src]="videoUrl" (error)="onMediaError()"></video>
138
+ } @else {
139
+ <div class="video-viewer__state">
140
+ <i class="fa-solid fa-spinner fa-spin"></i>
141
+ <span>Loading video…</span>
142
+ </div>
143
+ }
144
+ </div>
145
+ `, styles: ["\n :host {\n display: block;\n height: 100%;\n }\n\n .video-viewer {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 16px;\n background: var(--mj-bg-surface-sunken);\n }\n\n .video-viewer__video {\n max-width: 100%;\n max-height: 100%;\n border-radius: 4px;\n background: var(--mj-bg-surface);\n }\n\n .video-viewer__state {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n color: var(--mj-text-muted);\n font-size: 14px;\n }\n\n .video-viewer__state--error {\n color: var(--mj-status-error-text);\n }\n "] }]
146
+ }], null, null); })();
147
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(VideoArtifactViewerComponent, { className: "VideoArtifactViewerComponent", filePath: "src/lib/components/plugins/video-artifact-viewer.component.ts", lineNumber: 77 }); })();
148
+ //# sourceMappingURL=video-artifact-viewer.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"video-artifact-viewer.component.js","sourceRoot":"","sources":["../../../../src/lib/components/plugins/video-artifact-viewer.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { ChangeDetectorRef, Component, inject, OnInit, Type } from '@angular/core';\nimport { DataSnapshot } from '@memberjunction/core';\nimport { RegisterClass } from '@memberjunction/global';\nimport { BaseArtifactViewerPluginComponent } from '../base-artifact-viewer.component';\nimport { ArtifactFileService } from '../../services/artifact-file.service';\nimport { IArtifactPreviewComponent } from '../../interfaces/artifact-viewer-plugin.interface';\nimport { VideoArtifactPreviewComponent } from '../previews/video-artifact-preview.component';\n\n/**\n * Minimal full-size viewer plugin for video artifacts. Renders a `<video controls>` player that\n * fills the panel. Supports both storage modes:\n * - `ContentMode === 'File'` — fetch a pre-auth URL from MJStorage via {@link ArtifactFileService}.\n * - `ContentMode === 'Text'` — `Content` is a `data:<mime>;base64,…` URI, bound directly.\n *\n * Deliberately minimal — the richer inline experience lives in {@link VideoArtifactPreviewComponent}\n * (exposed via {@link previewComponentType}); this just needs to satisfy the required `componentType`.\n */\n@Component({\n standalone: false,\n selector: 'mj-video-artifact-viewer',\n template: `\n <div class=\"video-viewer\" [ngClass]=\"cssClass\">\n @if (errorMessage) {\n <div class=\"video-viewer__state video-viewer__state--error\">\n <i class=\"fa-solid fa-exclamation-triangle\"></i>\n <span>{{ errorMessage }}</span>\n </div>\n } @else if (videoUrl) {\n <video class=\"video-viewer__video\" controls preload=\"metadata\" [src]=\"videoUrl\" (error)=\"onMediaError()\"></video>\n } @else {\n <div class=\"video-viewer__state\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n <span>Loading video…</span>\n </div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: block;\n height: 100%;\n }\n\n .video-viewer {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 16px;\n background: var(--mj-bg-surface-sunken);\n }\n\n .video-viewer__video {\n max-width: 100%;\n max-height: 100%;\n border-radius: 4px;\n background: var(--mj-bg-surface);\n }\n\n .video-viewer__state {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n color: var(--mj-text-muted);\n font-size: 14px;\n }\n\n .video-viewer__state--error {\n color: var(--mj-status-error-text);\n }\n `,\n ],\n})\n@RegisterClass(BaseArtifactViewerPluginComponent, 'VideoArtifactViewerPlugin')\nexport class VideoArtifactViewerComponent extends BaseArtifactViewerPluginComponent implements OnInit {\n private readonly fileService = inject(ArtifactFileService);\n private readonly cdr = inject(ChangeDetectorRef);\n\n public videoUrl = '';\n public errorMessage = '';\n\n /**\n * Inline-preview component for video artifacts ({@link IArtifactViewerPluginPreviewStatics}).\n * STATIC so the resolver reads it off the constructor without instantiating this DI-using component.\n */\n public static readonly PreviewComponentType: Type<IArtifactPreviewComponent> = VideoArtifactPreviewComponent;\n\n /** Matches video artifacts by type name or `video/*` MIME. STATIC — drives preview resolution. */\n public static CanHandlePreview(artifactTypeName: string, contentType?: string): boolean {\n const mime = (contentType ?? '').toLowerCase();\n if (mime.startsWith('video/')) {\n return true;\n }\n return (artifactTypeName ?? '').trim().toLowerCase() === 'video';\n }\n\n public override get hasDisplayContent(): boolean {\n return true;\n }\n\n async ngOnInit(): Promise<void> {\n try {\n if (this.artifactVersion?.ContentMode === 'File' && this.artifactVersion.ID) {\n this.videoUrl = await this.fileService.getDownloadUrl(this.artifactVersion.ID);\n } else {\n const content = this.artifactVersion?.Content;\n if (content) {\n this.videoUrl = content;\n } else {\n this.errorMessage = 'This artifact has no video content.';\n }\n }\n } catch (err) {\n const detail = err instanceof Error ? err.message : String(err);\n this.errorMessage = `Could not load video: ${detail}`;\n }\n this.cdr.markForCheck();\n }\n\n public onMediaError(): void {\n this.errorMessage = 'The video could not be played. It may be corrupt or in an unsupported format.';\n this.videoUrl = '';\n this.cdr.markForCheck();\n }\n\n public override GetCurrentStateSnapshot(): DataSnapshot | null {\n if (!this.artifactVersion) {\n return null;\n }\n const snap = new DataSnapshot();\n snap.title = this.getDisplayTitle() ?? undefined;\n snap.interpretation = `Video (${this.artifactVersion.MimeType ?? 'video'}).`;\n snap.custom = {\n mimeType: this.artifactVersion.MimeType ?? undefined,\n fileName: this.artifactVersion.FileName ?? undefined,\n contentMode: this.artifactVersion.ContentMode,\n };\n return snap;\n }\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { OnInit } from '@angular/core';
2
+ import { BaseArtifactPreviewComponent } from './base-artifact-preview.component';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * Inline preview for audio artifacts. Renders an `<audio controls>` mini player inside the
6
+ * conversation message card. Click events are stopped from propagating so using the transport
7
+ * controls does NOT open the full-size viewer.
8
+ */
9
+ export declare class AudioArtifactPreviewComponent extends BaseArtifactPreviewComponent implements OnInit {
10
+ private readonly cdr;
11
+ /** Resolved URL bound to `<audio src>` — data URI (inline) or pre-auth URL (file). */
12
+ audioUrl: string;
13
+ /** Non-empty hides the player and shows a compact error line. */
14
+ errorMessage: string;
15
+ ngOnInit(): Promise<void>;
16
+ onMediaError(): void;
17
+ static ɵfac: i0.ɵɵFactoryDeclaration<AudioArtifactPreviewComponent, never>;
18
+ static ɵcmp: i0.ɵɵComponentDeclaration<AudioArtifactPreviewComponent, "mj-audio-artifact-preview", never, {}, {}, never, never, false, never>;
19
+ }
20
+ //# sourceMappingURL=audio-artifact-preview.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-artifact-preview.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/previews/audio-artifact-preview.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,MAAM,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;;AAEjF;;;;GAIG;AACH,qBA0Ca,6BAA8B,SAAQ,4BAA6B,YAAW,MAAM;IAC7F,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IAEjD,sFAAsF;IAC/E,QAAQ,SAAM;IAErB,iEAAiE;IAC1D,YAAY,SAAM;IAEnB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAcxB,YAAY,IAAI,IAAI;yCAvBlB,6BAA6B;2CAA7B,6BAA6B;CA4BzC"}
@@ -0,0 +1,83 @@
1
+ import { ChangeDetectorRef, Component, inject } from '@angular/core';
2
+ import { BaseArtifactPreviewComponent } from './base-artifact-preview.component';
3
+ import * as i0 from "@angular/core";
4
+ function AudioArtifactPreviewComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
5
+ i0.ɵɵelementStart(0, "div", 0);
6
+ i0.ɵɵelement(1, "i", 2);
7
+ i0.ɵɵelementStart(2, "span");
8
+ i0.ɵɵtext(3);
9
+ i0.ɵɵelementEnd()();
10
+ } if (rf & 2) {
11
+ const ctx_r0 = i0.ɵɵnextContext();
12
+ i0.ɵɵadvance(3);
13
+ i0.ɵɵtextInterpolate(ctx_r0.errorMessage);
14
+ } }
15
+ function AudioArtifactPreviewComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
16
+ const _r2 = i0.ɵɵgetCurrentView();
17
+ i0.ɵɵelementStart(0, "audio", 3);
18
+ i0.ɵɵlistener("click", function AudioArtifactPreviewComponent_Conditional_1_Template_audio_click_0_listener($event) { i0.ɵɵrestoreView(_r2); return i0.ɵɵresetView($event.stopPropagation()); })("error", function AudioArtifactPreviewComponent_Conditional_1_Template_audio_error_0_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onMediaError()); });
19
+ i0.ɵɵelementEnd();
20
+ } if (rf & 2) {
21
+ const ctx_r0 = i0.ɵɵnextContext();
22
+ i0.ɵɵproperty("src", ctx_r0.audioUrl);
23
+ } }
24
+ /**
25
+ * Inline preview for audio artifacts. Renders an `<audio controls>` mini player inside the
26
+ * conversation message card. Click events are stopped from propagating so using the transport
27
+ * controls does NOT open the full-size viewer.
28
+ */
29
+ export class AudioArtifactPreviewComponent extends BaseArtifactPreviewComponent {
30
+ cdr = inject(ChangeDetectorRef);
31
+ /** Resolved URL bound to `<audio src>` — data URI (inline) or pre-auth URL (file). */
32
+ audioUrl = '';
33
+ /** Non-empty hides the player and shows a compact error line. */
34
+ errorMessage = '';
35
+ async ngOnInit() {
36
+ try {
37
+ const url = await this.resolveContentUrl();
38
+ if (url) {
39
+ this.audioUrl = url;
40
+ }
41
+ else {
42
+ this.errorMessage = 'No audio content.';
43
+ }
44
+ }
45
+ catch {
46
+ this.errorMessage = 'Could not load audio.';
47
+ }
48
+ this.cdr.markForCheck();
49
+ }
50
+ onMediaError() {
51
+ this.errorMessage = 'Audio could not be played.';
52
+ this.audioUrl = '';
53
+ this.cdr.markForCheck();
54
+ }
55
+ static ɵfac = /*@__PURE__*/ (() => { let ɵAudioArtifactPreviewComponent_BaseFactory; return function AudioArtifactPreviewComponent_Factory(__ngFactoryType__) { return (ɵAudioArtifactPreviewComponent_BaseFactory || (ɵAudioArtifactPreviewComponent_BaseFactory = i0.ɵɵgetInheritedFactory(AudioArtifactPreviewComponent)))(__ngFactoryType__ || AudioArtifactPreviewComponent); }; })();
56
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: AudioArtifactPreviewComponent, selectors: [["mj-audio-artifact-preview"]], standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 2, vars: 1, consts: [[1, "audio-preview", "audio-preview--error"], ["controls", "", "preload", "metadata", 1, "audio-preview__audio", 3, "src"], [1, "fa-solid", "fa-music"], ["controls", "", "preload", "metadata", 1, "audio-preview__audio", 3, "click", "error", "src"]], template: function AudioArtifactPreviewComponent_Template(rf, ctx) { if (rf & 1) {
57
+ i0.ɵɵconditionalCreate(0, AudioArtifactPreviewComponent_Conditional_0_Template, 4, 1, "div", 0)(1, AudioArtifactPreviewComponent_Conditional_1_Template, 1, 1, "audio", 1);
58
+ } if (rf & 2) {
59
+ i0.ɵɵconditional(ctx.errorMessage ? 0 : ctx.audioUrl ? 1 : -1);
60
+ } }, styles: ["[_nghost-%COMP%] {\n display: block;\n }\n\n .audio-preview__audio[_ngcontent-%COMP%] {\n display: block;\n width: 100%;\n }\n\n .audio-preview--error[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 16px;\n color: var(--mj-text-muted);\n font-size: 13px;\n }"] });
61
+ }
62
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(AudioArtifactPreviewComponent, [{
63
+ type: Component,
64
+ args: [{ standalone: false, selector: 'mj-audio-artifact-preview', template: `
65
+ @if (errorMessage) {
66
+ <div class="audio-preview audio-preview--error">
67
+ <i class="fa-solid fa-music"></i>
68
+ <span>{{ errorMessage }}</span>
69
+ </div>
70
+ } @else if (audioUrl) {
71
+ <audio
72
+ class="audio-preview__audio"
73
+ controls
74
+ preload="metadata"
75
+ [src]="audioUrl"
76
+ (click)="$event.stopPropagation()"
77
+ (error)="onMediaError()"
78
+ ></audio>
79
+ }
80
+ `, styles: ["\n :host {\n display: block;\n }\n\n .audio-preview__audio {\n display: block;\n width: 100%;\n }\n\n .audio-preview--error {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 16px;\n color: var(--mj-text-muted);\n font-size: 13px;\n }\n "] }]
81
+ }], null, null); })();
82
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(AudioArtifactPreviewComponent, { className: "AudioArtifactPreviewComponent", filePath: "src/lib/components/previews/audio-artifact-preview.component.ts", lineNumber: 51 }); })();
83
+ //# sourceMappingURL=audio-artifact-preview.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audio-artifact-preview.component.js","sourceRoot":"","sources":["../../../../src/lib/components/previews/audio-artifact-preview.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,EAAU,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;;;IAYrE,8BAAgD;IAC5C,uBAAiC;IACjC,4BAAM;IAAA,YAAkB;IAC5B,AAD4B,iBAAO,EAC7B;;;IADI,eAAkB;IAAlB,yCAAkB;;;;IAG5B,gCAOC;IADG,AADA,mKAAS,wBAAwB,KAAC,mLACzB,qBAAc,KAAC;IAC3B,iBAAQ;;;IAHL,qCAAgB;;AAnBhC;;;;GAIG;AA2CH,MAAM,OAAO,6BAA8B,SAAQ,4BAA4B;IAC1D,GAAG,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;IAEjD,sFAAsF;IAC/E,QAAQ,GAAG,EAAE,CAAC;IAErB,iEAAiE;IAC1D,YAAY,GAAG,EAAE,CAAC;IAEzB,KAAK,CAAC,QAAQ;QACV,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAC3C,IAAI,GAAG,EAAE,CAAC;gBACN,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC;YACxB,CAAC;iBAAM,CAAC;gBACJ,IAAI,CAAC,YAAY,GAAG,mBAAmB,CAAC;YAC5C,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACL,IAAI,CAAC,YAAY,GAAG,uBAAuB,CAAC;QAChD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;IAC5B,CAAC;IAEM,YAAY;QACf,IAAI,CAAC,YAAY,GAAG,4BAA4B,CAAC;QACjD,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,CAAC;IAC5B,CAAC;iSA3BQ,6BAA6B,yBAA7B,6BAA6B;6DAA7B,6BAA6B;YAjChC,AALF,+FAAoB,2EAKG;;YALvB,8DAcC;;;iFAwBI,6BAA6B;cA1CzC,SAAS;6BACM,KAAK,YACP,2BAA2B,YAC3B;;;;;;;;;;;;;;;;KAgBT;;kFAuBQ,6BAA6B","sourcesContent":["import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';\nimport { BaseArtifactPreviewComponent } from './base-artifact-preview.component';\n\n/**\n * Inline preview for audio artifacts. Renders an `<audio controls>` mini player inside the\n * conversation message card. Click events are stopped from propagating so using the transport\n * controls does NOT open the full-size viewer.\n */\n@Component({\n standalone: false,\n selector: 'mj-audio-artifact-preview',\n template: `\n @if (errorMessage) {\n <div class=\"audio-preview audio-preview--error\">\n <i class=\"fa-solid fa-music\"></i>\n <span>{{ errorMessage }}</span>\n </div>\n } @else if (audioUrl) {\n <audio\n class=\"audio-preview__audio\"\n controls\n preload=\"metadata\"\n [src]=\"audioUrl\"\n (click)=\"$event.stopPropagation()\"\n (error)=\"onMediaError()\"\n ></audio>\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n .audio-preview__audio {\n display: block;\n width: 100%;\n }\n\n .audio-preview--error {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 12px 16px;\n color: var(--mj-text-muted);\n font-size: 13px;\n }\n `,\n ],\n})\nexport class AudioArtifactPreviewComponent extends BaseArtifactPreviewComponent implements OnInit {\n private readonly cdr = inject(ChangeDetectorRef);\n\n /** Resolved URL bound to `<audio src>` — data URI (inline) or pre-auth URL (file). */\n public audioUrl = '';\n\n /** Non-empty hides the player and shows a compact error line. */\n public errorMessage = '';\n\n async ngOnInit(): Promise<void> {\n try {\n const url = await this.resolveContentUrl();\n if (url) {\n this.audioUrl = url;\n } else {\n this.errorMessage = 'No audio content.';\n }\n } catch {\n this.errorMessage = 'Could not load audio.';\n }\n this.cdr.markForCheck();\n }\n\n public onMediaError(): void {\n this.errorMessage = 'Audio could not be played.';\n this.audioUrl = '';\n this.cdr.markForCheck();\n }\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import { MJArtifactVersionEntity } from '@memberjunction/core-entities';
2
+ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
3
+ import { IArtifactPreviewComponent } from '../../interfaces/artifact-viewer-plugin.interface';
4
+ import { ArtifactFileService } from '../../services/artifact-file.service';
5
+ import * as i0 from "@angular/core";
6
+ /**
7
+ * Thin abstract base for inline artifact PREVIEW components.
8
+ *
9
+ * Deliberately does NOT extend the heavy {@link BaseArtifactViewerPluginComponent} — previews
10
+ * are lightweight, read-only, and have no toolbar / feedback / tabs / snapshot contract. They
11
+ * render a compact representation of the artifact inside a conversation message card.
12
+ *
13
+ * Content resolution (inline data-URL vs. file-storage download URL) is shared with the full
14
+ * viewers via the injected {@link ArtifactFileService}, so previews and viewers load bytes the
15
+ * same way. Subclasses call {@link resolveContentUrl} in their `ngOnInit`.
16
+ */
17
+ export declare abstract class BaseArtifactPreviewComponent extends BaseAngularComponent implements IArtifactPreviewComponent {
18
+ /** The artifact version to preview. Set by the resolver before the component renders. */
19
+ artifactVersion: MJArtifactVersionEntity;
20
+ /** Shared service used by both previews and full viewers to fetch file-mode download URLs. */
21
+ protected readonly fileService: ArtifactFileService;
22
+ /**
23
+ * Resolve a URL to bind to a media element (`<img>`, `<video>`, `<audio>`):
24
+ * - `ContentMode === 'File'` → fetch a pre-authenticated download URL from MJStorage.
25
+ * - otherwise (`'Text'`) → `Content` is already a `data:<mime>;base64,…` URI; bind directly.
26
+ *
27
+ * Returns `null` when there is no usable content (caller should show its own empty/error state).
28
+ */
29
+ protected resolveContentUrl(): Promise<string | null>;
30
+ static ɵfac: i0.ɵɵFactoryDeclaration<BaseArtifactPreviewComponent, never>;
31
+ static ɵcmp: i0.ɵɵComponentDeclaration<BaseArtifactPreviewComponent, "ng-component", never, { "artifactVersion": { "alias": "artifactVersion"; "required": false; }; }, {}, never, never, false, never>;
32
+ }
33
+ //# sourceMappingURL=base-artifact-preview.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-artifact-preview.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/previews/base-artifact-preview.component.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,+BAA+B,CAAC;AACxE,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,mDAAmD,CAAC;AAC9F,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;;AAE3E;;;;;;;;;;GAUG;AACH,8BAIsB,4BAA6B,SAAQ,oBAAqB,YAAW,yBAAyB;IAChH,yFAAyF;IAChF,eAAe,EAAG,uBAAuB,CAAC;IAEnD,8FAA8F;IAC9F,SAAS,CAAC,QAAQ,CAAC,WAAW,sBAA+B;IAE7D;;;;;;OAMG;cACa,iBAAiB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;yCAdzC,4BAA4B;2CAA5B,4BAA4B;CA2BjD"}
@@ -0,0 +1,52 @@
1
+ import { Component, inject, Input } from '@angular/core';
2
+ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
3
+ import { ArtifactFileService } from '../../services/artifact-file.service';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Thin abstract base for inline artifact PREVIEW components.
7
+ *
8
+ * Deliberately does NOT extend the heavy {@link BaseArtifactViewerPluginComponent} — previews
9
+ * are lightweight, read-only, and have no toolbar / feedback / tabs / snapshot contract. They
10
+ * render a compact representation of the artifact inside a conversation message card.
11
+ *
12
+ * Content resolution (inline data-URL vs. file-storage download URL) is shared with the full
13
+ * viewers via the injected {@link ArtifactFileService}, so previews and viewers load bytes the
14
+ * same way. Subclasses call {@link resolveContentUrl} in their `ngOnInit`.
15
+ */
16
+ export class BaseArtifactPreviewComponent extends BaseAngularComponent {
17
+ /** The artifact version to preview. Set by the resolver before the component renders. */
18
+ artifactVersion;
19
+ /** Shared service used by both previews and full viewers to fetch file-mode download URLs. */
20
+ fileService = inject(ArtifactFileService);
21
+ /**
22
+ * Resolve a URL to bind to a media element (`<img>`, `<video>`, `<audio>`):
23
+ * - `ContentMode === 'File'` → fetch a pre-authenticated download URL from MJStorage.
24
+ * - otherwise (`'Text'`) → `Content` is already a `data:<mime>;base64,…` URI; bind directly.
25
+ *
26
+ * Returns `null` when there is no usable content (caller should show its own empty/error state).
27
+ */
28
+ async resolveContentUrl() {
29
+ const version = this.artifactVersion;
30
+ if (!version?.ID) {
31
+ return null;
32
+ }
33
+ if (version.ContentMode === 'File') {
34
+ return this.fileService.getDownloadUrl(version.ID);
35
+ }
36
+ const content = version.Content;
37
+ return content && content.length > 0 ? content : null;
38
+ }
39
+ static ɵfac = /*@__PURE__*/ (() => { let ɵBaseArtifactPreviewComponent_BaseFactory; return function BaseArtifactPreviewComponent_Factory(__ngFactoryType__) { return (ɵBaseArtifactPreviewComponent_BaseFactory || (ɵBaseArtifactPreviewComponent_BaseFactory = i0.ɵɵgetInheritedFactory(BaseArtifactPreviewComponent)))(__ngFactoryType__ || BaseArtifactPreviewComponent); }; })();
40
+ static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: BaseArtifactPreviewComponent, selectors: [["ng-component"]], inputs: { artifactVersion: "artifactVersion" }, standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 0, vars: 0, template: function BaseArtifactPreviewComponent_Template(rf, ctx) { }, encapsulation: 2 });
41
+ }
42
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(BaseArtifactPreviewComponent, [{
43
+ type: Component,
44
+ args: [{
45
+ standalone: false,
46
+ template: '',
47
+ }]
48
+ }], null, { artifactVersion: [{
49
+ type: Input
50
+ }] }); })();
51
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(BaseArtifactPreviewComponent, { className: "BaseArtifactPreviewComponent", filePath: "src/lib/components/previews/base-artifact-preview.component.ts", lineNumber: 22 }); })();
52
+ //# sourceMappingURL=base-artifact-preview.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-artifact-preview.component.js","sourceRoot":"","sources":["../../../../src/lib/components/previews/base-artifact-preview.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAEzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAErE,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;;AAE3E;;;;;;;;;;GAUG;AAKH,MAAM,OAAgB,4BAA6B,SAAQ,oBAAoB;IAC3E,yFAAyF;IAChF,eAAe,CAA2B;IAEnD,8FAA8F;IAC3E,WAAW,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAE7D;;;;;;OAMG;IACO,KAAK,CAAC,iBAAiB;QAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC;QACrC,IAAI,CAAC,OAAO,EAAE,EAAE,EAAE,CAAC;YACf,OAAO,IAAI,CAAC;QAChB,CAAC;QAED,IAAI,OAAO,CAAC,WAAW,KAAK,MAAM,EAAE,CAAC;YACjC,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACvD,CAAC;QAED,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,OAAO,OAAO,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1D,CAAC;6RA1BiB,4BAA4B,yBAA5B,4BAA4B;6DAA5B,4BAA4B;;iFAA5B,4BAA4B;cAJjD,SAAS;eAAC;gBACP,UAAU,EAAE,KAAK;gBACjB,QAAQ,EAAE,EAAE;aACf;;kBAGI,KAAK;;kFAFY,4BAA4B","sourcesContent":["import { Component, inject, Input } from '@angular/core';\nimport { MJArtifactVersionEntity } from '@memberjunction/core-entities';\nimport { BaseAngularComponent } from '@memberjunction/ng-base-types';\nimport { IArtifactPreviewComponent } from '../../interfaces/artifact-viewer-plugin.interface';\nimport { ArtifactFileService } from '../../services/artifact-file.service';\n\n/**\n * Thin abstract base for inline artifact PREVIEW components.\n *\n * Deliberately does NOT extend the heavy {@link BaseArtifactViewerPluginComponent} — previews\n * are lightweight, read-only, and have no toolbar / feedback / tabs / snapshot contract. They\n * render a compact representation of the artifact inside a conversation message card.\n *\n * Content resolution (inline data-URL vs. file-storage download URL) is shared with the full\n * viewers via the injected {@link ArtifactFileService}, so previews and viewers load bytes the\n * same way. Subclasses call {@link resolveContentUrl} in their `ngOnInit`.\n */\n@Component({\n standalone: false,\n template: '',\n})\nexport abstract class BaseArtifactPreviewComponent extends BaseAngularComponent implements IArtifactPreviewComponent {\n /** The artifact version to preview. Set by the resolver before the component renders. */\n @Input() artifactVersion!: MJArtifactVersionEntity;\n\n /** Shared service used by both previews and full viewers to fetch file-mode download URLs. */\n protected readonly fileService = inject(ArtifactFileService);\n\n /**\n * Resolve a URL to bind to a media element (`<img>`, `<video>`, `<audio>`):\n * - `ContentMode === 'File'` → fetch a pre-authenticated download URL from MJStorage.\n * - otherwise (`'Text'`) → `Content` is already a `data:<mime>;base64,…` URI; bind directly.\n *\n * Returns `null` when there is no usable content (caller should show its own empty/error state).\n */\n protected async resolveContentUrl(): Promise<string | null> {\n const version = this.artifactVersion;\n if (!version?.ID) {\n return null;\n }\n\n if (version.ContentMode === 'File') {\n return this.fileService.getDownloadUrl(version.ID);\n }\n\n const content = version.Content;\n return content && content.length > 0 ? content : null;\n }\n}\n"]}
@@ -0,0 +1,28 @@
1
+ import { OnInit } from '@angular/core';
2
+ import { BaseArtifactPreviewComponent } from './base-artifact-preview.component';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * Inline preview for image artifacts. Renders a single contained `<img>` (max 140px tall) inside
6
+ * the conversation message card. Read-only — no toolbar, no zoom, no download. Clicking it bubbles
7
+ * up through the card's clickable wrapper to open the full-size image viewer.
8
+ *
9
+ * Height note: this is a *thumbnail*, not the full view — kept deliberately compact (140px) so a
10
+ * message with an image artifact doesn't dominate the conversation. `max-width: 100%` (rather than a
11
+ * fixed width) is the only width constraint, so the aspect ratio is preserved and portrait images
12
+ * aren't distorted; the height cap is the single lever for overall size. Kept in sync with the video
13
+ * preview's cap so both visual media previews stay visually consistent.
14
+ */
15
+ export declare class ImageArtifactPreviewComponent extends BaseArtifactPreviewComponent implements OnInit {
16
+ private readonly cdr;
17
+ /** Resolved URL bound to `<img src>` — data URI (inline) or pre-auth URL (file). */
18
+ imageUrl: string;
19
+ /** Non-empty hides the image and shows a compact error line. */
20
+ errorMessage: string;
21
+ /** Accessible alt text — prefers a descriptive name over a raw filename. */
22
+ get altText(): string;
23
+ ngOnInit(): Promise<void>;
24
+ onImageError(): void;
25
+ static ɵfac: i0.ɵɵFactoryDeclaration<ImageArtifactPreviewComponent, never>;
26
+ static ɵcmp: i0.ɵɵComponentDeclaration<ImageArtifactPreviewComponent, "mj-image-artifact-preview", never, {}, {}, never, never, false, never>;
27
+ }
28
+ //# sourceMappingURL=image-artifact-preview.component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-artifact-preview.component.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/previews/image-artifact-preview.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwC,MAAM,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;;AAEjF;;;;;;;;;;GAUG;AACH,qBA8Ca,6BAA8B,SAAQ,4BAA6B,YAAW,MAAM;IAC7F,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA6B;IAEjD,oFAAoF;IAC7E,QAAQ,SAAM;IAErB,gEAAgE;IACzD,YAAY,SAAM;IAEzB,4EAA4E;IAC5E,IAAW,OAAO,IAAI,MAAM,CAE3B;IAEK,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAcxB,YAAY,IAAI,IAAI;yCA5BlB,6BAA6B;2CAA7B,6BAA6B;CAiCzC"}