@memori.ai/memori-react 8.29.0 → 8.30.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 (142) hide show
  1. package/CHANGELOG.md +30 -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 +6 -6
  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/DrawerFooter/DrawerFooter.css +0 -1
  13. package/dist/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  14. package/dist/components/MemoriWidget/MemoriWidget.js +13 -3
  15. package/dist/components/MemoriWidget/MemoriWidget.js.map +1 -1
  16. package/dist/components/MicrophoneButton/MicrophoneButton.css +4 -14
  17. package/dist/components/MicrophoneButton/MicrophoneButton.js +1 -1
  18. package/dist/components/MicrophoneButton/MicrophoneButton.js.map +1 -1
  19. package/dist/components/StartPanel/StartPanel.js +1 -1
  20. package/dist/components/StartPanel/StartPanel.js.map +1 -1
  21. package/dist/components/UploadButton/UploadButton.js +8 -14
  22. package/dist/components/UploadButton/UploadButton.js.map +1 -1
  23. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +0 -1
  24. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js +1 -20
  25. package/dist/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  26. package/dist/components/layouts/fullpage.css +3 -1
  27. package/dist/components/layouts/website-assistant.css +1 -0
  28. package/dist/components/ui/Tooltip.css +9 -1
  29. package/dist/components/ui/Tooltip.js +80 -4
  30. package/dist/components/ui/Tooltip.js.map +1 -1
  31. package/dist/helpers/constants.d.ts +1 -2
  32. package/dist/helpers/constants.js +2 -3
  33. package/dist/helpers/constants.js.map +1 -1
  34. package/dist/helpers/llmUsage.d.ts +60 -0
  35. package/dist/helpers/llmUsage.js +223 -0
  36. package/dist/helpers/llmUsage.js.map +1 -0
  37. package/dist/helpers/message.d.ts +1 -0
  38. package/dist/helpers/message.js +3 -1
  39. package/dist/helpers/message.js.map +1 -1
  40. package/dist/helpers/userMessage.d.ts +2 -0
  41. package/dist/helpers/userMessage.js +23 -0
  42. package/dist/helpers/userMessage.js.map +1 -0
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.js +3 -1
  45. package/dist/index.js.map +1 -1
  46. package/dist/locales/de.json +22 -0
  47. package/dist/locales/en.json +22 -0
  48. package/dist/locales/es.json +22 -0
  49. package/dist/locales/fr.json +22 -0
  50. package/dist/locales/it.json +22 -0
  51. package/dist/version.d.ts +1 -1
  52. package/dist/version.js +1 -1
  53. package/esm/components/Chat/Chat.css +110 -9
  54. package/esm/components/Chat/Chat.d.ts +1 -0
  55. package/esm/components/Chat/Chat.js +74 -7
  56. package/esm/components/Chat/Chat.js.map +1 -1
  57. package/esm/components/ChatBubble/ChatBubble.d.ts +1 -0
  58. package/esm/components/ChatBubble/ChatBubble.js +7 -7
  59. package/esm/components/ChatBubble/ChatBubble.js.map +1 -1
  60. package/esm/components/ChatInputs/ChatInputs.js +2 -11
  61. package/esm/components/ChatInputs/ChatInputs.js.map +1 -1
  62. package/esm/components/DrawerFooter/DrawerFooter.css +0 -1
  63. package/esm/components/MemoriWidget/MemoriWidget.d.ts +2 -1
  64. package/esm/components/MemoriWidget/MemoriWidget.js +13 -3
  65. package/esm/components/MemoriWidget/MemoriWidget.js.map +1 -1
  66. package/esm/components/MicrophoneButton/MicrophoneButton.css +4 -14
  67. package/esm/components/MicrophoneButton/MicrophoneButton.js +1 -1
  68. package/esm/components/MicrophoneButton/MicrophoneButton.js.map +1 -1
  69. package/esm/components/StartPanel/StartPanel.js +1 -1
  70. package/esm/components/StartPanel/StartPanel.js.map +1 -1
  71. package/esm/components/UploadButton/UploadButton.js +8 -14
  72. package/esm/components/UploadButton/UploadButton.js.map +1 -1
  73. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.d.ts +0 -1
  74. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js +1 -20
  75. package/esm/components/UploadButton/UploadDocuments/UploadDocuments.js.map +1 -1
  76. package/esm/components/layouts/fullpage.css +3 -1
  77. package/esm/components/layouts/website-assistant.css +1 -0
  78. package/esm/components/ui/Tooltip.css +9 -1
  79. package/esm/components/ui/Tooltip.js +81 -5
  80. package/esm/components/ui/Tooltip.js.map +1 -1
  81. package/esm/helpers/constants.d.ts +1 -2
  82. package/esm/helpers/constants.js +1 -2
  83. package/esm/helpers/constants.js.map +1 -1
  84. package/esm/helpers/llmUsage.d.ts +60 -0
  85. package/esm/helpers/llmUsage.js +212 -0
  86. package/esm/helpers/llmUsage.js.map +1 -0
  87. package/esm/helpers/message.d.ts +1 -0
  88. package/esm/helpers/message.js +1 -0
  89. package/esm/helpers/message.js.map +1 -1
  90. package/esm/helpers/userMessage.d.ts +2 -0
  91. package/esm/helpers/userMessage.js +18 -0
  92. package/esm/helpers/userMessage.js.map +1 -0
  93. package/esm/index.d.ts +1 -0
  94. package/esm/index.js +3 -1
  95. package/esm/index.js.map +1 -1
  96. package/esm/locales/de.json +22 -0
  97. package/esm/locales/en.json +22 -0
  98. package/esm/locales/es.json +22 -0
  99. package/esm/locales/fr.json +22 -0
  100. package/esm/locales/it.json +22 -0
  101. package/esm/version.d.ts +1 -1
  102. package/esm/version.js +1 -1
  103. package/package.json +1 -1
  104. package/src/components/BlockedMemoriBadge/__snapshots__/BlockedMemoriBadge.test.tsx.snap +0 -29
  105. package/src/components/Chat/Chat.css +110 -9
  106. package/src/components/Chat/Chat.stories.tsx +42 -0
  107. package/src/components/Chat/Chat.test.tsx +47 -0
  108. package/src/components/Chat/Chat.tsx +238 -5
  109. package/src/components/Chat/__snapshots__/Chat.test.tsx.snap +745 -0
  110. package/src/components/ChatBubble/ChatBubble.tsx +20 -7
  111. package/src/components/ChatBubble/__snapshots__/ChatBubble.test.tsx.snap +0 -20
  112. package/src/components/ChatInputs/ChatInputs.tsx +4 -15
  113. package/src/components/CompletionProviderStatus/__snapshots__/CompletionProviderStatus.test.tsx.snap +0 -54
  114. package/src/components/FeedbackButtons/__snapshots__/FeedbackButtons.test.tsx.snap +0 -5
  115. package/src/components/MemoriWidget/MemoriWidget.tsx +20 -2
  116. package/src/components/MicrophoneButton/MicrophoneButton.css +4 -14
  117. package/src/components/MicrophoneButton/MicrophoneButton.tsx +0 -1
  118. package/src/components/StartPanel/StartPanel.tsx +1 -1
  119. package/src/components/StartPanel/__snapshots__/StartPanel.test.tsx.snap +11 -368
  120. package/src/components/UploadButton/UploadButton.stories.tsx +3 -3
  121. package/src/components/UploadButton/UploadButton.tsx +8 -23
  122. package/src/components/UploadButton/UploadDocuments/UploadDocuments.tsx +1 -27
  123. package/src/components/layouts/__snapshots__/Chat.test.tsx.snap +1 -30
  124. package/src/components/layouts/__snapshots__/FullPage.test.tsx.snap +2 -60
  125. package/src/components/layouts/__snapshots__/HiddenChat.test.tsx.snap +1 -30
  126. package/src/components/layouts/__snapshots__/Totem.test.tsx.snap +1 -30
  127. package/src/components/layouts/__snapshots__/ZoomedFullBody.test.tsx.snap +1 -30
  128. package/src/components/layouts/website-assistant.css +1 -0
  129. package/src/components/ui/Tooltip.css +9 -1
  130. package/src/components/ui/Tooltip.tsx +133 -17
  131. package/src/components/ui/__snapshots__/Tooltip.test.tsx.snap +5 -55
  132. package/src/helpers/constants.ts +1 -2
  133. package/src/helpers/llmUsage.ts +328 -0
  134. package/src/helpers/message.ts +3 -0
  135. package/src/index.stories.tsx +2 -3
  136. package/src/index.tsx +5 -1
  137. package/src/locales/de.json +22 -0
  138. package/src/locales/en.json +22 -0
  139. package/src/locales/es.json +22 -0
  140. package/src/locales/fr.json +22 -0
  141. package/src/locales/it.json +22 -0
  142. package/src/version.ts +1 -1
