@bytexbyte/nxtlinq-ai-agent-ui-react-development 0.1.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 (142) hide show
  1. package/dist/NxtlinqAgentChat.d.ts +26 -0
  2. package/dist/NxtlinqAgentChat.d.ts.map +1 -0
  3. package/dist/NxtlinqAgentChat.js +28 -0
  4. package/dist/components/AgentAssistantShell.d.ts +5 -0
  5. package/dist/components/AgentAssistantShell.d.ts.map +1 -0
  6. package/dist/components/AgentAssistantShell.js +52 -0
  7. package/dist/components/AgentComposer.d.ts +3 -0
  8. package/dist/components/AgentComposer.d.ts.map +1 -0
  9. package/dist/components/AgentComposer.js +60 -0
  10. package/dist/components/AgentMessageList.d.ts +3 -0
  11. package/dist/components/AgentMessageList.d.ts.map +1 -0
  12. package/dist/components/AgentMessageList.js +37 -0
  13. package/dist/components/AgentRemoteAudio.d.ts +4 -0
  14. package/dist/components/AgentRemoteAudio.d.ts.map +1 -0
  15. package/dist/components/AgentRemoteAudio.js +34 -0
  16. package/dist/components/AgentVoiceBar.d.ts +3 -0
  17. package/dist/components/AgentVoiceBar.d.ts.map +1 -0
  18. package/dist/components/AgentVoiceBar.js +91 -0
  19. package/dist/components/PresetMessageChips.d.ts +3 -0
  20. package/dist/components/PresetMessageChips.d.ts.map +1 -0
  21. package/dist/components/PresetMessageChips.js +23 -0
  22. package/dist/context/AgentAssistantContext.d.ts +32 -0
  23. package/dist/context/AgentAssistantContext.d.ts.map +1 -0
  24. package/dist/context/AgentAssistantContext.js +159 -0
  25. package/dist/index.d.ts +16 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +12 -0
  28. package/dist/legacy/assets/images/adiSideItalicDataUri.d.ts +2 -0
  29. package/dist/legacy/assets/images/adiSideItalicDataUri.d.ts.map +1 -0
  30. package/dist/legacy/assets/images/adiSideItalicDataUri.js +1 -0
  31. package/dist/legacy/chatbot/ChatBot.d.ts +5 -0
  32. package/dist/legacy/chatbot/ChatBot.d.ts.map +1 -0
  33. package/dist/legacy/chatbot/ChatBot.js +35 -0
  34. package/dist/legacy/chatbot/context/ChatBotContext.d.ts +5 -0
  35. package/dist/legacy/chatbot/context/ChatBotContext.d.ts.map +1 -0
  36. package/dist/legacy/chatbot/context/ChatBotContext.js +2908 -0
  37. package/dist/legacy/chatbot/types/ChatBotTypes.d.ts +166 -0
  38. package/dist/legacy/chatbot/types/ChatBotTypes.d.ts.map +1 -0
  39. package/dist/legacy/chatbot/types/ChatBotTypes.js +1 -0
  40. package/dist/legacy/chatbot/ui/BerifyMeModal.d.ts +17 -0
  41. package/dist/legacy/chatbot/ui/BerifyMeModal.d.ts.map +1 -0
  42. package/dist/legacy/chatbot/ui/BerifyMeModal.js +110 -0
  43. package/dist/legacy/chatbot/ui/ChatBotUI.d.ts +3 -0
  44. package/dist/legacy/chatbot/ui/ChatBotUI.d.ts.map +1 -0
  45. package/dist/legacy/chatbot/ui/ChatBotUI.js +625 -0
  46. package/dist/legacy/chatbot/ui/MessageInput.d.ts +3 -0
  47. package/dist/legacy/chatbot/ui/MessageInput.d.ts.map +1 -0
  48. package/dist/legacy/chatbot/ui/MessageInput.js +321 -0
  49. package/dist/legacy/chatbot/ui/MessageList.d.ts +4 -0
  50. package/dist/legacy/chatbot/ui/MessageList.d.ts.map +1 -0
  51. package/dist/legacy/chatbot/ui/MessageList.js +455 -0
  52. package/dist/legacy/chatbot/ui/ModelSelector.d.ts +4 -0
  53. package/dist/legacy/chatbot/ui/ModelSelector.d.ts.map +1 -0
  54. package/dist/legacy/chatbot/ui/ModelSelector.js +122 -0
  55. package/dist/legacy/chatbot/ui/NotificationModal.d.ts +15 -0
  56. package/dist/legacy/chatbot/ui/NotificationModal.d.ts.map +1 -0
  57. package/dist/legacy/chatbot/ui/NotificationModal.js +53 -0
  58. package/dist/legacy/chatbot/ui/PermissionForm.d.ts +8 -0
  59. package/dist/legacy/chatbot/ui/PermissionForm.d.ts.map +1 -0
  60. package/dist/legacy/chatbot/ui/PermissionForm.js +465 -0
  61. package/dist/legacy/chatbot/ui/PresetMessages.d.ts +4 -0
  62. package/dist/legacy/chatbot/ui/PresetMessages.d.ts.map +1 -0
  63. package/dist/legacy/chatbot/ui/PresetMessages.js +33 -0
  64. package/dist/legacy/chatbot/ui/VoiceModePanel.d.ts +3 -0
  65. package/dist/legacy/chatbot/ui/VoiceModePanel.d.ts.map +1 -0
  66. package/dist/legacy/chatbot/ui/VoiceModePanel.js +95 -0
  67. package/dist/legacy/chatbot/ui/styles/isolatedStyles.d.ts +73 -0
  68. package/dist/legacy/chatbot/ui/styles/isolatedStyles.d.ts.map +1 -0
  69. package/dist/legacy/chatbot/ui/styles/isolatedStyles.js +985 -0
  70. package/dist/legacy/index.d.ts +14 -0
  71. package/dist/legacy/index.d.ts.map +1 -0
  72. package/dist/legacy/index.js +12 -0
  73. package/dist/theme/defaultTheme.d.ts +3 -0
  74. package/dist/theme/defaultTheme.d.ts.map +1 -0
  75. package/dist/theme/defaultTheme.js +20 -0
  76. package/dist/types.d.ts +62 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +1 -0
  79. package/dist/voice/useVoiceConnectOrchestration.d.ts +21 -0
  80. package/dist/voice/useVoiceConnectOrchestration.d.ts.map +1 -0
  81. package/dist/voice/useVoiceConnectOrchestration.js +86 -0
  82. package/dist/voice/useVoiceMicState.d.ts +15 -0
  83. package/dist/voice/useVoiceMicState.d.ts.map +1 -0
  84. package/dist/voice/useVoiceMicState.js +94 -0
  85. package/dist/voice/useVoiceSilenceCommit.d.ts +10 -0
  86. package/dist/voice/useVoiceSilenceCommit.d.ts.map +1 -0
  87. package/dist/voice/useVoiceSilenceCommit.js +67 -0
  88. package/dist/voice/useVoiceTranscriptMessages.d.ts +16 -0
  89. package/dist/voice/useVoiceTranscriptMessages.d.ts.map +1 -0
  90. package/dist/voice/useVoiceTranscriptMessages.js +129 -0
  91. package/dist/voice/useWsRealtimeAudio.d.ts +18 -0
  92. package/dist/voice/useWsRealtimeAudio.d.ts.map +1 -0
  93. package/dist/voice/useWsRealtimeAudio.js +102 -0
  94. package/dist/voice/voiceMicConstants.d.ts +4 -0
  95. package/dist/voice/voiceMicConstants.d.ts.map +1 -0
  96. package/dist/voice/voiceMicConstants.js +10 -0
  97. package/dist/voice/ws/BrowserWsPcmPlayer.d.ts +23 -0
  98. package/dist/voice/ws/BrowserWsPcmPlayer.d.ts.map +1 -0
  99. package/dist/voice/ws/BrowserWsPcmPlayer.js +137 -0
  100. package/dist/voice/ws/BrowserWsPcmRecorder.d.ts +17 -0
  101. package/dist/voice/ws/BrowserWsPcmRecorder.d.ts.map +1 -0
  102. package/dist/voice/ws/BrowserWsPcmRecorder.js +71 -0
  103. package/dist/voice/ws/float32ToPcm16.d.ts +2 -0
  104. package/dist/voice/ws/float32ToPcm16.d.ts.map +1 -0
  105. package/dist/voice/ws/float32ToPcm16.js +8 -0
  106. package/dist/voice/ws/voiceSilenceConstants.d.ts +5 -0
  107. package/dist/voice/ws/voiceSilenceConstants.d.ts.map +1 -0
  108. package/dist/voice/ws/voiceSilenceConstants.js +4 -0
  109. package/dist/voice/ws/wsRealtimeConstants.d.ts +2 -0
  110. package/dist/voice/ws/wsRealtimeConstants.d.ts.map +1 -0
  111. package/dist/voice/ws/wsRealtimeConstants.js +1 -0
  112. package/package.json +60 -0
  113. package/src/NxtlinqAgentChat.tsx +79 -0
  114. package/src/components/AgentAssistantShell.tsx +104 -0
  115. package/src/components/AgentComposer.tsx +134 -0
  116. package/src/components/AgentMessageList.tsx +78 -0
  117. package/src/components/AgentRemoteAudio.tsx +34 -0
  118. package/src/components/AgentVoiceBar.tsx +173 -0
  119. package/src/components/PresetMessageChips.tsx +41 -0
  120. package/src/context/AgentAssistantContext.tsx +276 -0
  121. package/src/index.ts +78 -0
  122. package/src/legacy/assets/images/adiSideItalicDataUri.ts +1 -0
  123. package/src/legacy/chatbot/ChatBot.tsx +61 -0
  124. package/src/legacy/chatbot/context/ChatBotContext.tsx +3227 -0
  125. package/src/legacy/chatbot/types/ChatBotTypes.ts +195 -0
  126. package/src/legacy/chatbot/ui/BerifyMeModal.tsx +145 -0
  127. package/src/legacy/chatbot/ui/ChatBotUI.tsx +949 -0
  128. package/src/legacy/chatbot/ui/MessageInput.tsx +517 -0
  129. package/src/legacy/chatbot/ui/MessageList.tsx +764 -0
  130. package/src/legacy/chatbot/ui/ModelSelector.tsx +190 -0
  131. package/src/legacy/chatbot/ui/NotificationModal.tsx +110 -0
  132. package/src/legacy/chatbot/ui/PermissionForm.tsx +632 -0
  133. package/src/legacy/chatbot/ui/PresetMessages.tsx +50 -0
  134. package/src/legacy/chatbot/ui/VoiceModePanel.tsx +168 -0
  135. package/src/legacy/chatbot/ui/styles/isolatedStyles.ts +1058 -0
  136. package/src/legacy/index.ts +26 -0
  137. package/src/theme/defaultTheme.ts +22 -0
  138. package/src/types.ts +65 -0
  139. package/src/voice/useVoiceConnectOrchestration.ts +117 -0
  140. package/src/voice/useVoiceMicState.ts +117 -0
  141. package/src/voice/useVoiceTranscriptMessages.ts +173 -0
  142. package/src/voice/voiceMicConstants.ts +13 -0
