@memori.ai/memori-react 8.29.1 → 8.30.1

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 (90) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +1 -1
  3. package/dist/components/Chat/Chat.css +110 -9
  4. package/dist/components/Chat/Chat.d.ts +1 -0
  5. package/dist/components/Chat/Chat.js +72 -5
  6. package/dist/components/Chat/Chat.js.map +1 -1
  7. package/dist/components/ChatBubble/ChatBubble.d.ts +1 -0
  8. package/dist/components/ChatBubble/ChatBubble.js +2 -2
  9. package/dist/components/ChatBubble/ChatBubble.js.map +1 -1
  10. package/dist/components/ChatInputs/ChatInputs.js +2 -11
  11. package/dist/components/ChatInputs/ChatInputs.js.map +1 -1
  12. package/dist/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  13. package/dist/components/MemoriWidget/MemoriWidget.js +13 -3
  14. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  15. package/dist/components/UploadButton/UploadButton.js +8 -14
  16. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  17. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +0 -1
  18. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +1 -20
  19. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  20. package/dist/helpers/constants.d.ts +1 -2
  21. package/dist/helpers/constants.js +2 -3
  22. package/dist/helpers/constants.js.map +1 -1
  23. package/dist/helpers/llmUsage.d.ts +60 -0
  24. package/dist/helpers/llmUsage.js +223 -0
  25. package/dist/helpers/llmUsage.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +3 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/locales/de.json +22 -0
  30. package/dist/locales/en.json +22 -0
  31. package/dist/locales/es.json +22 -0
  32. package/dist/locales/fr.json +22 -0
  33. package/dist/locales/it.json +22 -0
  34. package/dist/version.d.ts +1 -1
  35. package/dist/version.js +1 -1
  36. package/esm/components/Chat/Chat.css +110 -9
  37. package/esm/components/Chat/Chat.d.ts +1 -0
  38. package/esm/components/Chat/Chat.js +74 -7
  39. package/esm/components/Chat/Chat.js.map +1 -1
  40. package/esm/components/ChatBubble/ChatBubble.d.ts +1 -0
  41. package/esm/components/ChatBubble/ChatBubble.js +2 -2
  42. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  43. package/esm/components/ChatInputs/ChatInputs.js +2 -11
  44. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  45. package/esm/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  46. package/esm/components/MemoriWidget/MemoriWidget.js +13 -3
  47. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  48. package/esm/components/UploadButton/UploadButton.js +8 -14
  49. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  50. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +0 -1
  51. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +1 -20
  52. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  53. package/esm/helpers/constants.d.ts +1 -2
  54. package/esm/helpers/constants.js +1 -2
  55. package/esm/helpers/constants.js.map +1 -1
  56. package/esm/helpers/llmUsage.d.ts +60 -0
  57. package/esm/helpers/llmUsage.js +212 -0
  58. package/esm/helpers/llmUsage.js.map +1 -0
  59. package/esm/index.d.ts +1 -0
  60. package/esm/index.js +3 -1
  61. package/esm/index.js.map +1 -1
  62. package/esm/locales/de.json +22 -0
  63. package/esm/locales/en.json +22 -0
  64. package/esm/locales/es.json +22 -0
  65. package/esm/locales/fr.json +22 -0
  66. package/esm/locales/it.json +22 -0
  67. package/esm/version.d.ts +1 -1
  68. package/esm/version.js +1 -1
  69. package/package.json +3 -4
  70. package/src/components/Chat/Chat.css +110 -9
  71. package/src/components/Chat/Chat.stories.tsx +42 -0
  72. package/src/components/Chat/Chat.test.tsx +47 -0
  73. package/src/components/Chat/Chat.tsx +238 -5
  74. package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +745 -0
  75. package/src/components/ChatBubble/ChatBubble.tsx +9 -0
  76. package/src/components/ChatInputs/ChatInputs.tsx +4 -15
  77. package/src/components/MemoriWidget/MemoriWidget.tsx +20 -2
  78. package/src/components/UploadButton/UploadButton.stories.tsx +3 -3
  79. package/src/components/UploadButton/UploadButton.tsx +8 -23
  80. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +1 -27
  81. package/src/helpers/constants.ts +1 -2
  82. package/src/helpers/llmUsage.ts +328 -0
  83. package/src/index.stories.tsx +2 -3
  84. package/src/index.tsx +5 -1
  85. package/src/locales/de.json +22 -0
  86. package/src/locales/en.json +22 -0
  87. package/src/locales/es.json +22 -0
  88. package/src/locales/fr.json +22 -0
  89. package/src/locales/it.json +22 -0
  90. package/src/version.ts +1 -1