@@ -5,11 +5,6 @@ exports[`renders BlockedMemoriBadge for not enough credits unchanged 1`] = `
5
5
  <div
6
6
  class="memori-tooltip memori-tooltip--align-right blocked-memori-badge--tooltip"
7
7
  >
8
- <div
9
- class="memori-tooltip--content"
10
- >
11
- notEnoughCredits
12
- </div>
13
8
  <div
14
9
  class="memori-tooltip--trigger"
15
10
  >
@@ -45,11 +40,6 @@ exports[`renders BlockedMemoriBadge unchanged 1`] = `
45
40
  <div
46
41
  class="memori-tooltip memori-tooltip--align-right blocked-memori-badge--tooltip"
47
42
  >
48
- <div
49
- class="memori-tooltip--content"
50
- >
51
- memoriBlockedAnon
52
- </div>
53
43
  <div
54
44
  class="memori-tooltip--trigger"
55
45
  >
@@ -85,15 +75,6 @@ exports[`renders BlockedMemoriBadge with giver info unchanged 1`] = `
85
75
  <div
86
76
  class="memori-tooltip memori-tooltip--align-right blocked-memori-badge--tooltip"
87
77
  >
88
- <div
89
- class="memori-tooltip--content"
90
- >
91
- memoriBlockedUntil
92
- memoriBlockedReasonExceedChats
93
- <br />
94
-
95
- memoriBlockedGiverHelper
96
- </div>
97
78
  <div
98
79
  class="memori-tooltip--trigger"
99
80
  >
@@ -129,11 +110,6 @@ exports[`renders BlockedMemoriBadge with margin left unchanged 1`] = `
129
110
  <div
