@memberjunction/ng-conversations 5.49.0 → 5.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -3
- package/dist/lib/components/collection/collections-full-view.component.d.ts +10 -0
- package/dist/lib/components/collection/collections-full-view.component.d.ts.map +1 -1
- package/dist/lib/components/collection/collections-full-view.component.js +74 -14
- package/dist/lib/components/collection/collections-full-view.component.js.map +1 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts +31 -5
- package/dist/lib/components/conversation/conversation-chat-area.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/conversation-chat-area.component.js +169 -101
- package/dist/lib/components/conversation/conversation-chat-area.component.js.map +1 -1
- package/dist/lib/components/conversation/conversation-empty-state.component.d.ts +5 -1
- package/dist/lib/components/conversation/conversation-empty-state.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/conversation-empty-state.component.js +13 -3
- package/dist/lib/components/conversation/conversation-empty-state.component.js.map +1 -1
- package/dist/lib/components/conversation/conversation-list.component.d.ts +16 -1
- package/dist/lib/components/conversation/conversation-list.component.d.ts.map +1 -1
- package/dist/lib/components/conversation/conversation-list.component.js +439 -351
- package/dist/lib/components/conversation/conversation-list.component.js.map +1 -1
- package/dist/lib/components/export/export-modal.component.d.ts +25 -3
- package/dist/lib/components/export/export-modal.component.d.ts.map +1 -1
- package/dist/lib/components/export/export-modal.component.js +172 -34
- package/dist/lib/components/export/export-modal.component.js.map +1 -1
- package/dist/lib/components/message/message-input.component.d.ts +4 -1
- package/dist/lib/components/message/message-input.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-input.component.js +15 -3
- package/dist/lib/components/message/message-input.component.js.map +1 -1
- package/dist/lib/components/message/message-item.component.d.ts +46 -1
- package/dist/lib/components/message/message-item.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-item.component.js +295 -201
- package/dist/lib/components/message/message-item.component.js.map +1 -1
- package/dist/lib/components/message/message-list.component.d.ts +19 -1
- package/dist/lib/components/message/message-list.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-list.component.js +55 -1
- package/dist/lib/components/message/message-list.component.js.map +1 -1
- package/dist/lib/components/sidebar/conversation-sidebar.component.d.ts +14 -1
- package/dist/lib/components/sidebar/conversation-sidebar.component.d.ts.map +1 -1
- package/dist/lib/components/sidebar/conversation-sidebar.component.js +38 -5
- package/dist/lib/components/sidebar/conversation-sidebar.component.js.map +1 -1
- package/dist/lib/components/slots/slot-interfaces.d.ts +23 -1
- package/dist/lib/components/slots/slot-interfaces.d.ts.map +1 -1
- package/dist/lib/components/slots/slot-interfaces.js.map +1 -1
- package/dist/lib/directives/chat-slot.directive.d.ts +10 -2
- package/dist/lib/directives/chat-slot.directive.d.ts.map +1 -1
- package/dist/lib/directives/chat-slot.directive.js.map +1 -1
- package/dist/lib/models/lazy-artifact-info.d.ts +8 -7
- package/dist/lib/models/lazy-artifact-info.d.ts.map +1 -1
- package/dist/lib/models/lazy-artifact-info.js +12 -11
- package/dist/lib/models/lazy-artifact-info.js.map +1 -1
- package/dist/lib/services/export.service.d.ts +149 -0
- package/dist/lib/services/export.service.d.ts.map +1 -1
- package/dist/lib/services/export.service.js +270 -36
- package/dist/lib/services/export.service.js.map +1 -1
- package/package.json +29 -29
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
2
|
import { RunView, Metadata } from '@memberjunction/core';
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
|
+
/**
|
|
5
|
+
* Tokens captured by the theme auto-snapshot — exactly the custom properties the
|
|
6
|
+
* HTML export's stylesheet consumes. Hosts wanting more control supply their own
|
|
7
|
+
* map via {@link ExportBranding.brandTokens} instead.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_EXPORT_THEME_TOKENS = [
|
|
10
|
+
'--mj-bg-page',
|
|
11
|
+
'--mj-text-primary',
|
|
12
|
+
'--mj-text-secondary',
|
|
13
|
+
'--mj-text-disabled',
|
|
14
|
+
'--mj-border-default',
|
|
15
|
+
'--mj-brand-primary',
|
|
16
|
+
'--mj-brand-accent-subtle',
|
|
17
|
+
'--mj-bg-surface-card',
|
|
18
|
+
'--mj-font-family',
|
|
19
|
+
'--mj-radius-md',
|
|
20
|
+
];
|
|
4
21
|
export class ExportService {
|
|
5
22
|
_provider = null;
|
|
6
23
|
/**
|
|
@@ -14,41 +31,219 @@ export class ExportService {
|
|
|
14
31
|
}
|
|
15
32
|
async exportConversation(conversationId, format, currentUser, options = {}) {
|
|
16
33
|
const conversation = await this.loadConversationData(conversationId, currentUser);
|
|
17
|
-
|
|
34
|
+
const { content, filename, mimeType } = await this.BuildExportContent(conversation, format, options);
|
|
35
|
+
this.downloadFile(content, filename, mimeType);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Build the export document for already-loaded conversation data — the
|
|
39
|
+
* download-free seam of {@link exportConversation}. Async because HTML-format
|
|
40
|
+
* branding may inline a logo. Exposed publicly so hosts (and tests) can build
|
|
41
|
+
* export content without triggering a browser download.
|
|
42
|
+
*/
|
|
43
|
+
async BuildExportContent(data, format, options = {}) {
|
|
18
44
|
const exportOptions = {
|
|
19
45
|
includeMessages: options.includeMessages ?? true,
|
|
20
46
|
includeMetadata: options.includeMetadata ?? true,
|
|
21
47
|
prettyPrint: options.prettyPrint ?? true,
|
|
22
|
-
includeCSS: options.includeCSS ?? true
|
|
48
|
+
includeCSS: options.includeCSS ?? true,
|
|
49
|
+
includeTheme: options.includeTheme ?? false,
|
|
50
|
+
themeMode: options.themeMode ?? 'light',
|
|
51
|
+
branding: options.branding
|
|
23
52
|
};
|
|
24
|
-
|
|
25
|
-
let filename;
|
|
26
|
-
let mimeType;
|
|
53
|
+
const baseName = `conversation-${data.conversation.Name}-${this.getTimestamp()}`;
|
|
27
54
|
switch (format) {
|
|
28
55
|
case 'json':
|
|
29
|
-
content
|
|
30
|
-
filename = `conversation-${conversation.conversation.Name}-${this.getTimestamp()}.json`;
|
|
31
|
-
mimeType = 'application/json';
|
|
32
|
-
break;
|
|
56
|
+
return { content: this.exportAsJSON(data, exportOptions), filename: `${baseName}.json`, mimeType: 'application/json' };
|
|
33
57
|
case 'markdown':
|
|
34
|
-
content
|
|
35
|
-
filename = `conversation-${conversation.conversation.Name}-${this.getTimestamp()}.md`;
|
|
36
|
-
mimeType = 'text/markdown';
|
|
37
|
-
break;
|
|
58
|
+
return { content: this.exportAsMarkdown(data, exportOptions), filename: `${baseName}.md`, mimeType: 'text/markdown' };
|
|
38
59
|
case 'html':
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
return {
|
|
61
|
+
content: this.exportAsHTML(data, exportOptions, await this.resolveTheme(exportOptions)),
|
|
62
|
+
filename: `${baseName}.html`,
|
|
63
|
+
mimeType: 'text/html'
|
|
64
|
+
};
|
|
43
65
|
case 'text':
|
|
44
|
-
content
|
|
45
|
-
filename = `conversation-${conversation.conversation.Name}-${this.getTimestamp()}.txt`;
|
|
46
|
-
mimeType = 'text/plain';
|
|
47
|
-
break;
|
|
66
|
+
return { content: this.exportAsText(data, exportOptions), filename: `${baseName}.txt`, mimeType: 'text/plain' };
|
|
48
67
|
default:
|
|
49
68
|
throw new Error(`Unsupported export format: ${format}`);
|
|
50
69
|
}
|
|
51
|
-
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Snapshot the given theme tokens from the live document. Empty/unset tokens are
|
|
73
|
+
* skipped.
|
|
74
|
+
*
|
|
75
|
+
* `mode` captures a SPECIFIC palette rather than whatever the app is rendering right
|
|
76
|
+
* now. Without it, exporting from a dark session bakes dark text onto the export's
|
|
77
|
+
* white page and the file is unreadable. When the requested mode isn't the active one,
|
|
78
|
+
* `data-theme` is flipped on `documentElement`, read, and restored — all synchronous
|
|
79
|
+
* within one task, so the browser never paints the intermediate state. Omit `mode`
|
|
80
|
+
* to read the live values as-is.
|
|
81
|
+
*/
|
|
82
|
+
SnapshotBrandTokens(tokens = DEFAULT_EXPORT_THEME_TOKENS, mode) {
|
|
83
|
+
const root = document.documentElement;
|
|
84
|
+
const previous = root.getAttribute('data-theme');
|
|
85
|
+
// The app's two real states are `data-theme="dark"` and NO attribute at all —
|
|
86
|
+
// `MJExplorerAppComponent` removes it for light rather than writing "light"
|
|
87
|
+
// (explorer-app.component.ts). Reproduce those exactly rather than inventing a
|
|
88
|
+
// third state, so the transient DOM is one the stylesheet has actually seen.
|
|
89
|
+
const wantDark = mode === 'dark';
|
|
90
|
+
const isDark = previous === 'dark';
|
|
91
|
+
const mustSwap = mode != null && wantDark !== isDark;
|
|
92
|
+
if (mustSwap) {
|
|
93
|
+
if (wantDark) {
|
|
94
|
+
root.setAttribute('data-theme', 'dark');
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
root.removeAttribute('data-theme');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const style = getComputedStyle(root);
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const token of tokens) {
|
|
104
|
+
const value = style.getPropertyValue(token).trim();
|
|
105
|
+
if (value) {
|
|
106
|
+
out[token] = value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
// Restore even if reading throws — never strand the app in the other theme.
|
|
113
|
+
if (mustSwap) {
|
|
114
|
+
if (previous === null) {
|
|
115
|
+
root.removeAttribute('data-theme');
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
root.setAttribute('data-theme', previous);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Resolve the HTML export's theme pieces once: tokens (explicit > snapshot), logo, title. */
|
|
124
|
+
async resolveTheme(options) {
|
|
125
|
+
const tokens = this.resolveBrandTokens(options);
|
|
126
|
+
const rootBlock = tokens ? this.buildRootBlock(tokens) : '';
|
|
127
|
+
const logoUrl = options.branding?.logoUrl?.trim();
|
|
128
|
+
return {
|
|
129
|
+
rootBlock,
|
|
130
|
+
// The logo needs its `.brand-logo` rule (as the trademark footer needs its own),
|
|
131
|
+
// so both are gated on includeCSS — a CSS-less export must never carry a
|
|
132
|
+
// full-size unstyled logo. The modal enforces this too, but the public
|
|
133
|
+
// BuildExportContent seam has to hold the line on its own.
|
|
134
|
+
logoDataUri: options.includeCSS && logoUrl ? await this.inlineLogo(logoUrl) : null,
|
|
135
|
+
// The title is deliberately NOT includeCSS-gated: it lands in <title>/<h1>, needs
|
|
136
|
+
// no stylesheet, and is the one branding field every format carries unconditionally.
|
|
137
|
+
title: options.branding?.title?.trim() || null
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/** Explicit host tokens win; `includeTheme` falls back to a live snapshot; else no theme. */
|
|
141
|
+
resolveBrandTokens(options) {
|
|
142
|
+
const explicit = options.branding?.brandTokens;
|
|
143
|
+
if (explicit && Object.keys(explicit).length > 0) {
|
|
144
|
+
return explicit;
|
|
145
|
+
}
|
|
146
|
+
return options.includeTheme ? this.SnapshotBrandTokens(DEFAULT_EXPORT_THEME_TOKENS, options.themeMode) : null;
|
|
147
|
+
}
|
|
148
|
+
/** Document title used by every format: branding override → conversation name → generic. */
|
|
149
|
+
resolveTitle(data, options) {
|
|
150
|
+
return options.branding?.title?.trim() || data.conversation.Name || 'Conversation';
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The JSON `branding` block — the data-format analogue of the HTML header +
|
|
154
|
+
* trademark footer. Carries the title, trademark, and logo URL the host set;
|
|
155
|
+
* null when no branding (or none of those fields) is present. Theme color
|
|
156
|
+
* tokens are HTML-only and never included here.
|
|
157
|
+
*/
|
|
158
|
+
buildBrandingBlock(options) {
|
|
159
|
+
const b = options.branding;
|
|
160
|
+
if (!b) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const block = {};
|
|
164
|
+
const title = b.title?.trim();
|
|
165
|
+
const trademark = b.trademark?.trim();
|
|
166
|
+
const logoUrl = b.logoUrl?.trim();
|
|
167
|
+
if (title)
|
|
168
|
+
block.title = title;
|
|
169
|
+
if (trademark)
|
|
170
|
+
block.trademark = trademark;
|
|
171
|
+
if (logoUrl)
|
|
172
|
+
block.logoUrl = logoUrl;
|
|
173
|
+
return Object.keys(block).length > 0 ? block : null;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* CSS functions permitted inside an exported token value. This is an ALLOWLIST
|
|
177
|
+
* (not a blocklist) so no network-capable function can slip through: blocking
|
|
178
|
+
* only `url(`/`expression(` let `image-set()`, `-webkit-image-set()`,
|
|
179
|
+
* `cross-fade()`, `image()`, and `src()` fetch a beacon URL without ever
|
|
180
|
+
* writing the literal `url(`. Anything not on this list drops the whole entry.
|
|
181
|
+
* The set is the safe value/color-math functions the export could legitimately
|
|
182
|
+
* carry; extend it only with functions that CANNOT load an external resource.
|
|
183
|
+
*/
|
|
184
|
+
static SAFE_CSS_FUNCTIONS = new Set([
|
|
185
|
+
'var', 'calc', 'min', 'max', 'clamp',
|
|
186
|
+
'rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch',
|
|
187
|
+
'color', 'color-mix',
|
|
188
|
+
]);
|
|
189
|
+
/**
|
|
190
|
+
* Emit the sanitized `:root{}` style block. Keys must be custom-property names.
|
|
191
|
+
* A value is REJECTED (the entry is dropped, not stripped) when it carries any
|
|
192
|
+
* declaration/tag-escape or network-reach vector: `<>{}` (tag/rule breakout),
|
|
193
|
+
* `;` (declaration breakout — e.g. `; background: url(beacon)`), `\` (CSS
|
|
194
|
+
* escapes), `@` (at-rules), OR any CSS function not in
|
|
195
|
+
* {@link SAFE_CSS_FUNCTIONS} (which excludes every resource-loading function —
|
|
196
|
+
* url/image-set/cross-fade/image/src/expression/…). Plain colors, `var()`, and
|
|
197
|
+
* `color-mix()` pass untouched.
|
|
198
|
+
*/
|
|
199
|
+
buildRootBlock(tokens) {
|
|
200
|
+
const rules = [];
|
|
201
|
+
for (const [key, rawValue] of Object.entries(tokens)) {
|
|
202
|
+
if (!/^--[A-Za-z0-9_-]+$/.test(key)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const value = String(rawValue).trim();
|
|
206
|
+
if (!value || /[<>{};@\\]/.test(value) || this.hasUnsafeCssFunction(value)) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
rules.push(`${key}: ${value};`);
|
|
210
|
+
}
|
|
211
|
+
return rules.length > 0 ? `
|
|
212
|
+
<style>:root { ${rules.join(' ')} }</style>` : '';
|
|
213
|
+
}
|
|
214
|
+
/** True if the value calls any CSS function outside {@link SAFE_CSS_FUNCTIONS}. */
|
|
215
|
+
hasUnsafeCssFunction(value) {
|
|
216
|
+
// Every `name(` occurrence — the identifier immediately before a '('.
|
|
217
|
+
const calls = value.match(/([a-zA-Z][a-zA-Z-]*)\s*\(/g) ?? [];
|
|
218
|
+
return calls.some((c) => {
|
|
219
|
+
const name = c.replace(/\s*\($/, '').toLowerCase();
|
|
220
|
+
return !ExportService.SAFE_CSS_FUNCTIONS.has(name);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/** Fetch → data URI so the export stays self-contained; raw URL on any failure
|
|
224
|
+
* (or when the response isn't actually an image — don't inline HTML/other as
|
|
225
|
+
* a bogus `data:` image). */
|
|
226
|
+
async inlineLogo(url) {
|
|
227
|
+
try {
|
|
228
|
+
const response = await fetch(url);
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
return url;
|
|
231
|
+
}
|
|
232
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
233
|
+
if (!/^image\//i.test(contentType)) {
|
|
234
|
+
return url;
|
|
235
|
+
}
|
|
236
|
+
const blob = await response.blob();
|
|
237
|
+
return await new Promise((resolve, reject) => {
|
|
238
|
+
const reader = new FileReader();
|
|
239
|
+
reader.onloadend = () => resolve(String(reader.result));
|
|
240
|
+
reader.onerror = () => reject(reader.error);
|
|
241
|
+
reader.readAsDataURL(blob);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return url;
|
|
246
|
+
}
|
|
52
247
|
}
|
|
53
248
|
async loadConversationData(conversationId, currentUser) {
|
|
54
249
|
const rv = RunView.FromMetadataProvider(this.Provider);
|
|
@@ -91,6 +286,12 @@ export class ExportService {
|
|
|
91
286
|
name: data.conversation.Name
|
|
92
287
|
};
|
|
93
288
|
}
|
|
289
|
+
// Branding block (title / trademark / logo) when the host supplied branding —
|
|
290
|
+
// the data-format analogue of the HTML header + trademark footer.
|
|
291
|
+
const branding = this.buildBrandingBlock(options);
|
|
292
|
+
if (branding) {
|
|
293
|
+
exportData.branding = branding;
|
|
294
|
+
}
|
|
94
295
|
// Add messages if requested
|
|
95
296
|
if (options.includeMessages) {
|
|
96
297
|
exportData.messages = data.details.map((detail, index) => {
|
|
@@ -110,7 +311,16 @@ export class ExportService {
|
|
|
110
311
|
return JSON.stringify(exportData, null, options.prettyPrint ? 2 : 0);
|
|
111
312
|
}
|
|
112
313
|
exportAsMarkdown(data, options) {
|
|
113
|
-
|
|
314
|
+
const logoUrl = options.branding?.logoUrl?.trim();
|
|
315
|
+
// Unbranded markdown must stay byte-identical to before (`# <Name>`), so DON'T
|
|
316
|
+
// route through resolveTitle's 'Conversation' fallback here — only a supplied
|
|
317
|
+
// branding title overrides the raw conversation name.
|
|
318
|
+
const title = options.branding?.title?.trim() || data.conversation.Name;
|
|
319
|
+
let md = '';
|
|
320
|
+
if (logoUrl) {
|
|
321
|
+
md += `\n\n`;
|
|
322
|
+
}
|
|
323
|
+
md += `# ${title}\n\n`;
|
|
114
324
|
if (data.conversation.Description) {
|
|
115
325
|
md += `${data.conversation.Description}\n\n`;
|
|
116
326
|
}
|
|
@@ -128,30 +338,42 @@ export class ExportService {
|
|
|
128
338
|
md += `---\n\n`;
|
|
129
339
|
}
|
|
130
340
|
}
|
|
341
|
+
const trademark = options.branding?.trademark?.trim();
|
|
342
|
+
if (trademark) {
|
|
343
|
+
md += `_${trademark}_\n`;
|
|
344
|
+
}
|
|
131
345
|
return md;
|
|
132
346
|
}
|
|
133
|
-
exportAsHTML(data, options) {
|
|
347
|
+
exportAsHTML(data, options, theme) {
|
|
348
|
+
const trademark = options.branding?.trademark?.trim();
|
|
349
|
+
// Every color reads a theme token with the legacy hex as its fallback, so an
|
|
350
|
+
// unthemed export (no :root block emitted) renders exactly as it always has,
|
|
351
|
+
// while a themed one follows the app's palette.
|
|
134
352
|
const styles = options.includeCSS ? `
|
|
135
353
|
<style>
|
|
136
|
-
body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.6; }
|
|
137
|
-
h1 { color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }
|
|
138
|
-
.meta { color: #666; font-size: 14px; margin-bottom: 30px; }
|
|
139
|
-
.message { margin-bottom: 30px; padding: 20px; border-radius: 8px; background: #f5f5f5; }
|
|
140
|
-
.message.user { background: #e3f2fd; }
|
|
141
|
-
.message.assistant { background: #f5f5f5; }
|
|
142
|
-
.role { font-weight: 600; color: #007bff; margin-bottom: 10px; }
|
|
354
|
+
body { font-family: var(--mj-font-family, system-ui, -apple-system, sans-serif); background: var(--mj-bg-page, #fff); color: var(--mj-text-primary, #333); max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.6; }
|
|
355
|
+
h1 { color: var(--mj-text-primary, #333); border-bottom: 2px solid var(--mj-brand-primary, #007bff); padding-bottom: 10px; }
|
|
356
|
+
.meta { color: var(--mj-text-secondary, #666); font-size: 14px; margin-bottom: 30px; }
|
|
357
|
+
.message { margin-bottom: 30px; padding: 20px; border-radius: var(--mj-radius-md, 8px); background: var(--mj-bg-surface-card, #f5f5f5); }
|
|
358
|
+
.message.user { background: var(--mj-brand-accent-subtle, #e3f2fd); }
|
|
359
|
+
.message.assistant { background: var(--mj-bg-surface-card, #f5f5f5); }
|
|
360
|
+
.role { font-weight: 600; color: var(--mj-brand-primary, #007bff); margin-bottom: 10px; }
|
|
143
361
|
.content { white-space: pre-wrap; }
|
|
144
|
-
.timestamp { color: #999; font-size: 12px; margin-top: 10px; }
|
|
145
|
-
|
|
362
|
+
.timestamp { color: var(--mj-text-disabled, #999); font-size: 12px; margin-top: 10px; }${theme.logoDataUri ? `
|
|
363
|
+
.brand-logo { display: block; max-height: 48px; margin-bottom: 12px; }` : ''}${trademark ? `
|
|
364
|
+
.brand-trademark { margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--mj-border-default, #ddd); color: var(--mj-text-secondary, #666); font-size: 12px; text-align: center; }` : ''}
|
|
365
|
+
</style>${theme.rootBlock}` : '';
|
|
366
|
+
const title = theme.title ?? (data.conversation.Name || 'Conversation');
|
|
146
367
|
let html = `<!DOCTYPE html>
|
|
147
368
|
<html lang="en">
|
|
148
369
|
<head>
|
|
149
370
|
<meta charset="UTF-8">
|
|
150
371
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
151
|
-
<title>${this.escapeHtml(
|
|
372
|
+
<title>${this.escapeHtml(title)}</title>${styles}
|
|
152
373
|
</head>
|
|
153
374
|
<body>
|
|
154
|
-
|
|
375
|
+
${theme.logoDataUri ? `<img class="brand-logo" src="${this.escapeAttr(theme.logoDataUri)}" alt="" />
|
|
376
|
+
` : ''}<h1>${this.escapeHtml(title)}</h1>`;
|
|
155
377
|
if (options.includeMetadata) {
|
|
156
378
|
html += `
|
|
157
379
|
<div class="meta">
|
|
@@ -174,13 +396,17 @@ export class ExportService {
|
|
|
174
396
|
</div>`;
|
|
175
397
|
}
|
|
176
398
|
}
|
|
399
|
+
if (options.includeCSS && trademark) {
|
|
400
|
+
html += `
|
|
401
|
+
<footer class="brand-trademark">${this.escapeHtml(trademark)}</footer>`;
|
|
402
|
+
}
|
|
177
403
|
html += `
|
|
178
404
|
</body>
|
|
179
405
|
</html>`;
|
|
180
406
|
return html;
|
|
181
407
|
}
|
|
182
408
|
exportAsText(data, options) {
|
|
183
|
-
const name = data
|
|
409
|
+
const name = this.resolveTitle(data, options) || 'Conversation';
|
|
184
410
|
let text = `${name}\n`;
|
|
185
411
|
text += '='.repeat(name.length) + '\n\n';
|
|
186
412
|
if (data.conversation.Description) {
|
|
@@ -200,6 +426,10 @@ export class ExportService {
|
|
|
200
426
|
text += '\n' + '-'.repeat(80) + '\n\n';
|
|
201
427
|
}
|
|
202
428
|
}
|
|
429
|
+
const trademark = options.branding?.trademark?.trim();
|
|
430
|
+
if (trademark) {
|
|
431
|
+
text += `${trademark}\n`;
|
|
432
|
+
}
|
|
203
433
|
return text;
|
|
204
434
|
}
|
|
205
435
|
downloadFile(content, filename, mimeType) {
|
|
@@ -231,6 +461,10 @@ export class ExportService {
|
|
|
231
461
|
div.textContent = text;
|
|
232
462
|
return div.innerHTML;
|
|
233
463
|
}
|
|
464
|
+
/** escapeHtml + quote escaping — for values interpolated into HTML attributes. */
|
|
465
|
+
escapeAttr(text) {
|
|
466
|
+
return this.escapeHtml(text).replace(/"/g, '"');
|
|
467
|
+
}
|
|
234
468
|
getTimestamp() {
|
|
235
469
|
const now = new Date();
|
|
236
470
|
return now.toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"export.service.js","sourceRoot":"","sources":["../../../src/lib/services/export.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable } from '@angular/core';\nimport { MJConversationEntity, MJConversationDetailEntity } from '@memberjunction/core-entities';\nimport { RunView, UserInfo, Metadata, IMetadataProvider } from '@memberjunction/core';\n\nexport type ExportFormat = 'json' | 'markdown' | 'html' | 'text';\n\nexport interface ExportOptions {\n includeMessages?: boolean;\n includeMetadata?: boolean;\n prettyPrint?: boolean;\n includeCSS?: boolean;\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class ExportService {\n private _provider: IMetadataProvider | null = null;\n\n /**\n * Set the metadata provider this service should use. When unset, falls back to Metadata.Provider.\n */\n public set Provider(value: IMetadataProvider | null) {\n this._provider = value;\n }\n\n public get Provider(): IMetadataProvider {\n return this._provider ?? Metadata.Provider;\n }\n\n async exportConversation(\n conversationId: string,\n format: ExportFormat,\n currentUser: UserInfo,\n options: ExportOptions = {}\n ): Promise<void> {\n const conversation = await this.loadConversationData(conversationId, currentUser);\n\n // Apply default options\n const exportOptions: Required<ExportOptions> = {\n includeMessages: options.includeMessages ?? true,\n includeMetadata: options.includeMetadata ?? true,\n prettyPrint: options.prettyPrint ?? true,\n includeCSS: options.includeCSS ?? true\n };\n\n let content: string;\n let filename: string;\n let mimeType: string;\n\n switch (format) {\n case 'json':\n content = this.exportAsJSON(conversation, exportOptions);\n filename = `conversation-${conversation.conversation.Name}-${this.getTimestamp()}.json`;\n mimeType = 'application/json';\n break;\n case 'markdown':\n content = this.exportAsMarkdown(conversation, exportOptions);\n filename = `conversation-${conversation.conversation.Name}-${this.getTimestamp()}.md`;\n mimeType = 'text/markdown';\n break;\n case 'html':\n content = this.exportAsHTML(conversation, exportOptions);\n filename = `conversation-${conversation.conversation.Name}-${this.getTimestamp()}.html`;\n mimeType = 'text/html';\n break;\n case 'text':\n content = this.exportAsText(conversation, exportOptions);\n filename = `conversation-${conversation.conversation.Name}-${this.getTimestamp()}.txt`;\n mimeType = 'text/plain';\n break;\n default:\n throw new Error(`Unsupported export format: ${format}`);\n }\n\n this.downloadFile(content, filename, mimeType);\n }\n\n private async loadConversationData(\n conversationId: string,\n currentUser: UserInfo\n ): Promise<{ conversation: MJConversationEntity; details: MJConversationDetailEntity[] }> {\n const rv = RunView.FromMetadataProvider(this.Provider);\n\n // Load conversation and details in parallel\n const [conversationResult, detailsResult] = await rv.RunViews([\n {\n EntityName: 'MJ: Conversations',\n ExtraFilter: `ID='${conversationId}'`,\n ResultType: 'entity_object'\n },\n {\n EntityName: 'MJ: Conversation Details',\n ExtraFilter: `ConversationID='${conversationId}'`,\n OrderBy: 'Sequence ASC',\n ResultType: 'entity_object'\n }\n ], currentUser);\n\n if (!conversationResult.Success || !conversationResult.Results?.length) {\n throw new Error('Conversation not found');\n }\n\n return {\n conversation: conversationResult.Results[0] as MJConversationEntity,\n details: (detailsResult.Results || []) as MJConversationDetailEntity[]\n };\n }\n\n private exportAsJSON(\n data: {\n conversation: MJConversationEntity;\n details: MJConversationDetailEntity[];\n },\n options: Required<ExportOptions>\n ): string {\n const exportData: Record<string, unknown> = {};\n\n // Add metadata if requested\n if (options.includeMetadata) {\n exportData.conversation = {\n id: data.conversation.ID,\n name: data.conversation.Name,\n description: data.conversation.Description,\n createdAt: data.conversation.__mj_CreatedAt,\n updatedAt: data.conversation.__mj_UpdatedAt\n };\n } else {\n exportData.conversation = {\n name: data.conversation.Name\n };\n }\n\n // Add messages if requested\n if (options.includeMessages) {\n exportData.messages = data.details.map((detail, index) => {\n const message: Record<string, unknown> = {\n role: detail.Role,\n message: detail.Message\n };\n\n if (options.includeMetadata) {\n message.id = detail.ID;\n message.sequence = index + 1;\n message.timestamp = detail.__mj_CreatedAt;\n }\n\n return message;\n });\n }\n\n // Use pretty print option\n return JSON.stringify(exportData, null, options.prettyPrint ? 2 : 0);\n }\n\n private exportAsMarkdown(\n data: {\n conversation: MJConversationEntity;\n details: MJConversationDetailEntity[];\n },\n options: Required<ExportOptions>\n ): string {\n let md = `# ${data.conversation.Name}\\n\\n`;\n\n if (data.conversation.Description) {\n md += `${data.conversation.Description}\\n\\n`;\n }\n\n if (options.includeMetadata) {\n md += `**Created:** ${this.formatDate(data.conversation.__mj_CreatedAt)}\\n\\n`;\n }\n\n md += `---\\n\\n`;\n\n if (options.includeMessages) {\n for (const detail of data.details) {\n md += `## ${this.capitalizeRole(detail.Role || 'Unknown')}\\n\\n`;\n md += `${detail.Message}\\n\\n`;\n\n if (options.includeMetadata) {\n md += `*${this.formatDate(detail.__mj_CreatedAt)}*\\n\\n`;\n }\n\n md += `---\\n\\n`;\n }\n }\n\n return md;\n }\n\n private exportAsHTML(\n data: {\n conversation: MJConversationEntity;\n details: MJConversationDetailEntity[];\n },\n options: Required<ExportOptions>\n ): string {\n const styles = options.includeCSS ? `\n <style>\n body { font-family: system-ui, -apple-system, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.6; }\n h1 { color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }\n .meta { color: #666; font-size: 14px; margin-bottom: 30px; }\n .message { margin-bottom: 30px; padding: 20px; border-radius: 8px; background: #f5f5f5; }\n .message.user { background: #e3f2fd; }\n .message.assistant { background: #f5f5f5; }\n .role { font-weight: 600; color: #007bff; margin-bottom: 10px; }\n .content { white-space: pre-wrap; }\n .timestamp { color: #999; font-size: 12px; margin-top: 10px; }\n </style>` : '';\n\n let html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${this.escapeHtml(data.conversation.Name || 'Conversation')}</title>${styles}\n</head>\n<body>\n <h1>${this.escapeHtml(data.conversation.Name || 'Conversation')}</h1>`;\n\n if (options.includeMetadata) {\n html += `\n <div class=\"meta\">\n ${data.conversation.Description ? `<p>${this.escapeHtml(data.conversation.Description)}</p>` : ''}\n <p>Created: ${this.formatDate(data.conversation.__mj_CreatedAt)}</p>\n </div>`;\n }\n\n if (options.includeMessages) {\n for (const detail of data.details) {\n const roleClass = detail.Role?.toLowerCase() || 'unknown';\n html += `\n <div class=\"message ${roleClass}\">\n <div class=\"role\">${this.capitalizeRole(detail.Role || 'Unknown')}</div>\n <div class=\"content\">${this.escapeHtml(detail.Message || '')}</div>`;\n\n if (options.includeMetadata) {\n html += `\n <div class=\"timestamp\">${this.formatDate(detail.__mj_CreatedAt)}</div>`;\n }\n\n html += `\n </div>`;\n }\n }\n\n html += `\n</body>\n</html>`;\n\n return html;\n }\n\n private exportAsText(\n data: {\n conversation: MJConversationEntity;\n details: MJConversationDetailEntity[];\n },\n options: Required<ExportOptions>\n ): string {\n const name = data.conversation.Name || 'Conversation';\n let text = `${name}\\n`;\n text += '='.repeat(name.length) + '\\n\\n';\n\n if (data.conversation.Description) {\n text += `${data.conversation.Description}\\n\\n`;\n }\n\n if (options.includeMetadata) {\n text += `Created: ${this.formatDate(data.conversation.__mj_CreatedAt)}\\n\\n`;\n }\n\n text += '-'.repeat(80) + '\\n\\n';\n\n if (options.includeMessages) {\n for (const detail of data.details) {\n text += `[${this.capitalizeRole(detail.Role || 'Unknown')}]\\n`;\n text += `${detail.Message}\\n`;\n\n if (options.includeMetadata) {\n text += `(${this.formatDate(detail.__mj_CreatedAt)})\\n`;\n }\n\n text += '\\n' + '-'.repeat(80) + '\\n\\n';\n }\n }\n\n return text;\n }\n\n private downloadFile(content: string, filename: string, mimeType: string): void {\n const blob = new Blob([content], { type: mimeType });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n }\n\n private formatDate(date: Date | string): string {\n const d = new Date(date);\n return d.toLocaleString('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit'\n });\n }\n\n private capitalizeRole(role: string): string {\n return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase();\n }\n\n private escapeHtml(text: string): string {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n private getTimestamp(): string {\n const now = new Date();\n return now.toISOString().replace(/[:.]/g, '-').slice(0, -5);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"export.service.js","sourceRoot":"","sources":["../../../src/lib/services/export.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable } from '@angular/core';\nimport { MJConversationEntity, MJConversationDetailEntity } from '@memberjunction/core-entities';\nimport { RunView, UserInfo, Metadata, IMetadataProvider } from '@memberjunction/core';\n\nexport type ExportFormat = 'json' | 'markdown' | 'html' | 'text';\n\n/**\n * Branding applied to exported files, across EVERY format. HTML gets the full\n * treatment (theme colors, an inlined logo, the title, and a trademark footer);\n * JSON/markdown/text carry the title and trademark (markdown also references the\n * logo). Hosts supply this via `mj-conversation-chat-area`'s `exportBranding`\n * input. Color theming (`brandTokens`) is HTML-only — it has no meaning in the\n * data/plain-text formats.\n */\nexport interface ExportBranding {\n /**\n * CSS custom-property snapshot emitted as a `:root{}` block in the exported\n * document, so the export's stylesheet (which reads `var(--mj-…, fallback)`)\n * renders in the app's theme. HTML export only. Keys must be valid\n * custom-property names (`--like-this`). Entries are DROPPED when the key is\n * invalid or the value carries declaration-escape / network-call characters\n * (`<>{};@\\`, `url(`, `expression(`) — plain colors, `var()`, and\n * `color-mix()` pass. When omitted and `ExportOptions.includeTheme` is true,\n * the current theme is auto-snapshotted from `document.documentElement` (see\n * {@link DEFAULT_EXPORT_THEME_TOKENS}).\n */\n brandTokens?: Record<string, string>;\n /**\n * Logo URL. In HTML it is fetched and inlined as a data URI (so the file stays\n * self-contained) and rendered above the title — requires\n * `ExportOptions.includeCSS`, since it needs its stylesheet rule. In markdown it is\n * referenced by URL. JSON carries it as a string; plain text omits it.\n *\n * If the fetch fails, or the response isn't `content-type: image/*`, the raw URL is\n * emitted instead — so the exported file will reach out to that URL when opened.\n * There is no size or timeout guard on the fetch: a very large logo inflates the\n * export.\n */\n logoUrl?: string;\n /** Overrides the exported document's title (defaults to the conversation name). */\n title?: string;\n /**\n * A short trademark / attribution line (e.g. `© 2026 Acme Association ·\n * Powered by Betty`) rendered at the FOOT of the exported document: a styled HTML\n * footer, a markdown/text trailer, and a field in the JSON `branding` block.\n *\n * In HTML it needs its stylesheet rule, so — like the logo — it is emitted only when\n * `ExportOptions.includeCSS` is true. The markdown/text/JSON trailers have no such\n * dependency and always emit.\n *\n * NOT escaped for markdown: a value containing `_`, `]`, `)`, or a newline can break\n * the surrounding markdown. Supply plain text (HTML and JSON are escaped).\n */\n trademark?: string;\n}\n\nexport interface ExportOptions {\n includeMessages?: boolean;\n includeMetadata?: boolean;\n prettyPrint?: boolean;\n includeCSS?: boolean;\n /**\n * Emit the current app theme into the HTML export. When true and\n * `branding.brandTokens` is absent, the {@link DEFAULT_EXPORT_THEME_TOKENS}\n * are auto-snapshotted from the live document at export time. Default false —\n * an unthemed export renders exactly as before (the stylesheet's `var()`\n * fallbacks carry the legacy palette).\n *\n * Gates only the TOKEN auto-snapshot: a supplied `branding`'s `logoUrl`,\n * `title`, and `trademark` apply whenever `branding` is present — in HTML AND\n * in the JSON/markdown/text formats. (The export modal passes `branding`\n * through only while its \"Include branding\" checkbox is on, so in the UI flow\n * everything travels together.)\n */\n includeTheme?: boolean;\n /**\n * Which theme mode the snapshot captures — NOT whichever mode the app happens to be\n * in when the user clicks Export. Defaults to `'light'`: an export taken during a\n * dark session would otherwise bake dark text onto the page's white background and\n * come out unreadable. `'dark'` pairs the dark palette with the dark\n * `--mj-bg-page` the body rule applies, so the file is coherent either way.\n * HTML only — the data formats carry no colors.\n */\n themeMode?: 'light' | 'dark';\n /** Branding (tokens / logo / title) applied to the export — see {@link ExportBranding}. */\n branding?: ExportBranding;\n}\n\n/**\n * Tokens captured by the theme auto-snapshot — exactly the custom properties the\n * HTML export's stylesheet consumes. Hosts wanting more control supply their own\n * map via {@link ExportBranding.brandTokens} instead.\n */\nexport const DEFAULT_EXPORT_THEME_TOKENS: readonly string[] = [\n '--mj-bg-page',\n '--mj-text-primary',\n '--mj-text-secondary',\n '--mj-text-disabled',\n '--mj-border-default',\n '--mj-brand-primary',\n '--mj-brand-accent-subtle',\n '--mj-bg-surface-card',\n '--mj-font-family',\n '--mj-radius-md',\n];\n\n/** Internal: options with defaults applied (branding stays optional). */\ntype ResolvedExportOptions = Required<Omit<ExportOptions, 'branding'>> & Pick<ExportOptions, 'branding'>;\n\n/** Internal: the theme pieces resolved once per HTML export. */\ninterface ResolvedExportTheme {\n /** `<style>:root{…}</style>` block (empty string when no tokens resolved). */\n rootBlock: string;\n /** Inlined logo (data URI, or the raw URL on fetch failure); null = no logo. */\n logoDataUri: string | null;\n /** Document-title override; null = use the conversation name. */\n title: string | null;\n}\n\n@Injectable({\n providedIn: 'root'\n})\nexport class ExportService {\n private _provider: IMetadataProvider | null = null;\n\n /**\n * Set the metadata provider this service should use. When unset, falls back to Metadata.Provider.\n */\n public set Provider(value: IMetadataProvider | null) {\n this._provider = value;\n }\n\n public get Provider(): IMetadataProvider {\n return this._provider ?? Metadata.Provider;\n }\n\n async exportConversation(\n conversationId: string,\n format: ExportFormat,\n currentUser: UserInfo,\n options: ExportOptions = {}\n ): Promise<void> {\n const conversation = await this.loadConversationData(conversationId, currentUser);\n const { content, filename, mimeType } = await this.BuildExportContent(conversation, format, options);\n this.downloadFile(content, filename, mimeType);\n }\n\n /**\n * Build the export document for already-loaded conversation data — the\n * download-free seam of {@link exportConversation}. Async because HTML-format\n * branding may inline a logo. Exposed publicly so hosts (and tests) can build\n * export content without triggering a browser download.\n */\n public async BuildExportContent(\n data: { conversation: MJConversationEntity; details: MJConversationDetailEntity[] },\n format: ExportFormat,\n options: ExportOptions = {}\n ): Promise<{ content: string; filename: string; mimeType: string }> {\n const exportOptions: ResolvedExportOptions = {\n includeMessages: options.includeMessages ?? true,\n includeMetadata: options.includeMetadata ?? true,\n prettyPrint: options.prettyPrint ?? true,\n includeCSS: options.includeCSS ?? true,\n includeTheme: options.includeTheme ?? false,\n themeMode: options.themeMode ?? 'light',\n branding: options.branding\n };\n const baseName = `conversation-${data.conversation.Name}-${this.getTimestamp()}`;\n\n switch (format) {\n case 'json':\n return { content: this.exportAsJSON(data, exportOptions), filename: `${baseName}.json`, mimeType: 'application/json' };\n case 'markdown':\n return { content: this.exportAsMarkdown(data, exportOptions), filename: `${baseName}.md`, mimeType: 'text/markdown' };\n case 'html':\n return {\n content: this.exportAsHTML(data, exportOptions, await this.resolveTheme(exportOptions)),\n filename: `${baseName}.html`,\n mimeType: 'text/html'\n };\n case 'text':\n return { content: this.exportAsText(data, exportOptions), filename: `${baseName}.txt`, mimeType: 'text/plain' };\n default:\n throw new Error(`Unsupported export format: ${format}`);\n }\n }\n\n /**\n * Snapshot the given theme tokens from the live document. Empty/unset tokens are\n * skipped.\n *\n * `mode` captures a SPECIFIC palette rather than whatever the app is rendering right\n * now. Without it, exporting from a dark session bakes dark text onto the export's\n * white page and the file is unreadable. When the requested mode isn't the active one,\n * `data-theme` is flipped on `documentElement`, read, and restored — all synchronous\n * within one task, so the browser never paints the intermediate state. Omit `mode`\n * to read the live values as-is.\n */\n public SnapshotBrandTokens(\n tokens: readonly string[] = DEFAULT_EXPORT_THEME_TOKENS,\n mode?: 'light' | 'dark'\n ): Record<string, string> {\n const root = document.documentElement;\n const previous = root.getAttribute('data-theme');\n // The app's two real states are `data-theme=\"dark\"` and NO attribute at all —\n // `MJExplorerAppComponent` removes it for light rather than writing \"light\"\n // (explorer-app.component.ts). Reproduce those exactly rather than inventing a\n // third state, so the transient DOM is one the stylesheet has actually seen.\n const wantDark = mode === 'dark';\n const isDark = previous === 'dark';\n const mustSwap = mode != null && wantDark !== isDark;\n\n if (mustSwap) {\n if (wantDark) {\n root.setAttribute('data-theme', 'dark');\n } else {\n root.removeAttribute('data-theme');\n }\n }\n try {\n const style = getComputedStyle(root);\n const out: Record<string, string> = {};\n for (const token of tokens) {\n const value = style.getPropertyValue(token).trim();\n if (value) {\n out[token] = value;\n }\n }\n return out;\n } finally {\n // Restore even if reading throws — never strand the app in the other theme.\n if (mustSwap) {\n if (previous === null) {\n root.removeAttribute('data-theme');\n } else {\n root.setAttribute('data-theme', previous);\n }\n }\n }\n }\n\n /** Resolve the HTML export's theme pieces once: tokens (explicit > snapshot), logo, title. */\n private async resolveTheme(options: ResolvedExportOptions): Promise<ResolvedExportTheme> {\n const tokens = this.resolveBrandTokens(options);\n const rootBlock = tokens ? this.buildRootBlock(tokens) : '';\n const logoUrl = options.branding?.logoUrl?.trim();\n return {\n rootBlock,\n // The logo needs its `.brand-logo` rule (as the trademark footer needs its own),\n // so both are gated on includeCSS — a CSS-less export must never carry a\n // full-size unstyled logo. The modal enforces this too, but the public\n // BuildExportContent seam has to hold the line on its own.\n logoDataUri: options.includeCSS && logoUrl ? await this.inlineLogo(logoUrl) : null,\n // The title is deliberately NOT includeCSS-gated: it lands in <title>/<h1>, needs\n // no stylesheet, and is the one branding field every format carries unconditionally.\n title: options.branding?.title?.trim() || null\n };\n }\n\n /** Explicit host tokens win; `includeTheme` falls back to a live snapshot; else no theme. */\n private resolveBrandTokens(options: ResolvedExportOptions): Record<string, string> | null {\n const explicit = options.branding?.brandTokens;\n if (explicit && Object.keys(explicit).length > 0) {\n return explicit;\n }\n return options.includeTheme ? this.SnapshotBrandTokens(DEFAULT_EXPORT_THEME_TOKENS, options.themeMode) : null;\n }\n\n /** Document title used by every format: branding override → conversation name → generic. */\n private resolveTitle(\n data: { conversation: MJConversationEntity; details: MJConversationDetailEntity[] },\n options: ResolvedExportOptions\n ): string {\n return options.branding?.title?.trim() || data.conversation.Name || 'Conversation';\n }\n\n /**\n * The JSON `branding` block — the data-format analogue of the HTML header +\n * trademark footer. Carries the title, trademark, and logo URL the host set;\n * null when no branding (or none of those fields) is present. Theme color\n * tokens are HTML-only and never included here.\n */\n private buildBrandingBlock(options: ResolvedExportOptions): Record<string, string> | null {\n const b = options.branding;\n if (!b) {\n return null;\n }\n const block: Record<string, string> = {};\n const title = b.title?.trim();\n const trademark = b.trademark?.trim();\n const logoUrl = b.logoUrl?.trim();\n if (title) block.title = title;\n if (trademark) block.trademark = trademark;\n if (logoUrl) block.logoUrl = logoUrl;\n return Object.keys(block).length > 0 ? block : null;\n }\n\n /**\n * CSS functions permitted inside an exported token value. This is an ALLOWLIST\n * (not a blocklist) so no network-capable function can slip through: blocking\n * only `url(`/`expression(` let `image-set()`, `-webkit-image-set()`,\n * `cross-fade()`, `image()`, and `src()` fetch a beacon URL without ever\n * writing the literal `url(`. Anything not on this list drops the whole entry.\n * The set is the safe value/color-math functions the export could legitimately\n * carry; extend it only with functions that CANNOT load an external resource.\n */\n private static readonly SAFE_CSS_FUNCTIONS = new Set([\n 'var', 'calc', 'min', 'max', 'clamp',\n 'rgb', 'rgba', 'hsl', 'hsla', 'hwb', 'lab', 'lch', 'oklab', 'oklch',\n 'color', 'color-mix',\n ]);\n\n /**\n * Emit the sanitized `:root{}` style block. Keys must be custom-property names.\n * A value is REJECTED (the entry is dropped, not stripped) when it carries any\n * declaration/tag-escape or network-reach vector: `<>{}` (tag/rule breakout),\n * `;` (declaration breakout — e.g. `; background: url(beacon)`), `\\` (CSS\n * escapes), `@` (at-rules), OR any CSS function not in\n * {@link SAFE_CSS_FUNCTIONS} (which excludes every resource-loading function —\n * url/image-set/cross-fade/image/src/expression/…). Plain colors, `var()`, and\n * `color-mix()` pass untouched.\n */\n private buildRootBlock(tokens: Record<string, string>): string {\n const rules: string[] = [];\n for (const [key, rawValue] of Object.entries(tokens)) {\n if (!/^--[A-Za-z0-9_-]+$/.test(key)) {\n continue;\n }\n const value = String(rawValue).trim();\n if (!value || /[<>{};@\\\\]/.test(value) || this.hasUnsafeCssFunction(value)) {\n continue;\n }\n rules.push(`${key}: ${value};`);\n }\n return rules.length > 0 ? `\n <style>:root { ${rules.join(' ')} }</style>` : '';\n }\n\n /** True if the value calls any CSS function outside {@link SAFE_CSS_FUNCTIONS}. */\n private hasUnsafeCssFunction(value: string): boolean {\n // Every `name(` occurrence — the identifier immediately before a '('.\n const calls = value.match(/([a-zA-Z][a-zA-Z-]*)\\s*\\(/g) ?? [];\n return calls.some((c) => {\n const name = c.replace(/\\s*\\($/, '').toLowerCase();\n return !ExportService.SAFE_CSS_FUNCTIONS.has(name);\n });\n }\n\n /** Fetch → data URI so the export stays self-contained; raw URL on any failure\n * (or when the response isn't actually an image — don't inline HTML/other as\n * a bogus `data:` image). */\n private async inlineLogo(url: string): Promise<string> {\n try {\n const response = await fetch(url);\n if (!response.ok) {\n return url;\n }\n const contentType = response.headers.get('content-type') ?? '';\n if (!/^image\\//i.test(contentType)) {\n return url;\n }\n const blob = await response.blob();\n return await new Promise<string>((resolve, reject) => {\n const reader = new FileReader();\n reader.onloadend = () => resolve(String(reader.result));\n reader.onerror = () => reject(reader.error);\n reader.readAsDataURL(blob);\n });\n } catch {\n return url;\n }\n }\n\n private async loadConversationData(\n conversationId: string,\n currentUser: UserInfo\n ): Promise<{ conversation: MJConversationEntity; details: MJConversationDetailEntity[] }> {\n const rv = RunView.FromMetadataProvider(this.Provider);\n\n // Load conversation and details in parallel\n const [conversationResult, detailsResult] = await rv.RunViews([\n {\n EntityName: 'MJ: Conversations',\n ExtraFilter: `ID='${conversationId}'`,\n ResultType: 'entity_object'\n },\n {\n EntityName: 'MJ: Conversation Details',\n ExtraFilter: `ConversationID='${conversationId}'`,\n OrderBy: 'Sequence ASC',\n ResultType: 'entity_object'\n }\n ], currentUser);\n\n if (!conversationResult.Success || !conversationResult.Results?.length) {\n throw new Error('Conversation not found');\n }\n\n return {\n conversation: conversationResult.Results[0] as MJConversationEntity,\n details: (detailsResult.Results || []) as MJConversationDetailEntity[]\n };\n }\n\n private exportAsJSON(\n data: {\n conversation: MJConversationEntity;\n details: MJConversationDetailEntity[];\n },\n options: ResolvedExportOptions\n ): string {\n const exportData: Record<string, unknown> = {};\n\n // Add metadata if requested\n if (options.includeMetadata) {\n exportData.conversation = {\n id: data.conversation.ID,\n name: data.conversation.Name,\n description: data.conversation.Description,\n createdAt: data.conversation.__mj_CreatedAt,\n updatedAt: data.conversation.__mj_UpdatedAt\n };\n } else {\n exportData.conversation = {\n name: data.conversation.Name\n };\n }\n\n // Branding block (title / trademark / logo) when the host supplied branding —\n // the data-format analogue of the HTML header + trademark footer.\n const branding = this.buildBrandingBlock(options);\n if (branding) {\n exportData.branding = branding;\n }\n\n // Add messages if requested\n if (options.includeMessages) {\n exportData.messages = data.details.map((detail, index) => {\n const message: Record<string, unknown> = {\n role: detail.Role,\n message: detail.Message\n };\n\n if (options.includeMetadata) {\n message.id = detail.ID;\n message.sequence = index + 1;\n message.timestamp = detail.__mj_CreatedAt;\n }\n\n return message;\n });\n }\n\n // Use pretty print option\n return JSON.stringify(exportData, null, options.prettyPrint ? 2 : 0);\n }\n\n private exportAsMarkdown(\n data: {\n conversation: MJConversationEntity;\n details: MJConversationDetailEntity[];\n },\n options: ResolvedExportOptions\n ): string {\n const logoUrl = options.branding?.logoUrl?.trim();\n // Unbranded markdown must stay byte-identical to before (`# <Name>`), so DON'T\n // route through resolveTitle's 'Conversation' fallback here — only a supplied\n // branding title overrides the raw conversation name.\n const title = options.branding?.title?.trim() || data.conversation.Name;\n let md = '';\n if (logoUrl) {\n md += `\\n\\n`;\n }\n md += `# ${title}\\n\\n`;\n\n if (data.conversation.Description) {\n md += `${data.conversation.Description}\\n\\n`;\n }\n\n if (options.includeMetadata) {\n md += `**Created:** ${this.formatDate(data.conversation.__mj_CreatedAt)}\\n\\n`;\n }\n\n md += `---\\n\\n`;\n\n if (options.includeMessages) {\n for (const detail of data.details) {\n md += `## ${this.capitalizeRole(detail.Role || 'Unknown')}\\n\\n`;\n md += `${detail.Message}\\n\\n`;\n\n if (options.includeMetadata) {\n md += `*${this.formatDate(detail.__mj_CreatedAt)}*\\n\\n`;\n }\n\n md += `---\\n\\n`;\n }\n }\n\n const trademark = options.branding?.trademark?.trim();\n if (trademark) {\n md += `_${trademark}_\\n`;\n }\n\n return md;\n }\n\n private exportAsHTML(\n data: {\n conversation: MJConversationEntity;\n details: MJConversationDetailEntity[];\n },\n options: ResolvedExportOptions,\n theme: ResolvedExportTheme\n ): string {\n const trademark = options.branding?.trademark?.trim();\n // Every color reads a theme token with the legacy hex as its fallback, so an\n // unthemed export (no :root block emitted) renders exactly as it always has,\n // while a themed one follows the app's palette.\n const styles = options.includeCSS ? `\n <style>\n body { font-family: var(--mj-font-family, system-ui, -apple-system, sans-serif); background: var(--mj-bg-page, #fff); color: var(--mj-text-primary, #333); max-width: 800px; margin: 40px auto; padding: 0 20px; line-height: 1.6; }\n h1 { color: var(--mj-text-primary, #333); border-bottom: 2px solid var(--mj-brand-primary, #007bff); padding-bottom: 10px; }\n .meta { color: var(--mj-text-secondary, #666); font-size: 14px; margin-bottom: 30px; }\n .message { margin-bottom: 30px; padding: 20px; border-radius: var(--mj-radius-md, 8px); background: var(--mj-bg-surface-card, #f5f5f5); }\n .message.user { background: var(--mj-brand-accent-subtle, #e3f2fd); }\n .message.assistant { background: var(--mj-bg-surface-card, #f5f5f5); }\n .role { font-weight: 600; color: var(--mj-brand-primary, #007bff); margin-bottom: 10px; }\n .content { white-space: pre-wrap; }\n .timestamp { color: var(--mj-text-disabled, #999); font-size: 12px; margin-top: 10px; }${theme.logoDataUri ? `\n .brand-logo { display: block; max-height: 48px; margin-bottom: 12px; }` : ''}${trademark ? `\n .brand-trademark { margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--mj-border-default, #ddd); color: var(--mj-text-secondary, #666); font-size: 12px; text-align: center; }` : ''}\n </style>${theme.rootBlock}` : '';\n\n const title = theme.title ?? (data.conversation.Name || 'Conversation');\n let html = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${this.escapeHtml(title)}</title>${styles}\n</head>\n<body>\n ${theme.logoDataUri ? `<img class=\"brand-logo\" src=\"${this.escapeAttr(theme.logoDataUri)}\" alt=\"\" />\n ` : ''}<h1>${this.escapeHtml(title)}</h1>`;\n\n if (options.includeMetadata) {\n html += `\n <div class=\"meta\">\n ${data.conversation.Description ? `<p>${this.escapeHtml(data.conversation.Description)}</p>` : ''}\n <p>Created: ${this.formatDate(data.conversation.__mj_CreatedAt)}</p>\n </div>`;\n }\n\n if (options.includeMessages) {\n for (const detail of data.details) {\n const roleClass = detail.Role?.toLowerCase() || 'unknown';\n html += `\n <div class=\"message ${roleClass}\">\n <div class=\"role\">${this.capitalizeRole(detail.Role || 'Unknown')}</div>\n <div class=\"content\">${this.escapeHtml(detail.Message || '')}</div>`;\n\n if (options.includeMetadata) {\n html += `\n <div class=\"timestamp\">${this.formatDate(detail.__mj_CreatedAt)}</div>`;\n }\n\n html += `\n </div>`;\n }\n }\n\n if (options.includeCSS && trademark) {\n html += `\n <footer class=\"brand-trademark\">${this.escapeHtml(trademark)}</footer>`;\n }\n\n html += `\n</body>\n</html>`;\n\n return html;\n }\n\n private exportAsText(\n data: {\n conversation: MJConversationEntity;\n details: MJConversationDetailEntity[];\n },\n options: ResolvedExportOptions\n ): string {\n const name = this.resolveTitle(data, options) || 'Conversation';\n let text = `${name}\\n`;\n text += '='.repeat(name.length) + '\\n\\n';\n\n if (data.conversation.Description) {\n text += `${data.conversation.Description}\\n\\n`;\n }\n\n if (options.includeMetadata) {\n text += `Created: ${this.formatDate(data.conversation.__mj_CreatedAt)}\\n\\n`;\n }\n\n text += '-'.repeat(80) + '\\n\\n';\n\n if (options.includeMessages) {\n for (const detail of data.details) {\n text += `[${this.capitalizeRole(detail.Role || 'Unknown')}]\\n`;\n text += `${detail.Message}\\n`;\n\n if (options.includeMetadata) {\n text += `(${this.formatDate(detail.__mj_CreatedAt)})\\n`;\n }\n\n text += '\\n' + '-'.repeat(80) + '\\n\\n';\n }\n }\n\n const trademark = options.branding?.trademark?.trim();\n if (trademark) {\n text += `${trademark}\\n`;\n }\n\n return text;\n }\n\n private downloadFile(content: string, filename: string, mimeType: string): void {\n const blob = new Blob([content], { type: mimeType });\n const url = URL.createObjectURL(blob);\n const link = document.createElement('a');\n link.href = url;\n link.download = filename;\n document.body.appendChild(link);\n link.click();\n document.body.removeChild(link);\n URL.revokeObjectURL(url);\n }\n\n private formatDate(date: Date | string): string {\n const d = new Date(date);\n return d.toLocaleString('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n hour: '2-digit',\n minute: '2-digit'\n });\n }\n\n private capitalizeRole(role: string): string {\n return role.charAt(0).toUpperCase() + role.slice(1).toLowerCase();\n }\n\n private escapeHtml(text: string): string {\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n /** escapeHtml + quote escaping — for values interpolated into HTML attributes. */\n private escapeAttr(text: string): string {\n return this.escapeHtml(text).replace(/\"/g, '"');\n }\n\n private getTimestamp(): string {\n const now = new Date();\n return now.toISOString().replace(/[:.]/g, '-').slice(0, -5);\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-conversations",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.50.0",
|
|
4
4
|
"description": "MemberJunction: Conversation, Collection, and Artifact management components for any Angular application",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -31,34 +31,34 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@angular/animations": "21.1.3",
|
|
33
33
|
"@angular/cdk": "21.1.3",
|
|
34
|
-
"@memberjunction/ai": "5.
|
|
35
|
-
"@memberjunction/ai-agent-client": "5.
|
|
36
|
-
"@memberjunction/ai-core-plus": "5.
|
|
37
|
-
"@memberjunction/ai-engine-base": "5.
|
|
38
|
-
"@memberjunction/ai-realtime-client": "5.
|
|
39
|
-
"@memberjunction/conversations-runtime": "5.
|
|
40
|
-
"@memberjunction/core": "5.
|
|
41
|
-
"@memberjunction/core-entities": "5.
|
|
42
|
-
"@memberjunction/global": "5.
|
|
43
|
-
"@memberjunction/graphql-dataprovider": "5.
|
|
44
|
-
"@memberjunction/interactive-component-types": "5.
|
|
45
|
-
"@memberjunction/ng-agent-client": "5.
|
|
46
|
-
"@memberjunction/ng-artifacts": "5.
|
|
47
|
-
"@memberjunction/ng-base-types": "5.
|
|
48
|
-
"@memberjunction/ng-code-editor": "5.
|
|
49
|
-
"@memberjunction/ng-composer": "5.
|
|
50
|
-
"@memberjunction/ng-container-directives": "5.
|
|
51
|
-
"@memberjunction/ng-forms": "5.
|
|
52
|
-
"@memberjunction/ng-markdown": "5.
|
|
53
|
-
"@memberjunction/ng-media-player": "5.
|
|
54
|
-
"@memberjunction/ng-notifications": "5.
|
|
55
|
-
"@memberjunction/ng-resource-permissions": "5.
|
|
56
|
-
"@memberjunction/ng-shared-generic": "5.
|
|
57
|
-
"@memberjunction/ng-tasks": "5.
|
|
58
|
-
"@memberjunction/ng-testing": "5.
|
|
59
|
-
"@memberjunction/ng-ui-components": "5.
|
|
60
|
-
"@memberjunction/ng-user-routines": "5.
|
|
61
|
-
"@memberjunction/ng-whiteboard": "5.
|
|
34
|
+
"@memberjunction/ai": "5.50.0",
|
|
35
|
+
"@memberjunction/ai-agent-client": "5.50.0",
|
|
36
|
+
"@memberjunction/ai-core-plus": "5.50.0",
|
|
37
|
+
"@memberjunction/ai-engine-base": "5.50.0",
|
|
38
|
+
"@memberjunction/ai-realtime-client": "5.50.0",
|
|
39
|
+
"@memberjunction/conversations-runtime": "5.50.0",
|
|
40
|
+
"@memberjunction/core": "5.50.0",
|
|
41
|
+
"@memberjunction/core-entities": "5.50.0",
|
|
42
|
+
"@memberjunction/global": "5.50.0",
|
|
43
|
+
"@memberjunction/graphql-dataprovider": "5.50.0",
|
|
44
|
+
"@memberjunction/interactive-component-types": "5.50.0",
|
|
45
|
+
"@memberjunction/ng-agent-client": "5.50.0",
|
|
46
|
+
"@memberjunction/ng-artifacts": "5.50.0",
|
|
47
|
+
"@memberjunction/ng-base-types": "5.50.0",
|
|
48
|
+
"@memberjunction/ng-code-editor": "5.50.0",
|
|
49
|
+
"@memberjunction/ng-composer": "5.50.0",
|
|
50
|
+
"@memberjunction/ng-container-directives": "5.50.0",
|
|
51
|
+
"@memberjunction/ng-forms": "5.50.0",
|
|
52
|
+
"@memberjunction/ng-markdown": "5.50.0",
|
|
53
|
+
"@memberjunction/ng-media-player": "5.50.0",
|
|
54
|
+
"@memberjunction/ng-notifications": "5.50.0",
|
|
55
|
+
"@memberjunction/ng-resource-permissions": "5.50.0",
|
|
56
|
+
"@memberjunction/ng-shared-generic": "5.50.0",
|
|
57
|
+
"@memberjunction/ng-tasks": "5.50.0",
|
|
58
|
+
"@memberjunction/ng-testing": "5.50.0",
|
|
59
|
+
"@memberjunction/ng-ui-components": "5.50.0",
|
|
60
|
+
"@memberjunction/ng-user-routines": "5.50.0",
|
|
61
|
+
"@memberjunction/ng-whiteboard": "5.50.0",
|
|
62
62
|
"angular-split": "^20.0.0",
|
|
63
63
|
"rxjs": "^7.8.2",
|
|
64
64
|
"tslib": "^2.8.1"
|