@@ -0,0 +1,764 @@
1
+ /** @jsxImportSource @emotion/react */
2
+ import * as React from 'react';
3
+ import { css } from '@emotion/react';
4
+ import { convertUrlsToLinks, walletTextUtils } from '@bytexbyte/nxtlinq-ai-agent-web-development';
5
+ import type { Message } from '@bytexbyte/nxtlinq-ai-agent-core-development';
6
+ import { useChatBot } from '../context/ChatBotContext';
7
+ import AttachFileIcon from '@mui/icons-material/AttachFile';
8
+ import {
9
+ messageListContainer,
10
+ messageBubble,
11
+ userMessage,
12
+ messageContent,
13
+ userMessageContent,
14
+ retryMessageContent,
15
+ chatbotButton,
16
+ connectedButton,
17
+ loadingIndicator,
18
+ modelIndicator,
19
+ modelBadge,
20
+ modelDot,
21
+ streamingCaret,
22
+ streamingContainer,
23
+ streamingHeader,
24
+ streamingIcon,
25
+ streamingToolName,
26
+ streamingProgressPercent,
27
+ streamingProgressContainer,
28
+ streamingProgressBar,
29
+ streamingPartialText,
30
+ streamingStatus,
31
+ streamingStepsContainer,
32
+ streamingStepItem,
33
+ streamingStepCheck,
34
+ piiTokenReveal,
35
+ } from './styles/isolatedStyles';
36
+
37
+ const piiTokenStatic: React.CSSProperties = {
38
+ fontFamily: 'monospace',
39
+ fontWeight: 400,
40
+ display: 'inline',
41
+ };
42
+
43
+ /**
44
+ * Highlight PII tokens in anonymized content.
45
+ * When animate=true (live transition), each token gets a staggered glow animation.
46
+ * When animate=false (loaded from history), tokens are styled statically.
47
+ */
48
+ function highlightPiiTokens(text: string, animate: boolean = false): React.ReactNode {
49
+ const parts = text.split(/(<?[A-Z][A-Z_]*_\d+>?)/g);
50
+ if (parts.length === 1) return text;
51
+ let tokenIndex = 0;
52
+ return React.createElement(React.Fragment, null,
53
+ ...parts.map((part, i) => {
54
+ if (/^<?[A-Z][A-Z_]*_\d+>?$/.test(part)) {
55
+ const idx = tokenIndex++;
56
+ if (animate) {
57
+ return React.createElement('span', {
58
+ key: i,
59
+ css: piiTokenReveal,
60
+ style: { animationDelay: `${idx * 0.25 + 0.6}s` },
61
+ }, part);
62
+ }
63
+ return React.createElement('span', { key: i, style: piiTokenStatic }, part);
64
+ }
65
+ return part;
66
+ })
67
+ );
68
+ }
69
+
70
+ // ===== PII Bubble Header =====
71
+
72
+ /** Word-boundary match — prevents PERSON_1 from matching inside PERSON_11 */
73
+ function tokenInText(text: string, token: string): boolean {
74
+ return new RegExp(`\\b${token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(text);
75
+ }
76
+
77
+ const ShieldIcon: React.FC = () => (
78
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
79
+ <path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-2 16l-4-4 1.41-1.41L10 14.17l6.59-6.59L18 9l-8 8z"/>
80
+ </svg>
81
+ );
82
+
83
+ // --- Styles: all PII header styles co-located for maintainability ---
84
+
85
+ /** Scan line overlay on the blue bubble during scanning */
86
+ const piiScanOverlay = css`
87
+ overflow: hidden !important;
88
+ &::before {
89
+ content: '' !important;
90
+ position: absolute !important;
91
+ top: 0 !important;
92
+ left: 0 !important;
93
+ width: 100% !important;
94
+ height: 100% !important;
95
+ border-radius: inherit !important;
96
+ pointer-events: none !important;
97
+ z-index: 1 !important;
98
+ background: linear-gradient(
99
+ 90deg,
100
+ transparent 0%,
101
+ rgba(255,255,255,0.06) 30%,
102
+ rgba(255,255,255,0.18) 50%,
103
+ rgba(255,255,255,0.06) 70%,
104
+ transparent 100%
105
+ ) !important;
106
+ @keyframes piiScanLine {
107
+ 0% { transform: translateX(-100%); }
108
+ 100% { transform: translateX(100%); }
109
+ }
110
+ animation: piiScanLine 1.8s ease-in-out infinite !important;
111
+ }
112
+ `;
113
+
114
+ /** Content fade-in when text swaps from original → redacted */
115
+ const piiContentFadeIn = css`
116
+ @keyframes piiFadeIn {
117
+ 0% { opacity: 0.2; }
118
+ 100% { opacity: 1; }
119
+ }
120
+ animation: piiFadeIn 0.6s ease-out !important;
121
+ `;
122
+
123
+ const piiHeaderRow = css`
124
+ display: flex !important;
125
+ align-items: center !important;
126
+ gap: 5px !important;
127
+ font-size: 11px !important;
128
+ font-weight: 500 !important;
129
+ font-family: inherit !important;
130
+ margin-bottom: 6px !important;
131
+ padding-bottom: 5px !important;
132
+ border-bottom: 1px solid rgba(255,255,255,0.15) !important;
133
+ color: rgba(255,255,255,0.85) !important;
134
+ `;
135
+
136
+ const piiScanLabel = css`
137
+ @keyframes piiPulse {
138
+ 0%, 100% { opacity: 1; }
139
+ 50% { opacity: 0.5; }
140
+ }
141
+ display: inline-flex !important;
142
+ align-items: baseline !important;
143
+ animation: piiPulse 1.2s ease-in-out infinite !important;
144
+ `;
145
+
146
+ const piiBounceDot0 = css`
147
+ @keyframes piiBounce {
148
+ 0%, 100% { transform: translateY(0); }
149
+ 50% { transform: translateY(-3px); }
150
+ }
151
+ display: inline-block !important;
152
+ margin-left: 2px !important;
153
+ animation: piiBounce 0.6s ease-in-out infinite !important;
154
+ animation-delay: 0ms !important;
155
+ `;
156
+ const piiBounceDot1 = css`
157
+ display: inline-block !important;
158
+ animation: piiBounce 0.6s ease-in-out infinite !important;
159
+ animation-delay: 150ms !important;
160
+ `;
161
+ const piiBounceDot2 = css`
162
+ display: inline-block !important;
163
+ animation: piiBounce 0.6s ease-in-out infinite !important;
164
+ animation-delay: 300ms !important;
165
+ `;
166
+
167
+ /** Below-bubble green badge: "✓ {N} items protected ▸" */
168
+ const piiBelowBubbleBtn = css`
169
+ all: unset !important;
170
+ display: inline-flex !important;
171
+ align-items: center !important;
172
+ gap: 4px !important;
173
+ font-size: 11px !important;
174
+ font-weight: 600 !important;
175
+ font-family: inherit !important;
176
+ color: #2e7d32 !important;
177
+ background: #e8f5e9 !important;
178
+ border-radius: 12px !important;
179
+ padding: 3px 10px !important;
180
+ margin-top: 6px !important;
181
+ cursor: pointer !important;
182
+ &:hover { background: #c8e6c9 !important; }
183
+ `;
184
+
185
+ const piiNoPiiStyle = css`
186
+ @keyframes piiFadeOut {
187
+ 0% { opacity: 1; }
188
+ 70% { opacity: 1; }
189
+ 100% { opacity: 0; }
190
+ }
191
+ animation: piiFadeOut 2s ease-out forwards !important;
192
+ `;
193
+
194
+ /** Entity mapping panel — below bubble, original card style */
195
+ const piiEntityPanel = css`
196
+ margin-top: 6px !important;
197
+ padding: 8px 12px !important;
198
+ background-color: #fafafa !important;
199
+ border: 1px solid #e0e0e0 !important;
200
+ border-radius: 8px !important;
201
+ font-family: inherit !important;
202
+ `;
203
+
204
+ const piiEntityRowStyle = css`
205
+ display: flex !important;
206
+ align-items: center !important;
207
+ gap: 6px !important;
208
+ font-size: 11px !important;
209
+ line-height: 1.6 !important;
210
+ `;
211
+
212
+ const piiOriginalStyle = css`
213
+ color: #d32f2f !important;
214
+ text-decoration: line-through !important;
215
+ font-family: inherit !important;
216
+ `;
217
+
218
+ const piiArrowStyle = css`
219
+ color: #bdbdbd !important;
220
+ font-size: 10px !important;
221
+ `;
222
+
223
+ const piiTokenMappingStyle = css`
224
+ color: #1565c0 !important;
225
+ font-family: monospace !important;
226
+ font-weight: 600 !important;
227
+ `;
228
+
229
+ // --- Component ---
230
+
231
+ type PiiPhase = 'idle' | 'scanning' | 'done' | 'no-pii' | 'faded';
232
+
233
+ /** Green checkmark icon for below-bubble badge */
234
+ const GreenCheckIcon: React.FC = () => (
235
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="#2e7d32" xmlns="http://www.w3.org/2000/svg">
236
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
237
+ </svg>
238
+ );
239
+
240
+ /** Status-only header inside the bubble */
241
+ const PiiStatusHeader: React.FC<{
242
+ message: Message;
243
+ }> = React.memo(({ message }) => {
244
+ const getInitialPhase = (): PiiPhase => {
245
+ if (message.piiStatus === 'complete' && message.piiProtection?.anonymizedContent) return 'done';
246
+ if (message.piiStatus === 'none') return 'faded';
247
+ if (message.piiStatus === 'scanning') return 'scanning';
248
+ return 'idle';
249
+ };
250
+
251
+ const [phase, setPhase] = React.useState<PiiPhase>(getInitialPhase);
252
+ const prevStatusRef = React.useRef(message.piiStatus);
253
+ const timerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
254
+
255
+ React.useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
256
+
257
+ React.useEffect(() => {
258
+ const prev = prevStatusRef.current;
259
+ const curr = message.piiStatus;
260
+ prevStatusRef.current = curr;
261
+ if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; }
262
+
263
+ if (curr === 'scanning' && prev !== 'scanning') {
264
+ setPhase('scanning');
265
+ } else if (curr === 'complete') {
266
+ setPhase(message.piiProtection?.anonymizedContent ? 'done' : 'faded');
267
+ } else if (curr === 'none') {
268
+ if (prev === 'scanning') {
269
+ setPhase('no-pii');
270
+ timerRef.current = setTimeout(() => setPhase('faded'), 2000);
271
+ } else {
272
+ setPhase('faded');
273
+ }
274
+ }
275
+ }, [message.piiStatus, message.piiProtection?.anonymizedContent]);
276
+
277
+ if (phase === 'idle' || phase === 'faded') return null;
278
+
279
+ if (phase === 'scanning') {
280
+ const step = message.piiStep ?? 0;
281
+ const label = step >= 1 ? 'Sending' : 'Scanning';
282
+ return (
283
+ <div css={piiHeaderRow}>
284
+ <ShieldIcon />
285
+ <span css={piiScanLabel}>
286
+ {label}
287
+ <span css={piiBounceDot0}>.</span>
288
+ <span css={piiBounceDot1}>.</span>
289
+ <span css={piiBounceDot2}>.</span>
290
+ </span>
291
+ </div>
292
+ );
293
+ }
294
+
295
+ if (phase === 'no-pii') {
296
+ return (
297
+ <div css={[piiHeaderRow, piiNoPiiStyle]}>
298
+ <ShieldIcon />
299
+ <span>No sensitive data found</span>
300
+ </div>
301
+ );
302
+ }
303
+
304
+ // Done: simple status label
305
+ return (
306
+ <div css={piiHeaderRow}>
307
+ <ShieldIcon />
308
+ <span>✓ Protected</span>
309
+ </div>
310
+ );
311
+ });
312
+
313
+ export const MessageList: React.FC = () => {
314
+ const {
315
+ messages,
316
+ isLoading,
317
+ isTtsProcessing,
318
+ requiresGesture,
319
+ retryTtsWithGesture,
320
+ connectWallet,
321
+ signInWallet,
322
+ hitAddress,
323
+ isAutoConnecting,
324
+ isNeedSignInWithWallet,
325
+ enableAIT,
326
+ isAITLoading,
327
+ isAITEnabling,
328
+ sendMessage,
329
+ permissions,
330
+ availableModels,
331
+ serviceId,
332
+ piiDisplayMode,
333
+ } = useChatBot();
334
+ const messagesEndRef = React.useRef<HTMLDivElement>(null);
335
+
336
+ const scrollToBottom = () => {
337
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
338
+ };
339
+
340
+ React.useEffect(() => {
341
+ scrollToBottom();
342
+ }, [messages]);
343
+
344
+ // Track which message's PII entity panel is expanded (only one at a time)
345
+ const [expandedPiiMsgId, setExpandedPiiMsgId] = React.useState<string | null>(null);
346
+
347
+ const handleButtonClick = async (buttonType: string, message: Message) => {
348
+ // Prevent sending messages while AI Agent is processing
349
+ if (isLoading) {
350
+ return;
351
+ }
352
+
353
+ if (buttonType === 'connectWallet') {
354
+ connectWallet(true);
355
+ } else if (buttonType === 'signIn') {
356
+ signInWallet(true);
357
+ } else if (buttonType === 'enableAIT') {
358
+ const requiredPermission = message.metadata?.requiredPermission;
359
+ if (requiredPermission) {
360
+ const success = await enableAIT(requiredPermission);
361
+ if (success) {
362
+ // Find the last user message
363
+ const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
364
+ if (lastUserMsg && lastUserMsg.content) {
365
+ // Re-execute the previous user message (retryCount=1 to skip duplicate PII log)
366
+ await sendMessage(lastUserMsg.content, 1);
367
+ }
368
+ }
369
+ }
370
+ } else if (buttonType === 'continue') {
371
+ const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
372
+ if (lastUserMsg && lastUserMsg.content) {
373
+ // Re-execute the previous user message (retryCount=1 to skip duplicate PII log)
374
+ await sendMessage(lastUserMsg.content, 1);
375
+ }
376
+ }
377
+ };
378
+
379
+ // Get model display name from API-provided model list
380
+ const getModelDisplayName = (modelValue?: string) => {
381
+ if (!modelValue) return '';
382
+ // Special case: show "Adi" for specific serviceId
383
+ const targetServiceId = 'e48fc2b9-a7d1-49e3-85cb-9d621a0bf774';
384
+ if (serviceId && serviceId.trim() === targetServiceId) {
385
+ return 'Adi';
386
+ }
387
+ // Find model in the list returned by API
388
+ const model = availableModels.find(m => m.value === modelValue);
389
+ // Use label from API or fallback to value
390
+ return model?.label || modelValue;
391
+ };
392
+
393
+ return (
394
+ <div css={messageListContainer}>
395
+ {messages.map((message) => {
396
+ // Determine user message content based on piiStep
397
+ // Show redacted as soon as piiStep >= 1 ({N} protected) — data available
398
+ const showRedacted = message.role === 'user'
399
+ && piiDisplayMode === 'redacted'
400
+ && message.piiProtection?.anonymizedContent
401
+ && (message.piiStep !== undefined && message.piiStep >= 1);
402
+
403
+ // Is the bubble currently in scanning state? (for scan overlay animation)
404
+ const isUserScanning = message.role === 'user'
405
+ && piiDisplayMode === 'redacted'
406
+ && message.piiStatus === 'scanning';
407
+ const showPiiHeader = message.role === 'user'
408
+ && piiDisplayMode === 'redacted'
409
+ && !!message.piiStatus;
410
+ const isPiiExpanded = expandedPiiMsgId === message.id;
411
+
412
+ // Compute relevant entity mapping for this message
413
+ let relevantMapping: Record<string, string> = {};
414
+ if (showPiiHeader && message.piiProtection?.mapping && message.piiProtection?.anonymizedContent) {
415
+ const content = message.piiProtection.anonymizedContent;
416
+ for (const [original, token] of Object.entries(message.piiProtection.mapping)) {
417
+ if (tokenInText(content, token)) {
418
+ relevantMapping[original] = token;
419
+ }
420
+ }
421
+ }
422
+ const entityCount = Object.keys(relevantMapping).length;
423
+
424
+ const isAgentLikeStream =
425
+ message.metadata?.agentLlmStream
426
+ || message.metadata?.voiceRealtime
427
+ || message.metadata?.model?.endsWith('-stream');
428
+
429
+ const isAgentStreamAwaitingFirstToken =
430
+ message.role === 'assistant'
431
+ && message.isStreaming
432
+ && (message.metadata?.agentLlmStream || message.metadata?.model?.endsWith('-stream'))
433
+ && !(message.partialContent ?? '').length;
434
+
435
+ if (isAgentStreamAwaitingFirstToken) {
436
+ return null;
437
+ }
438
+
439
+ return (
440
+ <div
441
+ key={message.id}
442
+ css={message.role === 'user' ? userMessage : messageBubble}
443
+ >
444
+ <div
445
+ css={[
446
+ message.role === 'user'
447
+ ? userMessageContent
448
+ : message.metadata?.isRetry
449
+ ? retryMessageContent
450
+ : messageContent,
451
+ isUserScanning && piiScanOverlay,
452
+ ]}
453
+ >
454
+ {/* PII Status Header — inside bubble, status only */}
455
+ {showPiiHeader && (
456
+ <PiiStatusHeader message={message} />
457
+ )}
458
+
459
+ {message.metadata?.isRetry && (
460
+ <span css={css`margin-right: 8px !important; font-size: 14px !important;`}>🔄</span>
461
+ )}
462
+
463
+ {/* Display attachments */}
464
+ {message.attachments && message.attachments.length > 0 && (
465
+ <div
466
+ css={css`
467
+ display: flex !important;
468
+ flex-wrap: wrap !important;
469
+ gap: 8px !important;
470
+ margin-bottom: ${message.content ? '10px' : '0'} !important;
471
+ `}
472
+ >
473
+ {message.attachments.map((attachment, idx) => (
474
+ <div
475
+ key={idx}
476
+ css={css`
477
+ position: relative !important;
478
+ display: inline-block !important;
479
+ border: 1px solid #ddd !important;
480
+ border-radius: 8px !important;
481
+ overflow: hidden !important;
482
+ background: #f5f5f5 !important;
483
+ max-width: 200px !important;
484
+ `}
485
+ >
486
+ {attachment.type === 'image' && (
487
+ <img
488
+ src={attachment.url}
489
+ alt={attachment.name}
490
+ css={css`
491
+ max-width: 200px !important;
492
+ max-height: 200px !important;
493
+ object-fit: cover !important;
494
+ display: block !important;
495
+ cursor: pointer !important;
496
+ `}
497
+ onClick={() => {
498
+ // Open image in new window on click
499
+ const newWindow = window.open();
500
+ if (newWindow) {
501
+ newWindow.document.write(`<img src="${attachment.url}" style="max-width: 100%; height: auto;" />`);
502
+ }
503
+ }}
504
+ />
505
+ )}
506
+ {attachment.type === 'file' && (
507
+ <div
508
+ css={css`
509
+ padding: 20px 15px !important;
510
+ text-align: center !important;
511
+ min-width: 150px !important;
512
+ display: flex !important;
513
+ align-items: center !important;
514
+ justify-content: center !important;
515
+ flex-direction: column !important;
516
+ gap: 8px !important;
517
+ `}
518
+ >
519
+ <AttachFileIcon css={css`font-size: 32px !important; color: #666 !important;`} />
520
+ <span
521
+ css={css`
522
+ font-size: 12px !important;
523
+ color: #666 !important;
524
+ word-break: break-word !important;
525
+ max-width: 120px !important;
526
+ `}
527
+ >
528
+ {attachment.name}
529
+ </span>
530
+ {attachment.size && (
531
+ <span
532
+ css={css`
533
+ font-size: 10px !important;
534
+ color: #999 !important;
535
+ `}
536
+ >
537
+ {(attachment.size / 1024).toFixed(1)} KB
538
+ </span>
539
+ )}
540
+ </div>
541
+ )}
542
+ </div>
543
+ ))}
544
+ </div>
545
+ )}
546
+
547
+ {/* Voice realtime: user + assistant stream in history bubbles */}
548
+ {message.isStreaming && message.metadata?.voiceRealtime
549
+ && (message.partialContent ?? '').length > 0 && (
550
+ <div css={streamingPartialText}>
551
+ {message.role === 'assistant'
552
+ ? convertUrlsToLinks(message.partialContent ?? '')
553
+ : message.partialContent}
554
+ <span css={streamingCaret}>▊</span>
555
+ </div>
556
+ )}
557
+
558
+ {/* Agent SSE (text chat): stream after first token */}
559
+ {message.isStreaming && message.role === 'assistant'
560
+ && (message.metadata?.agentLlmStream || message.metadata?.model?.endsWith('-stream'))
561
+ && !message.metadata?.voiceRealtime
562
+ && (message.partialContent ?? '').length > 0 && (
563
+ <div css={streamingPartialText}>
564
+ {convertUrlsToLinks(message.partialContent ?? '')}
565
+ <span css={streamingCaret}>▊</span>
566
+ </div>
567
+ )}
568
+
569
+ {/* Mode 1: Token streaming (e.g. LangGraph) */}
570
+ {message.isStreaming && message.partialContent
571
+ && !message.metadata?.voiceRealtime
572
+ && !(message.metadata?.agentLlmStream || message.metadata?.model?.endsWith('-stream')) && (
573
+ <div css={streamingPartialText}>
574
+ {message.role === 'assistant' ? convertUrlsToLinks(message.partialContent) : message.partialContent}
575
+ <span css={streamingCaret}>▊</span>
576
+ </div>
577
+ )}
578
+
579
+ {/* Mode 2: tool/progress streaming — never for Agent SSE (`agentLlmStream` or *-stream model id) */}
580
+ {message.isStreaming && !message.partialContent && message.role === 'assistant'
581
+ && !isAgentLikeStream && (
582
+ <div css={streamingContainer}>
583
+ <div css={streamingHeader}>
584
+ <span css={streamingIcon}>🔧</span>
585
+ <span css={streamingToolName}>
586
+ {message.streamingToolName || 'Processing'}
587
+ </span>
588
+ {message.streamingProgress !== undefined && (
589
+ <span css={streamingProgressPercent}>
590
+ {message.streamingProgress}%
591
+ </span>
592
+ )}
593
+ </div>
594
+
595
+ {/* Progress bar */}
596
+ {message.streamingProgress !== undefined && (
597
+ <div css={streamingProgressContainer}>
598
+ <div css={[streamingProgressBar, css`width: ${message.streamingProgress}% !important;`]} />
599
+ </div>
600
+ )}
601
+
602
+ {/* Status message */}
603
+ {message.streamingStatus && (
604
+ <div css={streamingStatus}>
605
+ {message.streamingStatus}
606
+ </div>
607
+ )}
608
+
609
+ {/* Steps list */}
610
+ {message.streamingSteps && message.streamingSteps.length > 0 && (
611
+ <div css={streamingStepsContainer}>
612
+ {message.streamingSteps.map((step, idx) => (
613
+ <div key={idx} css={streamingStepItem}>
614
+ <span css={streamingStepCheck}>✓</span>
615
+ <span>{step}</span>
616
+ </div>
617
+ ))}
618
+ </div>
619
+ )}
620
+ </div>
621
+ )}
622
+
623
+ {/* Final content - display final message content */}
624
+ {!message.isStreaming && (() => {
625
+ // User message with redacted PII available — show tokens with fade-in
626
+ if (showRedacted) {
627
+ const isLive = message.piiStatus === 'scanning';
628
+ const content = highlightPiiTokens(message.piiProtection!.anonymizedContent!, isLive);
629
+ return isLive
630
+ ? <span css={piiContentFadeIn}>{content}</span>
631
+ : content;
632
+ }
633
+
634
+ // User message during scanning (step 0) — show original text (scan overlay handles animation)
635
+ if (isUserScanning && !showRedacted) {
636
+ return message.content;
637
+ }
638
+
639
+ // Assistant message with redacted PII
640
+ if (message.role === 'assistant'
641
+ && piiDisplayMode === 'redacted'
642
+ && message.piiProtection?.anonymizedContent) {
643
+ return highlightPiiTokens(message.piiProtection.anonymizedContent, false);
644
+ }
645
+
646
+ // Default
647
+ return message.role === 'assistant'
648
+ ? convertUrlsToLinks(message.content)
649
+ : message.content;
650
+ })()}
651
+ {message.button && (
652
+ <div css={css`margin-top: 10px !important;`}>
653
+ <button
654
+ onClick={() => {
655
+ if (message.button && message.button.trim()) {
656
+ handleButtonClick(message.button, message);
657
+ }
658
+ }}
659
+ disabled={isAutoConnecting ||
660
+ (message.button === 'connectWallet' && Boolean(hitAddress)) ||
661
+ (message.button === 'signIn' && !isNeedSignInWithWallet) ||
662
+ (message.button === 'enableAIT' && (isAITLoading || isAITEnabling ||
663
+ (message.metadata?.requiredPermission && permissions.includes(message.metadata.requiredPermission)))) || false}
664
+ css={
665
+ (message.button === 'connectWallet' && Boolean(hitAddress)) ? connectedButton :
666
+ (message.button === 'signIn' && !isNeedSignInWithWallet) ? connectedButton :
667
+ (message.button === 'continue') ? connectedButton :
668
+ (message.button === 'enableAIT' && (message.metadata?.requiredPermission && permissions.includes(message.metadata.requiredPermission))) ? connectedButton :
669
+ chatbotButton
670
+ }
671
+ >
672
+ {isAutoConnecting ? 'Connecting...' :
673
+ message.button === 'connectWallet' ? (Boolean(hitAddress) ? 'Connected' : walletTextUtils.getWalletText('Connect Wallet', serviceId)) :
674
+ message.button === 'signIn' ? (!isNeedSignInWithWallet ? 'Signed In' : 'Sign In') :
675
+ message.button === 'continue' ? 'Continue' :
676
+ message.button === 'enableAIT' ?
677
+ ((isAITLoading || isAITEnabling) ? 'Enabling...' :
678
+ (message.metadata?.requiredPermission && permissions.includes(message.metadata.requiredPermission)) ? 'AIT Enabled' : 'Enable AIT Permissions') :
679
+ message.button}
680
+ </button>
681
+ </div>
682
+ )}
683
+ </div>
684
+
685
+ {/* PII Label + Entity Mapping Panel — below bubble */}
686
+ {showPiiHeader && message.piiStatus === 'complete' && entityCount > 0 && (
687
+ <div css={css`text-align: right !important;`}>
688
+ <button css={piiBelowBubbleBtn} onClick={() => setExpandedPiiMsgId(v => v === message.id ? null : message.id)} type="button">
689
+ <GreenCheckIcon />
690
+ {entityCount} item{entityCount !== 1 ? 's' : ''} protected {isPiiExpanded ? '▴' : '▸'}
691
+ </button>
692
+ {isPiiExpanded && (
693
+ <div css={piiEntityPanel}>
694
+ {Object.entries(relevantMapping).map(([original, token]) => (
695
+ <div key={original} css={piiEntityRowStyle}>
696
+ <span css={piiOriginalStyle}>{original}</span>
697
+ <span css={piiArrowStyle}>→</span>
698
+ <span css={piiTokenMappingStyle}>{token}</span>
699
+ </div>
700
+ ))}
701
+ </div>
702
+ )}
703
+ </div>
704
+ )}
705
+
706
+ {message.role === 'assistant' && message.metadata?.model && (
707
+ <div css={css`
708
+ ${modelIndicator}
709
+ gap: 8px !important;
710
+ `}>
711
+ <div css={modelBadge}>
712
+ <span css={modelDot}></span>
713
+ {getModelDisplayName(message.metadata.model)}
714
+ </div>
715
+ {(isTtsProcessing || requiresGesture) && !isLoading && message.id === messages[messages.length - 1]?.id && (
716
+ <div css={css`
717
+ display: flex !important;
718
+ align-items: center !important;
719
+ font-size: 12px !important;
720
+ color: #666 !important;
721
+ gap: 4px !important;
722
+ `}>
723
+ <span role="img" aria-hidden="true">🔊</span>
724
+ <span>
725
+ {requiresGesture ? (
726
+ <button
727
+ onClick={() => retryTtsWithGesture && retryTtsWithGesture()}
728
+ css={css`
729
+ background: none !important;
730
+ border: none !important;
731
+ color: #1976d2 !important;
732
+ padding: 0 !important;
733
+ text-decoration: underline !important;
734
+ cursor: pointer !important;
735
+ `}
736
+ >Tap to play voice</button>
737
+ ) : 'Preparing voice reply...'}
738
+ </span>
739
+ </div>
740
+ )}
741
+ </div>
742
+ )}
743
+ </div>
744
+ );
745
+ })}
746
+ {isLoading && (() => {
747
+ // During PII pipeline (step 0-1), hide "Thinking..." — message hasn't been sent to AI yet.
748
+ // Only show once Send step (2) is reached, meaning the anonymized message was sent to AI.
749
+ if (piiDisplayMode === 'redacted') {
750
+ const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
751
+ if (lastUserMsg?.piiStatus === 'scanning' && (lastUserMsg.piiStep === undefined || lastUserMsg.piiStep < 2)) {
752
+ return null;
753
+ }
754
+ }
755
+ return (
756
+ <div css={loadingIndicator}>
757
+ Thinking...
758
+ </div>
759
+ );
760
+ })()}
761
+ <div ref={messagesEndRef} />
762
+ </div>
763
+ );
764
+ };