130
111
  class="memori-tooltip memori-tooltip--align-right blocked-memori-badge--tooltip"
131
112
  >
132
- <div
133
- class="memori-tooltip--content"
134
- >
135
- memoriBlockedAnon
136
- </div>
137
113
  <div
138
114
  class="memori-tooltip--trigger"
139
115
  >
@@ -169,11 +145,6 @@ exports[`renders BlockedMemoriBadge with title unchanged 1`] = `
169
145
  <div
170
146
  class="memori-tooltip memori-tooltip--align-right blocked-memori-badge--tooltip"
171
147
  >
172
- <div
173
- class="memori-tooltip--content"
174
- >
175
- memoriBlockedAnon
176
- </div>
177
148
  <div
178
149
  class="memori-tooltip--trigger"
179
150
  >
@@ -6,15 +6,6 @@
6
6
  flex-direction: column;
7
7
  }
8
8
 
9
- .memori-chat-wrapper--expanded {
10
- /* Ensure proper spacing when textarea is expanded */
11
- }
12
-
13
- /* Handle the focused chat state on mobile */
14
- .memori-chat--wrapper.chat-focused {
15
- /* Mobile keyboard adjustment */
16
- }
17
-
18
9
  @media (max-width: 768px) {
19
10
  .memori-chat--wrapper.chat-focused {
20
11
  padding-bottom: 0;
@@ -181,4 +172,114 @@
181
172
  font-size: 1.2rem;
182
173
  font-weight: 500;
183
174
  text-align: center;
175
+ }
176
+
177
+ .memori-chat--llm-usage {
178
+ margin-top: 0.625rem;
179
+ }
180
+
181
+ .memori-chat--usage-inside-bubble {
182
+ margin-top: 0.25rem;
183
+ }
184
+
185
+ .memori-chat--llm-usage-hr {
186
+ border: 0;
187
+ border-top: 1px solid rgba(148, 163, 184, 0.35);
188
+ margin: 0 0 0.5rem;
189
+ }
190
+
191
+ .memori-chat--llm-usage-hint {
192
+ margin: 0 0 0.5rem;
193
+ color: #64748b;
194
+ font-size: 0.75rem;
195
+ }
196
+
197
+ .memori-chat--llm-usage-badges {
198
+ display: flex;
199
+ flex-wrap: wrap;
200
+ margin-top: 0.5rem;
201
+ gap: 0.375rem;
202
+ }
203
+
204
+ .memori-chat--usage-badge {
205
+ display: inline-flex;
206
+ align-items: center;
207
+ padding: 0.35rem 0.5rem;
208
+ border: 1px solid #d4d8dd;
209
+ border-radius: 999px;
210
+ background: #fff;
211
+ color: #1f2937;
212
+ cursor: pointer;
213
+ font-size: 0.75rem;
214
+ gap: 0.25rem;
215
+ line-height: 1;
216
+ transition: border-color 0.2s ease, background-color 0.2s ease;
217
+ }
218
+
219
+ .memori-chat--usage-badge:hover {
220
+ border-color: var(--memori-primary,#1890ff);
221
+ background-color: #f8fafc;
222
+ color: var(--memori-primary,#1890ff);
223
+ }
224
+
225
+ .memori-chat--usage-badge:focus-visible {
226
+ outline: 2px solid var(--memori-primary);
227
+ outline-offset: 2px;
228
+ }
229
+
230
+ .memori-chat--usage-badge-value {
231
+ font-weight: 600;
232
+ }
233
+
234
+ .memori-chat--usage-modal .memori-modal--panel {
235
+ min-width: 20rem;
236
+ max-width: 30rem;
237
+ }
238
+
239
+ .memori-chat--usage-details {
240
+ margin: 0;
241
+ }
242
+
243
+ .memori-chat--usage-details div {
244
+ display: flex;
245
+ justify-content: space-between;
246
+ padding: 0.375rem 0;
247
+ gap: 1rem;
248
+ }
249
+
250
+ .memori-chat--usage-details dt {
251
+ color: #64748b;
252
+ font-size: 0.875rem;
253
+ }
254
+
255
+ .memori-chat--usage-details dd {
256
+ margin: 0;
257
+ color: #111827;
258
+ font-size: 0.875rem;
259
+ font-weight: 600;
260
+ text-align: right;
261
+ }
262
+
263
+ .memori-chat--usage-educational-content {
264
+ display: flex;
265
+ flex-direction: column;
266
+ padding-top: 0.25rem;
267
+ gap: 0.5rem;
268
+ }
269
+
270
+ .memori-chat--usage-metric-value {
271
+ display: block;
272
+ margin: 0;
273
+ color: #111827;
274
+ font-size: 1.125rem;
275
+ }
276
+
277
+ .memori-chat--usage-comparable {
278
+ margin: 0;
279
+ color: #374151;
280
+ font-size: 0.875rem;
281
+ }
282
+
283
+ .memori-chat--usage-educational-content p {
284
+ margin: 0;
184
285
  }
@@ -100,6 +100,48 @@ WithHints.args = {
100
100
  setSendOnEnter: () => {},
101
101
  };
102
102
 
103
+ export const WithMessageConsumption = Template.bind({});
104
+ WithMessageConsumption.args = {
105
+ memori,
106
+ tenant,
107
+ sessionID,
108
+ dialogState,
109
+ layout: 'DEFAULT',
110
+ simulateUserPrompt: () => {},
111
+ sendMessage: (msg: string) => console.log(msg),
112
+ stopListening: () => {},
113
+ resetTranscript: () => {},
114
+ setAttachmentsMenuOpen: () => {},
115
+ setSendOnEnter: () => {},
116
+ showMessageConsumption: true,
117
+ history: [
118
+ ...history.slice(0, Math.max(0, history.length - 2)),
119
+ // Add one AI message with llmUsage for the badges
120
+ {
121
+ ...(history.find(m => !m.fromUser) ?? history[history.length - 1]),
122
+ fromUser: false,
123
+ text: 'Risposta di esempio con consumo LLM.',
124
+ llmUsage: {
125
+ provider: 'openai',
126
+ model: 'gpt-4.1-mini',
127
+ totalInputTokens: 123,
128
+ inputCacheReadTokens: 10,
129
+ inputCacheWriteTokens: 5,
130
+ outputTokens: 456,
131
+ durationMs: 2345,
132
+ energyImpact: {
133
+ energy: 0.00012,
134
+ energyUnit: 'kWh',
135
+ gwp: 0.00009,
136
+ gwpUnit: 'kgCO2eq',
137
+ wcf: 0.02,
138
+ wcfUnit: 'L',
139
+ },
140
+ },
141
+ } as any,
142
+ ],
143
+ };
144
+
103
145
  export const WithArtifacts = Template.bind({});
104
146
  WithArtifacts.args = {
105
147
  memori,
@@ -225,6 +225,53 @@ it('renders Chat with context vars unchanged', () => {
225
225
  expect(container).toMatchSnapshot();
226
226
  });
227
227
 
228
+ it('renders Chat with message consumption unchanged', () => {
229
+ const { container } = render(
230
+ <ArtifactProvider>
231
+ <Chat
232
+ memori={memori}
233
+ tenant={tenant}
234
+ dialogState={dialogState}
235
+ layout="DEFAULT"
236
+ client={client}
237
+ history={[
238
+ ...(history as any),
239
+ {
240
+ text: 'AI message with usage',
241
+ fromUser: false,
242
+ timestamp: new Date().toISOString(),
243
+ llmUsage: {
244
+ provider: 'openai',
245
+ model: 'gpt-4.1-mini',
246
+ totalInputTokens: 10,
247
+ outputTokens: 20,
248
+ durationMs: 1000,
249
+ energyImpact: { energy: 0.001, energyUnit: 'kWh' },
250
+ },
251
+ } as any,
252
+ ]}
253
+ pushMessage={jest.fn()}
254
+ sessionID={sessionID}
255
+ simulateUserPrompt={jest.fn()}
256
+ setAttachmentsMenuOpen={jest.fn()}
257
+ setSendOnEnter={jest.fn()}
258
+ userMessage=""
259
+ onChangeUserMessage={jest.fn()}
260
+ sendMessage={jest.fn()}
261
+ stopListening={jest.fn()}
262
+ isPlayingAudio={false}
263
+ stopAudio={jest.fn()}
264
+ showMicrophone={false}
265
+ listening={false}
266
+ startListening={jest.fn()}
267
+ setEnableFocusChatInput={jest.fn()}
268
+ showMessageConsumption
269
+ />
270
+ </ArtifactProvider>
271
+ );
272
+ expect(container).toMatchSnapshot();
273
+ });
274
+
228
275
  it('renders Chat with user unchanged', () => {
229
276
  const { container } = render(
230
277
  <ArtifactProvider
@@ -1,4 +1,11 @@
1
- import React, { useCallback, useEffect, memo, useRef, useState } from 'react';
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ memo,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from 'react';
2
9
  import cx from 'classnames';
3
10
  import {
4
11
  DialogState,
@@ -23,7 +30,21 @@ import { boardOfExpertsLoadingSentences } from '../../helpers/constants';
23
30
  import ArtifactHandler from '../MemoriArtifactSystem/components/ArtifactHandler/ArtifactHandler';
24
31
  import { DocumentIcon } from '../icons/Document';
25
32
  import { useTranslation } from 'react-i18next';
26
- import { maxDocumentsPerMessage, maxTotalMessagePayloadDefault, maxDocumentContentLength, pasteAsCardLineThreshold, pasteAsCardCharThreshold } from '../../helpers/constants';
33
+ import { maxDocumentsPerMessage, maxDocumentContentLength, pasteAsCardLineThreshold, pasteAsCardCharThreshold } from '../../helpers/constants';
34
+ import Modal from '../ui/Modal';
35
+ import Tooltip from '../ui/Tooltip';
36
+ import {
37
+ BADGE_EMOJI,
38
+ buildLlmUsageHtml,
39
+ formatDuration,
40
+ formatImpactWithApiUnit,
41
+ formatIntegerValue,
42
+ getImpactComparison,
43
+ getMetricValue,
44
+ LlmUsageLabels,
45
+ LlmUsageOnLine,
46
+ UsageBadgeType,
47
+ } from '../../helpers/llmUsage';
27
48
  export interface Props {
28
49
  memori: Memori;
29
50
  tenant?: Tenant;
@@ -46,6 +67,7 @@ export interface Props {
46
67
  showTranslationOriginal?: boolean;
47
68
  showWhyThisAnswer?: boolean;
48
69
  showReasoning?: boolean;
70
+ showMessageConsumption?: boolean;
49
71
  client?: ReturnType<typeof memoriApiClient>;
50
72
  preview?: boolean;
51
73
  microphoneMode?: 'CONTINUOUS' | 'HOLD_TO_TALK';
@@ -83,6 +105,12 @@ export interface Props {
83
105
 
84
106
  }
85
107
 
108
+ type MessageWithLlmUsage = Message & { llmUsage?: LlmUsageOnLine };
109
+ interface UsageBadgeModalState {
110
+ type: UsageBadgeType;
111
+ usage: LlmUsageOnLine;
112
+ }
113
+
86
114
  const Chat: React.FC<Props> = ({
87
115
  memori,
88
116
  tenant,
@@ -105,6 +133,7 @@ const Chat: React.FC<Props> = ({
105
133
  showCopyButton = true,
106
134
  showTranslationOriginal = false,
107
135
  showReasoning = false,
136
+ showMessageConsumption = false,
108
137
  preview = false,
109
138
  instruct = false,
110
139
  showInputs = true,
@@ -138,7 +167,50 @@ const Chat: React.FC<Props> = ({
138
167
 
139
168
  const [isTextareaExpanded, setIsTextareaExpanded] = useState(false);
140
169
  const [isDragging, setIsDragging] = useState(false);
170
+ const [activeUsageBadge, setActiveUsageBadge] =
171
+ useState<UsageBadgeModalState | null>(null);
172
+ const chatWrapperRef = useRef<HTMLDivElement>(null);
141
173
  const { t } = useTranslation();
174
+ const locale = (translateTo || memori.culture || 'it-IT').replace('_', '-');
175
+
176
+ const llmUsageLabels = useMemo<LlmUsageLabels>(
177
+ () => ({
178
+ llm: t('chatLogs.llm', { defaultValue: 'LLM' }),
179
+ model: t('chatLogs.model', { defaultValue: 'Model' }),
180
+ provider: t('chatLogs.provider', { defaultValue: 'Provider' }),
181
+ tokens: t('chatLogs.tokens', { defaultValue: 'Tokens' }),
182
+ input: t('chatLogs.input', { defaultValue: 'Input' }),
183
+ output: t('chatLogs.output', { defaultValue: 'Output' }),
184
+ cacheRead: t('chatLogs.cacheRead', { defaultValue: 'Cache read' }),
185
+ cacheWrite: t('chatLogs.cacheWrite', { defaultValue: 'Cache write' }),
186
+ duration: t('chatLogs.duration', { defaultValue: 'Duration' }),
187
+ energy: t('chatLogs.energy', { defaultValue: 'Energy' }),
188
+ co2: t('chatLogs.co2', { defaultValue: 'CO2' }),
189
+ water: t('chatLogs.water', { defaultValue: 'Water' }),
190
+ usageBadgesHint: t('chatLogs.usageBadgesHint', {
191
+ defaultValue: 'Click one of these buttons to show more information',
192
+ }),
193
+ }),
194
+ [t],
195
+ );
196
+
197
+ const usageHtmlByIndex = useMemo(
198
+ () =>
199
+ history.map((message, index) => {
200
+ const messageWithUsage = message as MessageWithLlmUsage;
201
+ return showMessageConsumption &&
202
+ !message.fromUser &&
203
+ messageWithUsage.llmUsage
204
+ ? buildLlmUsageHtml(
205
+ messageWithUsage.llmUsage,
206
+ llmUsageLabels,
207
+ index,
208
+ locale,
209
+ )
210
+ : '';
211
+ }),
212
+ [history, llmUsageLabels, locale, showMessageConsumption],
213
+ );
142
214
  const scrollToBottom = useCallback(() => {
143
215
  if (isHistoryView) return;
144
216
  setTimeout(() => {
@@ -269,8 +341,40 @@ const Chat: React.FC<Props> = ({
269
341
  };
270
342
  }, [showUpload]);
271
343
 
344
+ useEffect(() => {
345
+ const wrapper = chatWrapperRef.current;
346
+ if (!wrapper || !showMessageConsumption) return;
347
+
348
+ const handleUsageBadgeClick = (event: MouseEvent) => {
349
+ const target = event.target as HTMLElement | null;
350
+ const button = target?.closest<HTMLElement>(
351
+ '[data-llm-badge-type][data-line-index]',
352
+ );
353
+ if (!button) return;
354
+
355
+ const lineIndex = Number(button.dataset.lineIndex);
356
+ const badgeType = button.dataset.llmBadgeType as UsageBadgeType | undefined;
357
+ if (!Number.isInteger(lineIndex) || !badgeType) return;
358
+
359
+ const line = (history?.[lineIndex] as MessageWithLlmUsage) ?? null;
360
+ if (!line?.llmUsage) return;
361
+
362
+ event.preventDefault();
363
+ setActiveUsageBadge({
364
+ type: badgeType,
365
+ usage: line.llmUsage,
366
+ });
367
+ };
368
+
369
+ wrapper.addEventListener('click', handleUsageBadgeClick);
370
+ return () => {
371
+ wrapper.removeEventListener('click', handleUsageBadgeClick);
372
+ };
373
+ }, [history, showMessageConsumption]);
374
+
272
375
  return (
273
376
  <div
377
+ ref={chatWrapperRef}
274
378
  className={cx('memori-chat--wrapper', {
275
379
  'memori-chat-wrapper--translate': translateTo,
276
380
  'memori-chat-wrapper--expanded': isTextareaExpanded,
@@ -423,6 +527,7 @@ const Chat: React.FC<Props> = ({
423
527
  useMathFormatting={useMathFormatting}
424
528
  showFunctionCache={showFunctionCache}
425
529
  showReasoning={showReasoning}
530
+ usageHtml={usageHtmlByIndex[index]}
426
531
  />
427
532
 
428
533
  {showDates && !!message.timestamp && (
@@ -552,9 +657,7 @@ const Chat: React.FC<Props> = ({
552
657
  isPlayingAudio={isPlayingAudio}
553
658
  showMicrophone={showMicrophone}
554
659
  memoriID={memori?.memoriID}
555
- maxTotalMessagePayload={
556
- maxTotalMessagePayload ?? maxTotalMessagePayloadDefault
557
- }
660
+ maxTotalMessagePayload={maxTotalMessagePayload}
558
661
  maxTextareaCharacters={maxTextareaCharacters}
559
662
  maxDocumentsPerMessage={maxDocumentsPerMessage}
560
663
  maxDocumentContentLength={maxDocumentContentLength}
@@ -562,6 +665,136 @@ const Chat: React.FC<Props> = ({
562
665
  pasteAsCardCharThreshold={pasteAsCardCharThreshold}
563
666
  />
564
667
  )}
668
+
669
+ <Modal
670
+ open={!!activeUsageBadge}
671
+ onClose={() => setActiveUsageBadge(null)}
672
+ title={
673
+ activeUsageBadge?.type
674
+ ? `${BADGE_EMOJI[activeUsageBadge.type]} ${
675
+ llmUsageLabels[activeUsageBadge.type]
676
+ }`
677
+ : undefined
678
+ }
679
+ className="memori-chat--usage-modal"
680
+ >
681
+ {activeUsageBadge?.type === 'llm' && (
682
+ <dl className="memori-chat--usage-details">
683
+ <div>
684
+ <dt>{llmUsageLabels.provider}</dt>
685
+ <dd>{activeUsageBadge.usage.provider ?? '—'}</dd>
686
+ </div>
687
+ <div>
688
+ <dt>{llmUsageLabels.model}</dt>
689
+ <dd>{activeUsageBadge.usage.model ?? '—'}</dd>
690
+ </div>
691
+ <div>
692
+ <dt>
693
+ {llmUsageLabels.tokens} {llmUsageLabels.input}
694
+ </dt>
695
+ <dd>
696
+ {formatIntegerValue(
697
+ activeUsageBadge.usage.totalInputTokens ?? 0,
698
+ locale,
699
+ )}
700
+ </dd>
701
+ </div>
702
+ <div>
703
+ <dt>
704
+ {llmUsageLabels.tokens} {llmUsageLabels.output}
705
+ </dt>
706
+ <dd>
707
+ {formatIntegerValue(activeUsageBadge.usage.outputTokens ?? 0, locale)}
708
+ </dd>
709
+ </div>
710
+ </dl>
711
+ )}
712
+
713
+ {activeUsageBadge?.type === 'energy' && (
714
+ <div className="memori-chat--usage-educational-content">
715
+ <strong className="memori-chat--usage-metric-value">
716
+ {formatImpactWithApiUnit(
717
+ getMetricValue(activeUsageBadge.usage.energyImpact?.energy) ?? 0,
718
+ activeUsageBadge.usage.energyImpact?.energyUnit,
719
+ 'kWh',
720
+ 'energy',
721
+ locale,
722
+ )}
723
+ </strong>
724
+ <Tooltip
725
+ content={t('chatLogs.approximateValuesTooltip', {
726
+ defaultValue: 'These values are approximate.',
727
+ })}
728
+ >
729
+ <p className="memori-chat--usage-comparable">
730
+ {getImpactComparison(
731
+ getMetricValue(activeUsageBadge.usage.energyImpact?.energy) ?? 0,
732
+ 'energy',
733
+ locale,
734
+ t,
735
+ )}
736
+ </p>
737
+ </Tooltip>
738
+ <p>{t('chatLogs.energyImpactDescription')}</p>
739
+ </div>
740
+ )}
741
+ {activeUsageBadge?.type === 'co2' && (
742
+ <div className="memori-chat--usage-educational-content">
743
+ <strong className="memori-chat--usage-metric-value">
744
+ {formatImpactWithApiUnit(
745
+ getMetricValue(activeUsageBadge.usage.energyImpact?.gwp) ?? 0,
746
+ activeUsageBadge.usage.energyImpact?.gwpUnit,
747
+ 'kgCO2eq',
748
+ 'co2',
749
+ locale,
750
+ )}
751
+ </strong>
752
+ <Tooltip
753
+ content={t('chatLogs.approximateValuesTooltip', {
754
+ defaultValue: 'These values are approximate.',
755
+ })}
756
+ >
757
+ <p className="memori-chat--usage-comparable">
758
+ {getImpactComparison(
759
+ getMetricValue(activeUsageBadge.usage.energyImpact?.gwp) ?? 0,
760
+ 'co2',
761
+ locale,
762
+ t,
763
+ )}
764
+ </p>
765
+ </Tooltip>
766
+ <p>{t('chatLogs.co2ImpactDescription')}</p>
767
+ </div>
768
+ )}
769
+ {activeUsageBadge?.type === 'water' && (
770
+ <div className="memori-chat--usage-educational-content">
771
+ <strong className="memori-chat--usage-metric-value">
772
+ {formatImpactWithApiUnit(
773
+ getMetricValue(activeUsageBadge.usage.energyImpact?.wcf) ?? 0,
774
+ activeUsageBadge.usage.energyImpact?.wcfUnit,
775
+ 'L',
776
+ 'water',
777
+ locale,
778
+ )}
779
+ </strong>
780
+ <Tooltip
781
+ content={t('chatLogs.approximateValuesTooltip', {
782
+ defaultValue: 'These values are approximate.',
783
+ })}
784
+ >
785
+ <p className="memori-chat--usage-comparable">
786
+ {getImpactComparison(
787
+ getMetricValue(activeUsageBadge.usage.energyImpact?.wcf) ?? 0,
788
+ 'water',
789
+ locale,
790
+ t,
791
+ )}
792
+ </p>
793
+ </Tooltip>
794
+ <p>{t('chatLogs.waterImpactDescription')}</p>
795
+ </div>
796
+ )}
797
+ </Modal>
565
798
  </div>
566
799
  );
567
800
  };