@@ -61,6 +61,7 @@ export interface Props {
61
61
  experts?: ExpertReference[];
62
62
  showFunctionCache?: boolean;
63
63
  showReasoning?: boolean;
64
+ usageHtml?: string;
64
65
  }
65
66
 
66
67
  const ChatBubble: React.FC<Props> = ({
@@ -84,6 +85,7 @@ const ChatBubble: React.FC<Props> = ({
84
85
  experts,
85
86
  showFunctionCache = false,
86
87
  showReasoning = false,
88
+ usageHtml = '',
87
89
  }) => {
88
90
  const { t, i18n } = useTranslation();
89
91
  const lang = i18n.language || 'en';
@@ -455,6 +457,13 @@ const ChatBubble: React.FC<Props> = ({
455
457
  />
456
458
  )}
457
459
 
460
+ {!!usageHtml && (
461
+ <div
462
+ className="memori-chat--usage-inside-bubble"
463
+ dangerouslySetInnerHTML={{ __html: usageHtml }}
464
+ />
465
+ )}
466
+
458
467
  {(shouldShowCopyButtons ||
459
468
  (message.generatedByAI && showAIicon) ||
460
469
  (message.generatedByAI && showFunctionCache) ||
@@ -269,8 +269,10 @@ const ChatInputs: React.FC<Props> = ({
269
269
  return;
270
270
  }
271
271
 
272
- const totalPayloadLimit = maxTotalMessagePayload ?? 200000;
273
- const perDocumentLimit = maxDocumentContentLength ?? 200000;
272
+ // Only enforce a per-document limit. `maxTotalMessagePayload` is kept for backward compatibility
273
+ // and now acts as the per-document content length override.
274
+ const perDocumentLimit =
275
+ maxTotalMessagePayload ?? maxDocumentContentLength ?? 300000;
274
276
 
275
277
  if (text.length > perDocumentLimit) {
276
278
  e.preventDefault();
@@ -281,19 +283,6 @@ const ChatInputs: React.FC<Props> = ({
281
283
  return;
282
284
  }
283
285
 
284
- const currentTotal = documentPreviewFiles.reduce(
285
- (sum, f) => sum + f.content.length,
286
- 0
287
- );
288
- if (currentTotal + text.length > totalPayloadLimit) {
289
- e.preventDefault();
290
- toast(t('upload.pasteContentExceedsLimit', {
291
- defaultValue:
292
- 'Pasted content exceeds the size limit. Try shortening the text or splitting it into smaller parts.',
293
- }), { icon: '⚠️' });
294
- return;
295
- }
296
-
297
286
  e.preventDefault();
298
287
  const displayName = t('upload.pastedText') || 'pasted-text';
299
288
  const wrappedContent = `<document_attachment filename="pasted-text.txt" type="text/plain">
@@ -400,6 +400,7 @@ export interface Props {
400
400
  showInputs?: boolean;
401
401
  showDates?: boolean;
402
402
  showContextPerLine?: boolean;
403
+ showMessageConsumption?: boolean;
403
404
  showSettings?: boolean;
404
405
  showClear?: boolean;
405
406
  showOnlyLastMessages?: boolean;
@@ -469,6 +470,7 @@ const MemoriWidget = ({
469
470
  showInputs = true,
470
471
  showDates = false,
471
472
  showContextPerLine = false,
473
+ showMessageConsumption = false,
472
474
  showSettings,
473
475
  showTypingText = false,
474
476
  showClear = false,
@@ -662,6 +664,8 @@ const MemoriWidget = ({
662
664
  null
663
665
  );
664
666
  const [hideEmissions, setHideEmissions] = useState(false);
667
+ const [runtimeShowMessageConsumption, setRuntimeShowMessageConsumption] =
668
+ useState(false);
665
669
 
666
670
  const speechSynthesizerRef = useRef<any | null>(null);
667
671
  const [memoriSpeaking, setMemoriSpeaking] = useState(false);
@@ -700,6 +704,12 @@ const MemoriWidget = ({
700
704
  );
701
705
  setAvatarType(getLocalConfig('avatarType', 'avatar3d'));
702
706
  setHideEmissions(getLocalConfig('hideEmissions', false));
707
+ setRuntimeShowMessageConsumption(
708
+ getLocalConfig(
709
+ 'showMessageConsumption',
710
+ showMessageConsumption ?? integrationConfig?.showMessageConsumption ?? false
711
+ )
712
+ );
703
713
 
704
714
  if (!additionalInfo?.loginToken && !authToken) {
705
715
  setLoginToken(getLocalConfig<typeof loginToken>('loginToken', undefined));
@@ -999,6 +1009,7 @@ const MemoriWidget = ({
999
1009
  text: emission,
1000
1010
  emitter: currentState.emitter,
1001
1011
  media: currentState.emittedMedia ?? currentState.media,
1012
+ llmUsage: (currentState as any).llmUsage,
1002
1013
  fromUser: false,
1003
1014
  questionAnswered: msg,
1004
1015
  generatedByAI: !!currentState.completion,
@@ -1010,7 +1021,7 @@ const MemoriWidget = ({
1010
1021
  placeUncertaintyKm: currentState.currentUncertaintyKm,
1011
1022
  tag: currentState.currentTag,
1012
1023
  memoryTags: currentState.memoryTags,
1013
- });
1024
+ } as any);
1014
1025
  if (emission && shouldPlayAudio(emission)) {
1015
1026
  handleSpeak(emission);
1016
1027
  }
@@ -1097,7 +1108,7 @@ const MemoriWidget = ({
1097
1108
  const emission = state?.emission ?? currentDialogState?.emission;
1098
1109
 
1099
1110
  let translatedState = { ...state };
1100
- let translatedMsg = null;
1111
+ let translatedMsg: any = null;
1101
1112
 
1102
1113
  // Skip translation if not needed
1103
1114
  if (
@@ -1113,6 +1124,7 @@ const MemoriWidget = ({
1113
1124
  text: emission,
1114
1125
  emitter: state.emitter,
1115
1126
  media: state.emittedMedia ?? state.media,
1127
+ llmUsage: (state as any).llmUsage,
1116
1128
  fromUser: false,
1117
1129
  questionAnswered: msg,
1118
1130
  contextVars: state.contextVars,
@@ -1170,6 +1182,7 @@ const MemoriWidget = ({
1170
1182
  translatedText: t.text,
1171
1183
  emitter: state.emitter,
1172
1184
  media: state.emittedMedia ?? state.media,
1185
+ llmUsage: (state as any).llmUsage,
1173
1186
  fromUser: false,
1174
1187
  questionAnswered: msg,
1175
1188
  generatedByAI: !!state.completion,
@@ -1190,6 +1203,7 @@ const MemoriWidget = ({
1190
1203
  text: emission,
1191
1204
  emitter: state.emitter,
1192
1205
  media: state.emittedMedia ?? state.media,
1206
+ llmUsage: (state as any).llmUsage,
1193
1207
  fromUser: false,
1194
1208
  questionAnswered: msg,
1195
1209
  contextVars: state.contextVars,
@@ -2184,6 +2198,7 @@ const MemoriWidget = ({
2184
2198
  const enableUpload = !!(showUpload ?? integrationConfig?.showUpload);
2185
2199
 
2186
2200
  const enableReasoning = !!(showReasoning ?? integrationConfig?.showReasoning);
2201
+ const enableMessageConsumption = !!runtimeShowMessageConsumption;
2187
2202
 
2188
2203
  const showWhyThisAnswer =
2189
2204
  integrationConfig?.showWhyThisAnswer === undefined
@@ -2431,6 +2446,7 @@ const MemoriWidget = ({
2431
2446
  ...m,
2432
2447
  })),
2433
2448
  fromUser: l.inbound,
2449
+ llmUsage: (l as any).llmUsage,
2434
2450
  timestamp: l.timestamp,
2435
2451
  emitter: l.emitter,
2436
2452
  initial: i === 0,
@@ -2615,6 +2631,7 @@ const MemoriWidget = ({
2615
2631
  ...m,
2616
2632
  })),
2617
2633
  fromUser: l.inbound,
2634
+ llmUsage: (l as any).llmUsage,
2618
2635
  timestamp: l.timestamp,
2619
2636
  emitter: l.emitter,
2620
2637
  initial: i === 0,
@@ -2977,6 +2994,7 @@ const MemoriWidget = ({
2977
2994
  simulateUserPrompt,
2978
2995
  showDates,
2979
2996
  showContextPerLine,
2997
+ showMessageConsumption: enableMessageConsumption,
2980
2998
  showAIicon,
2981
2999
  showUpload: enableUpload,
2982
3000
  showReasoning: enableReasoning,
@@ -120,9 +120,9 @@ A unified upload button that supports both images and documents with the followi
120
120
 
121
121
  ## Limits
122
122
 
123
- - **Images**: Maximum 5 images, 10MB per file, formats: .jpg, .jpeg, .png
124
- - **Documents**: Maximum 5 documents, 10MB per file, formats: .pdf, .txt, .json, .xlsx, .csv, .md
125
- - **Content**: Document content is limited to 200,000 characters per document (truncated if exceeded)
123
+ - **Images**: Maximum 5 images, 15MB per file, formats: .jpg, .jpeg, .png
124
+ - **Documents**: Maximum 5 documents, 15MB per file, formats: .pdf, .txt, .json, .xlsx, .csv, .md
125
+ - **Content**: Document content is limited to 300,000 characters per document (truncated if exceeded)
126
126
  `,
127
127
  },
128
128
  },
@@ -25,7 +25,7 @@ interface UploadManagerProps {
25
25
  type?: string;
26
26
  }[];
27
27
  memoriID?: string;
28
- /** Override total document payload limit (character count). */
28
+ /** Override per-document content length limit (character count). */
29
29
  maxTotalMessagePayload?: number;
30
30
  /** Max attachments (docs + images) per message. */
31
31
  maxDocumentsPerMessage?: number;
@@ -43,8 +43,10 @@ const UploadButton: React.FC<UploadManagerProps> = ({
43
43
  memoriID = '',
44
44
  maxTotalMessagePayload,
45
45
  maxDocumentsPerMessage = 10,
46
- maxDocumentContentLength = 200000,
46
+ maxDocumentContentLength = 300000,
47
47
  }) => {
48
+ const effectivePerDocumentLimit =
49
+ maxTotalMessagePayload ?? maxDocumentContentLength ?? 300000;
48
50
  // State
49
51
  const [isLoading, setIsLoading] = useState(false);
50
52
  const [errors, setErrors] = useState<
@@ -411,7 +413,7 @@ ${file.content}
411
413
  '.md',
412
414
  '.html',
413
415
  ];
414
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
416
+ const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB
415
417
 
416
418
  if (!ALLOWED_FILE_TYPES.includes(fileExt)) {
417
419
  addError({
@@ -438,29 +440,13 @@ ${file.content}
438
440
 
439
441
  // Validate total payload size. Returns result so caller can avoid showing this error when truncation was already shown.
440
442
  const validatePayloadSize = (
441
- newDocuments: {
443
+ _newDocuments: {
442
444
  name: string;
443
445
  id: string;
444
446
  content: string;
445
447
  mimeType: string;
446
448
  }[]
447
449
  ): { valid: boolean; message?: string } => {
448
- const limit = maxTotalMessagePayload ?? 200000;
449
-
450
- const existingDocuments = documentPreviewFiles.filter(
451
- (file: any) => file.type === 'document'
452
- );
453
-
454
- const allDocuments = [...existingDocuments, ...newDocuments];
455
- const totalPayloadSize = allDocuments.reduce(
456
- (total, doc) => total + doc.content.length,
457
- 0
458
- );
459
-
460
- if (totalPayloadSize > limit) {
461
- return { valid: false, message: '' };
462
- }
463
-
464
450
  return { valid: true };
465
451
  };
466
452
 
@@ -476,7 +462,7 @@ ${file.content}
476
462
  const validateImageFile = (file: File): boolean => {
477
463
  const fileExt = `.${file.name.split('.').pop()?.toLowerCase()}`;
478
464
  const ALLOWED_FILE_TYPES = ['.jpg', '.jpeg', '.png'];
479
- const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
465
+ const MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB
480
466
 
481
467
  if (
482
468
  !ALLOWED_FILE_TYPES.includes(fileExt) &&
@@ -580,8 +566,7 @@ ${file.content}
580
566
  onDocumentError={handleDocumentError}
581
567
  onValidateFile={validateDocumentFile}
582
568
  onValidatePayloadSize={validatePayloadSize}
583
- maxTotalMessagePayload={maxTotalMessagePayload}
584
- maxDocumentContentLength={maxDocumentContentLength}
569
+ maxDocumentContentLength={effectivePerDocumentLimit}
585
570
  />
586
571
  </div>
587
572
 
@@ -51,8 +51,6 @@ interface UploadDocumentsProps {
51
51
  mimeType: string;
52
52
  }[]
53
53
  ) => boolean | { valid: boolean; message?: string };
54
- /** Same as total payload: overrides per-document content limit (character count). */
55
- maxTotalMessagePayload?: number;
56
54
  /** Per-document content character limit. */
57
55
  maxDocumentContentLength?: number;
58
56
  }
@@ -65,8 +63,7 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
65
63
  onDocumentError,
66
64
  onValidateFile,
67
65
  onValidatePayloadSize,
68
- maxTotalMessagePayload,
69
- maxDocumentContentLength = 200000,
66
+ maxDocumentContentLength = 300000,
70
67
  }) => {
71
68
  const { t } = useTranslation();
72
69
 
@@ -307,11 +304,6 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
307
304
  mimeType: string;
308
305
  }[] = [];
309
306
  let hadTruncation = false;
310
- let skippedDueToPayload = 0;
311
- const payloadLimit = maxTotalMessagePayload ?? 200000;
312
- const existingTotal = documentPreviewFiles
313
- .filter((f: any) => f.type === 'document')
314
- .reduce((sum: number, f: any) => sum + (f.content?.length ?? 0), 0);
315
307
 
316
308
  for (const file of filesToProcess) {
317
309
  if (!validateDocumentFile(file)) {
@@ -325,14 +317,6 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
325
317
  if (wasTruncated) hadTruncation = true;
326
318
 
327
319
  if (text) {
328
- const processedSum = processedFiles.reduce(
329
- (s, d) => s + d.content.length,
330
- 0
331
- );
332
- if (existingTotal + processedSum + text.length > payloadLimit) {
333
- skippedDueToPayload += 1;
334
- continue;
335
- }
336
320
  processedFiles.push({
337
321
  name: file.name,
338
322
  id: fileId,
@@ -351,16 +335,6 @@ const UploadDocuments: React.FC<UploadDocumentsProps> = ({
351
335
  }
352
336
  }
353
337
 
354
- if (skippedDueToPayload > 0) {
355
- onDocumentError?.({
356
- message: t('upload.documentsNotAddedContextSize', {
357
- count: skippedDueToPayload,
358
- defaultValue: `${skippedDueToPayload} document(s) not added (context size limit).`,
359
- }),
360
- severity: 'warning',
361
- });
362
- }
363
-
364
338
  // Add new documents to existing ones (only those that fit within payload)
365
339
  if (processedFiles.length > 0) {
366
340
  const existingDocuments = documentPreviewFiles.filter(
@@ -207,7 +207,6 @@ export const MAX_MSG_CHARS = 4000;
207
207
  export const MAX_MSG_WORDS = 300;
208
208
 
209
209
  export const maxDocumentsPerMessage = 10;
210
- export const maxTotalMessagePayloadDefault = 200000;
211
- export const maxDocumentContentLength = 200000;
210
+ export const maxDocumentContentLength = 300000;
212
211
  export const pasteAsCardLineThreshold = 100;
213
212
  export const pasteAsCardCharThreshold = 4200;
@@ -0,0 +1,328 @@
1
+ export interface LlmUsageOnLine {
2
+ provider?: string;
3
+ model?: string;
4
+ totalInputTokens?: number;
5
+ inputCacheReadTokens?: number;
6
+ inputCacheWriteTokens?: number;
7
+ outputTokens?: number;
8
+ durationMs?: number;
9
+ energyImpact?: {
10
+ energy?: number | { source?: string; parsedValue?: number };
11
+ energyUnit?: string;
12
+ gwp?: number | { source?: string; parsedValue?: number };
13
+ gwpUnit?: string;
14
+ wcf?: number | { source?: string; parsedValue?: number };
15
+ wcfUnit?: string;
16
+ };
17
+ }
18
+
19
+ export type UsageBadgeType = 'llm' | 'energy' | 'co2' | 'water';
20
+ type NumericMetric = number | { source?: string; parsedValue?: number };
21
+ type ImpactMetricType = 'energy' | 'co2' | 'water';
22
+ type TranslateFn = (key: string, options?: { [key: string]: unknown }) => string;
23
+
24
+ export interface LlmUsageLabels {
25
+ llm: string;
26
+ model: string;
27
+ provider: string;
28
+ tokens: string;
29
+ input: string;
30
+ output: string;
31
+ cacheRead: string;
32
+ cacheWrite: string;
33
+ duration: string;
34
+ energy: string;
35
+ co2: string;
36
+ water: string;
37
+ usageBadgesHint: string;
38
+ }
39
+
40
+ export const BADGE_EMOJI: Record<UsageBadgeType, string> = {
41
+ llm: '🤖',
42
+ energy: '⚡',
43
+ co2: '🌍',
44
+ water: '💧',
45
+ };
46
+
47
+ export const escapeHtml = (value: string): string =>
48
+ value
49
+ .replaceAll('&', '&amp;')
50
+ .replaceAll('<', '&lt;')
51
+ .replaceAll('>', '&gt;')
52
+ .replaceAll('"', '&quot;')
53
+ .replaceAll("'", '&#39;');
54
+
55
+ export const getMetricValue = (metric?: NumericMetric): number | undefined => {
56
+ if (typeof metric === 'number' && Number.isFinite(metric)) return metric;
57
+ if (!metric || typeof metric !== 'object') return undefined;
58
+ if (
59
+ typeof metric.parsedValue === 'number' &&
60
+ Number.isFinite(metric.parsedValue)
61
+ ) {
62
+ return metric.parsedValue;
63
+ }
64
+ if (typeof metric.source === 'string') {
65
+ const parsed = Number(metric.source);
66
+ if (Number.isFinite(parsed)) return parsed;
67
+ }
68
+ return undefined;
69
+ };
70
+
71
+ const formatMetricValue = (value: number, locale = 'it-IT'): string => {
72
+ if (!Number.isFinite(value)) return '—';
73
+ if (value === 0) return '0';
74
+
75
+ const absValue = Math.abs(value);
76
+ return new Intl.NumberFormat(locale, {
77
+ minimumFractionDigits: 0,
78
+ maximumFractionDigits: absValue >= 1 ? 3 : 4,
79
+ }).format(value);
80
+ };
81
+
82
+ export const formatIntegerValue = (value: number, locale = 'it-IT'): string => {
83
+ if (!Number.isFinite(value)) return '0';
84
+ return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(
85
+ value,
86
+ );
87
+ };
88
+
89
+ export const formatDuration = (durationMs?: number, locale = 'it-IT'): string => {
90
+ if (typeof durationMs !== 'number' || !Number.isFinite(durationMs)) {
91
+ return '—';
92
+ }
93
+ if (durationMs < 1000) return `${formatIntegerValue(durationMs, locale)} ms`;
94
+ return `${formatMetricValue(durationMs / 1000, locale)} s`;
95
+ };
96
+
97
+ const floorToSingleDecimal = (value: number): number =>
98
+ Math.floor(value * 10) / 10;
99
+
100
+ const formatComparisonNumber = (value: number, locale = 'it-IT'): string =>
101
+ new Intl.NumberFormat(locale, {
102
+ minimumFractionDigits: 1,
103
+ maximumFractionDigits: 1,
104
+ }).format(floorToSingleDecimal(value));
105
+
106
+ const formatReadableDuration = (seconds: number, locale = 'it-IT'): string => {
107
+ if (!Number.isFinite(seconds) || seconds <= 0) return '0 s';
108
+ if (seconds < 60) return `${formatComparisonNumber(seconds, locale)} s`;
109
+
110
+ const minutes = seconds / 60;
111
+ if (minutes < 60) return `${formatComparisonNumber(minutes, locale)} min`;
112
+
113
+ return `${formatComparisonNumber(minutes / 60, locale)} h`;
114
+ };
115
+
116
+ const formatReadableDistance = (meters: number, locale = 'it-IT'): string => {
117
+ if (!Number.isFinite(meters) || meters <= 0) return '0 m';
118
+ if (meters < 1000) return `${formatComparisonNumber(meters, locale)} m`;
119
+ return `${formatComparisonNumber(meters / 1000, locale)} km`;
120
+ };
121
+
122
+ const getApiUnitToBaseFactor = (
123
+ unitFromApi: string | undefined,
124
+ metricType: ImpactMetricType,
125
+ ): number => {
126
+ const u = (unitFromApi ?? '').trim().toLowerCase();
127
+ if (metricType === 'energy') {
128
+ if (u === 'kwh') return 1;
129
+ if (u === 'wh') return 0.001;
130
+ if (u === 'mwh') return 0.000001;
131
+ return 1;
132
+ }
133
+ if (metricType === 'co2') {
134
+ if (u === 'kg' || u === 'kgco2eq') return 1;
135
+ if (u === 'g') return 0.001;
136
+ if (u === 'mg') return 0.000001;
137
+ return 1;
138
+ }
139
+ if (u === 'l') return 1;
140
+ if (u === 'ml') return 0.001;
141
+ if (u === 'μl' || u === 'ul') return 0.000001;
142
+ return 1;
143
+ };
144
+
145
+ export const formatImpactInReadableUnit = (
146
+ value: number,
147
+ metricType: ImpactMetricType,
148
+ locale = 'it-IT',
149
+ ): string => {
150
+ const absValue = Math.abs(value);
151
+
152
+ if (metricType === 'energy') {
153
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} kWh`;
154
+ const wattHours = value * 1000;
155
+ if (Math.abs(wattHours) >= 1) return `${formatMetricValue(wattHours, locale)} Wh`;
156
+ return `${formatMetricValue(wattHours * 1000, locale)} mWh`;
157
+ }
158
+
159
+ if (metricType === 'co2') {
160
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} kg`;
161
+ const grams = value * 1000;
162
+ if (Math.abs(grams) >= 1) return `${formatMetricValue(grams, locale)} g`;
163
+ return `${formatMetricValue(grams * 1000, locale)} mg`;
164
+ }
165
+
166
+ if (absValue >= 1) return `${formatMetricValue(value, locale)} L`;
167
+ const milliliters = value * 1000;
168
+ if (Math.abs(milliliters) >= 1) return `${formatMetricValue(milliliters, locale)} mL`;
169
+ return `${formatMetricValue(milliliters * 1000, locale)} μL`;
170
+ };
171
+
172
+ export const formatImpactWithApiUnit = (
173
+ value: number,
174
+ unitFromApi: string | undefined,
175
+ fallbackUnit: string,
176
+ metricType: ImpactMetricType,
177
+ locale = 'it-IT',
178
+ ): string => {
179
+ const factor = getApiUnitToBaseFactor(
180
+ unitFromApi ?? fallbackUnit,
181
+ metricType,
182
+ );
183
+ const baseValue = value * factor;
184
+ return formatImpactInReadableUnit(baseValue, metricType, locale);
185
+ };
186
+
187
+ export const getImpactComparison = (
188
+ value: number,
189
+ metricType: ImpactMetricType,
190
+ locale = 'it-IT',
191
+ t: TranslateFn,
192
+ ): string => {
193
+ if (!Number.isFinite(value) || value <= 0) {
194
+ return t('chatLogs.impactComparisonUnavailable');
195
+ }
196
+
197
+ if (metricType === 'energy') {
198
+ const ledSeconds = (value * 1000 * 3600) / 10;
199
+ return t('chatLogs.impactComparisonEnergy', {
200
+ duration: formatReadableDuration(ledSeconds, locale),
201
+ });
202
+ }
203
+
204
+ if (metricType === 'co2') {
205
+ const averageCarMeters = (value / 0.12) * 1000;
206
+ return t('chatLogs.impactComparisonCo2', {
207
+ distance: formatReadableDistance(averageCarMeters, locale),
208
+ });
209
+ }
210
+
211
+ const drops = (value * 1000) / 0.05;
212
+ return t('chatLogs.impactComparisonWater', {
213
+ count: formatComparisonNumber(drops, locale),
214
+ });
215
+ };
216
+
217
+ const buildUsageBadgeHtml = ({
218
+ badgeType,
219
+ badgeClassName,
220
+ label,
221
+ value,
222
+ lineIndex,
223
+ emoji,
224
+ }: {
225
+ badgeType: UsageBadgeType;
226
+ badgeClassName: string;
227
+ label: string;
228
+ value?: string;
229
+ lineIndex: number;
230
+ emoji: string;
231
+ }): string => {
232
+ const escapedLabel = escapeHtml(label);
233
+ const valueHtml = value
234
+ ? `<span class="memori-chat--usage-badge-value">${value}</span>`
235
+ : '';
236
+ const content =
237
+ valueHtml ||
238
+ `<span class="memori-chat--usage-badge-label">${escapedLabel}</span>`;
239
+
240
+ return `<button type="button" class="memori-chat--usage-badge ${badgeClassName}" data-llm-badge-type="${badgeType}" data-line-index="${lineIndex}" aria-label="${escapedLabel}">${emoji} ${content}</button>`;
241
+ };
242
+
243
+ export const buildLlmUsageHtml = (
244
+ usage: LlmUsageOnLine,
245
+ labels: LlmUsageLabels,
246
+ lineIndex: number,
247
+ locale = 'it-IT',
248
+ ): string => {
249
+ const badges = [
250
+ buildUsageBadgeHtml({
251
+ badgeType: 'llm',
252
+ badgeClassName: 'memori-chat--usage-badge-llm',
253
+ label: labels.llm,
254
+ lineIndex,
255
+ emoji: BADGE_EMOJI.llm,
256
+ }),
257
+ ];
258
+
259
+ const energy = getMetricValue(usage.energyImpact?.energy);
260
+ const gwp = getMetricValue(usage.energyImpact?.gwp);
261
+ const wcf = getMetricValue(usage.energyImpact?.wcf);
262
+
263
+ if (typeof energy === 'number') {
264
+ const energyFormatted = formatImpactWithApiUnit(
265
+ energy,
266
+ usage.energyImpact?.energyUnit,
267
+ 'kWh',
268
+ 'energy',
269
+ locale,
270
+ );
271
+ badges.push(
272
+ buildUsageBadgeHtml({
273
+ badgeType: 'energy',
274
+ badgeClassName: 'memori-chat--usage-badge-energy',
275
+ label: `${labels.energy} ${energyFormatted}`,
276
+ value: escapeHtml(energyFormatted),
277
+ lineIndex,
278
+ emoji: BADGE_EMOJI.energy,
279
+ }),
280
+ );
281
+ }
282
+
283
+ if (typeof gwp === 'number') {
284
+ const co2Formatted = formatImpactWithApiUnit(
285
+ gwp,
286
+ usage.energyImpact?.gwpUnit,
287
+ 'kgCO2eq',
288
+ 'co2',
289
+ locale,
290
+ );
291
+ badges.push(
292
+ buildUsageBadgeHtml({
293
+ badgeType: 'co2',
294
+ badgeClassName: 'memori-chat--usage-badge-co2',
295
+ label: `${labels.co2} ${co2Formatted}`,
296
+ value: escapeHtml(co2Formatted),
297
+ lineIndex,
298
+ emoji: BADGE_EMOJI.co2,
299
+ }),
300
+ );
301
+ }
302
+
303
+ if (typeof wcf === 'number') {
304
+ const waterFormatted = formatImpactWithApiUnit(
305
+ wcf,
306
+ usage.energyImpact?.wcfUnit,
307
+ 'L',
308
+ 'water',
309
+ locale,
310
+ );
311
+ badges.push(
312
+ buildUsageBadgeHtml({
313
+ badgeType: 'water',
314
+ badgeClassName: 'memori-chat--usage-badge-water',
315
+ label: `${labels.water} ${waterFormatted}`,
316
+ value: escapeHtml(waterFormatted),
317
+ lineIndex,
318
+ emoji: BADGE_EMOJI.water,
319
+ }),
320
+ );
321
+ }
322
+
323
+ return `<div class="memori-chat--llm-usage" data-llm-usage><hr class="memori-chat--llm-usage-hr" /><p class="memori-chat--llm-usage-hint">${escapeHtml(
324
+ labels.usageBadgesHint,
325
+ )}</p><div class="memori-chat--llm-usage-badges">${badges.join(
326
+ '',
327
+ )}</div></div>`;
328
+ };
@@ -86,8 +86,8 @@ WithChatHistory.args = {
86
86
  showChatHistory: true,
87
87
  };
88
88
 
89
- export const WithUploadWithMaxTotalMessagePayload = Template.bind({});
90
- WithUploadWithMaxTotalMessagePayload.args = {
89
+ export const WithUploadNoTotalPayloadLimit = Template.bind({});
90
+ WithUploadNoTotalPayloadLimit.args = {
91
91
  ownerUserName: 'nzambello',
92
92
  memoriName: 'Nicola',
93
93
  tenantID: 'www.aisuru.com',
@@ -98,7 +98,6 @@ WithUploadWithMaxTotalMessagePayload.args = {
98
98
  spokenLang: 'IT',
99
99
  enableAudio: true,
100
100
  showUpload: true,
101
- maxTotalMessagePayload: 300000,
102
101
  };
103
102
 
104
103
  export const WithMaxTextareaCharacters = Template.bind({});