@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,517 @@
1
+ /** @jsxImportSource @emotion/react */
2
+ import { css } from '@emotion/react';
3
+ import MicIcon from '@mui/icons-material/Mic';
4
+ import MicOffIcon from '@mui/icons-material/MicOff';
5
+ import SendIcon from '@mui/icons-material/Send';
6
+ import VolumeUpIcon from '@mui/icons-material/VolumeUp';
7
+ import VolumeOffIcon from '@mui/icons-material/VolumeOff';
8
+ import AttachFileIcon from '@mui/icons-material/AttachFile';
9
+ import CloseIcon from '@mui/icons-material/Close';
10
+ import { IconButton, InputBase, Tooltip } from '@mui/material';
11
+ import * as React from 'react';
12
+ import { useChatBot } from '../context/ChatBotContext';
13
+ import { walletTextUtils } from '@bytexbyte/nxtlinq-ai-agent-web-development';
14
+ import type { Attachment } from '@bytexbyte/nxtlinq-ai-agent-core-development';
15
+ import { actionButton } from './styles/isolatedStyles';
16
+
17
+ export const MessageInput: React.FC = () => {
18
+ const {
19
+ inputValue,
20
+ setInputValue,
21
+ isLoading,
22
+ isAITLoading,
23
+ handleSubmit,
24
+ uploadAttachment,
25
+ showError,
26
+ isMicEnabled,
27
+ isAwaitingMicGesture,
28
+ startRecording,
29
+ stopRecording,
30
+ textInputRef,
31
+ autoSendEnabled,
32
+ setAutoSendEnabled,
33
+ textToSpeechEnabled,
34
+ setTextToSpeechEnabled,
35
+ serviceId,
36
+ getCurrentModel,
37
+ props: { placeholder = 'Type a message...' }
38
+ } = useChatBot();
39
+
40
+ type Pending = { attachment: Attachment; file: File };
41
+ const [pendingAttachments, setPendingAttachments] = React.useState<Pending[]>([]);
42
+ const [isUploading, setIsUploading] = React.useState(false);
43
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
44
+
45
+ // Check if current model is llama (upload not supported)
46
+ const currentModel = getCurrentModel();
47
+ const isLlamaModel = currentModel.value === 'llama' || currentModel.value.includes('llama');
48
+
49
+ // Clear pending attachments when switching to llama model
50
+ React.useEffect(() => {
51
+ if (isLlamaModel && pendingAttachments.length > 0) {
52
+ setPendingAttachments([]);
53
+ // Also clear the file input
54
+ if (fileInputRef.current) {
55
+ fileInputRef.current.value = '';
56
+ }
57
+ }
58
+ }, [isLlamaModel, pendingAttachments.length]);
59
+
60
+ const isDisabled = isLoading || isAITLoading || isUploading;
61
+ const inputPlaceholder = isAITLoading ? walletTextUtils.getWalletText('Loading wallet configuration...', serviceId) : placeholder;
62
+
63
+ const hasContent = React.useCallback(
64
+ () =>
65
+ (textInputRef.current?.value ?? inputValue).trim().length > 0 || pendingAttachments.length > 0,
66
+ [inputValue, pendingAttachments.length]
67
+ );
68
+
69
+ const doSubmit = React.useCallback(
70
+ async (e: React.FormEvent) => {
71
+ if (!hasContent() || isDisabled) return;
72
+ let attachmentsToSend: Attachment[] = [];
73
+
74
+ if (pendingAttachments.length > 0) {
75
+ setIsUploading(true);
76
+ const uploadErrors: string[] = [];
77
+ try {
78
+ for (const { attachment, file } of pendingAttachments) {
79
+ try {
80
+ const res = await uploadAttachment(file);
81
+ if ('error' in res) {
82
+ uploadErrors.push(`${attachment.name}: ${res.error || 'Upload failed'}`);
83
+ continue; // Continue with other files instead of returning
84
+ }
85
+ attachmentsToSend.push({
86
+ type: attachment.type,
87
+ url: res.url,
88
+ name: attachment.name,
89
+ mimeType: attachment.mimeType,
90
+ size: attachment.size,
91
+ });
92
+ } catch (err) {
93
+ const errorMessage = err instanceof Error ? err.message : 'Upload failed';
94
+ uploadErrors.push(`${attachment.name}: ${errorMessage}`);
95
+ console.error(`Failed to upload ${attachment.name}:`, err);
96
+ }
97
+ }
98
+
99
+ // Show errors if any files failed, but continue if some succeeded
100
+ if (uploadErrors.length > 0) {
101
+ if (attachmentsToSend.length === 0) {
102
+ // All files failed
103
+ showError(`All file uploads failed:\n${uploadErrors.join('\n')}`);
104
+ return;
105
+ } else {
106
+ // Some files succeeded, some failed
107
+ showError(`Some files failed to upload:\n${uploadErrors.join('\n')}\n\n${attachmentsToSend.length} file(s) uploaded successfully; continuing to send.`);
108
+ }
109
+ }
110
+ } finally {
111
+ setIsUploading(false);
112
+ }
113
+ }
114
+
115
+ setPendingAttachments([]);
116
+ await handleSubmit(e, attachmentsToSend);
117
+ },
118
+ [
119
+ hasContent,
120
+ pendingAttachments,
121
+ isDisabled,
122
+ uploadAttachment,
123
+ showError,
124
+ handleSubmit,
125
+ ]
126
+ );
127
+
128
+ const handleKeyPress = (e: React.KeyboardEvent) => {
129
+ if (e.key === 'Enter' && !e.shiftKey) {
130
+ e.preventDefault();
131
+ const syntheticEvent = {
132
+ preventDefault: () => e.preventDefault(),
133
+ stopPropagation: () => e.stopPropagation(),
134
+ nativeEvent: e.nativeEvent,
135
+ currentTarget: e.currentTarget,
136
+ target: e.target,
137
+ bubbles: e.bubbles,
138
+ cancelable: e.cancelable,
139
+ defaultPrevented: e.defaultPrevented,
140
+ eventPhase: e.eventPhase,
141
+ isTrusted: e.isTrusted,
142
+ timeStamp: e.timeStamp,
143
+ type: 'submit',
144
+ } as unknown as React.FormEvent;
145
+ void doSubmit(syntheticEvent);
146
+ }
147
+ };
148
+
149
+ const fileToDataURL = (file: File): Promise<string> => {
150
+ return new Promise((resolve, reject) => {
151
+ const reader = new FileReader();
152
+ reader.onload = () => resolve(reader.result as string);
153
+ reader.onerror = reject;
154
+ reader.readAsDataURL(file);
155
+ });
156
+ };
157
+
158
+ const getFileType = (mimeType: string): 'image' | 'file' => {
159
+ if (mimeType.startsWith('image/')) return 'image';
160
+ return 'file';
161
+ };
162
+
163
+ const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB - matches backend limit
164
+
165
+ const handleFileSelect = async (e: React.ChangeEvent<HTMLInputElement>) => {
166
+ const files = e.target.files;
167
+ if (!files || files.length === 0) return;
168
+
169
+ const newPending: Pending[] = [];
170
+ const errors: string[] = [];
171
+
172
+ for (let i = 0; i < files.length; i++) {
173
+ const file = files[i];
174
+
175
+ // Check file size before processing
176
+ if (file.size > MAX_FILE_SIZE) {
177
+ errors.push(`${file.name}: File exceeds size limit (max 20MB)`);
178
+ continue;
179
+ }
180
+
181
+ // Check if file size is exactly at limit (should be < not <=)
182
+ if (file.size >= MAX_FILE_SIZE) {
183
+ errors.push(`${file.name}: File exceeds size limit (max 20MB)`);
184
+ continue;
185
+ }
186
+
187
+ try {
188
+ const dataUrl = await fileToDataURL(file);
189
+ const type = getFileType(file.type);
190
+ newPending.push({
191
+ attachment: {
192
+ type,
193
+ url: dataUrl,
194
+ name: file.name,
195
+ mimeType: file.type,
196
+ size: file.size,
197
+ },
198
+ file,
199
+ });
200
+ } catch (err) {
201
+ const errorMessage = err instanceof Error ? err.message : 'Failed to read file';
202
+ errors.push(`${file.name}: ${errorMessage}`);
203
+ console.error(`Error reading file ${file.name}:`, err);
204
+ }
205
+ }
206
+
207
+ // Show errors if any, but still add successfully processed files
208
+ if (errors.length > 0) {
209
+ showError(`Some files could not be added:\n${errors.join('\n')}`);
210
+ }
211
+
212
+ // Add successfully processed files
213
+ if (newPending.length > 0) {
214
+ setPendingAttachments(prev => [...prev, ...newPending]);
215
+ }
216
+
217
+ if (fileInputRef.current) fileInputRef.current.value = '';
218
+ };
219
+
220
+ const removeAttachment = (index: number) => {
221
+ setPendingAttachments(prev => prev.filter((_, i) => i !== index));
222
+ };
223
+
224
+ const handleSubmitWithAttachments = (e: React.FormEvent) => {
225
+ void doSubmit(e);
226
+ };
227
+
228
+ return (
229
+ <>
230
+ {isAwaitingMicGesture && (
231
+ <div
232
+ role="status"
233
+ aria-live="polite"
234
+ css={css`
235
+ margin: 12px 15px 0 !important;
236
+ padding: 10px 14px !important;
237
+ border-radius: 8px !important;
238
+ border: 1px solid #ffeeba !important;
239
+ background-color: #fff3cd !important;
240
+ color: #856404 !important;
241
+ font-size: 13px !important;
242
+ line-height: 1.4 !important;
243
+ z-index: 1000 !important;
244
+ position: relative !important;
245
+ display: block !important;
246
+ visibility: visible !important;
247
+ opacity: 1 !important;
248
+ `}
249
+ >
250
+ ⚠️ The microphone needs a user interaction to re-enable. Please click on the page or press any key.
251
+ </div>
252
+ )}
253
+
254
+ {/* Attachments preview */}
255
+ {pendingAttachments.length > 0 && (
256
+ <div
257
+ css={css`
258
+ padding: 10px 15px 0 !important;
259
+ display: flex !important;
260
+ flex-wrap: wrap !important;
261
+ gap: 8px !important;
262
+ border-top: 1px solid #eee !important;
263
+ `}
264
+ >
265
+ {pendingAttachments.map(({ attachment }, index) => (
266
+ <div
267
+ key={index}
268
+ css={css`
269
+ position: relative !important;
270
+ display: inline-block !important;
271
+ border: 1px solid #ddd !important;
272
+ border-radius: 8px !important;
273
+ overflow: hidden !important;
274
+ background: #f5f5f5 !important;
275
+ `}
276
+ >
277
+ {attachment.type === 'image' && (
278
+ <img
279
+ src={attachment.url}
280
+ alt={attachment.name}
281
+ css={css`
282
+ max-width: 100px !important;
283
+ max-height: 100px !important;
284
+ object-fit: cover !important;
285
+ display: block !important;
286
+ `}
287
+ />
288
+ )}
289
+ {attachment.type === 'file' && (
290
+ <div
291
+ css={css`
292
+ padding: 20px 10px !important;
293
+ text-align: center !important;
294
+ min-width: 100px !important;
295
+ min-height: 100px !important;
296
+ display: flex !important;
297
+ align-items: center !important;
298
+ justify-content: center !important;
299
+ flex-direction: column !important;
300
+ `}
301
+ >
302
+ <AttachFileIcon css={css`font-size: 32px !important; color: #666 !important;`} />
303
+ <span
304
+ css={css`
305
+ font-size: 11px !important;
306
+ color: #666 !important;
307
+ margin-top: 4px !important;
308
+ word-break: break-word !important;
309
+ max-width: 80px !important;
310
+ `}
311
+ >
312
+ {attachment.name}
313
+ </span>
314
+ </div>
315
+ )}
316
+ <button
317
+ onClick={() => removeAttachment(index)}
318
+ css={css`
319
+ position: absolute !important;
320
+ top: 4px !important;
321
+ right: 4px !important;
322
+ background: rgba(0, 0, 0, 0.6) !important;
323
+ border: none !important;
324
+ border-radius: 50% !important;
325
+ width: 20px !important;
326
+ height: 20px !important;
327
+ display: flex !important;
328
+ align-items: center !important;
329
+ justify-content: center !important;
330
+ cursor: pointer !important;
331
+ padding: 0 !important;
332
+ color: white !important;
333
+ font-size: 12px !important;
334
+
335
+ &:hover {
336
+ background: rgba(0, 0, 0, 0.8) !important;
337
+ }
338
+ `}
339
+ >
340
+ <CloseIcon fontSize="small" />
341
+ </button>
342
+ </div>
343
+ ))}
344
+ </div>
345
+ )}
346
+
347
+ <div
348
+ css={css`
349
+ padding: 15px !important;
350
+ display: flex !important;
351
+ align-items: center !important;
352
+ gap: 10px !important;
353
+ border-top: 1px solid #eee !important;
354
+ `}
355
+ >
356
+ <input
357
+ ref={fileInputRef}
358
+ type="file"
359
+ multiple
360
+ accept="image/*,.pdf,.doc,.docx,.txt,.csv,.xlsx,.xls"
361
+ onChange={handleFileSelect}
362
+ css={css`
363
+ display: none !important;
364
+ `}
365
+ />
366
+ <Tooltip
367
+ title={
368
+ isLlamaModel
369
+ ? 'Llama model does not support file upload. Please switch to another model to use this feature.'
370
+ : 'Upload file'
371
+ }
372
+ >
373
+ <span>
374
+ <IconButton
375
+ onClick={() => !isLlamaModel && fileInputRef.current?.click()}
376
+ disabled={isDisabled || isLlamaModel}
377
+ css={css`
378
+ padding: 8px !important;
379
+ color: ${isDisabled || isLlamaModel ? '#ccc' : '#666'} !important;
380
+ cursor: ${isLlamaModel ? 'not-allowed' : 'pointer'} !important;
381
+ `}
382
+ >
383
+ <AttachFileIcon />
384
+ </IconButton>
385
+ </span>
386
+ </Tooltip>
387
+ <InputBase
388
+ value={inputValue}
389
+ onChange={(e) => setInputValue(e.target.value)}
390
+ onKeyPress={handleKeyPress}
391
+ placeholder={inputPlaceholder}
392
+ fullWidth
393
+ inputProps={{
394
+ ref: textInputRef
395
+ }}
396
+ endAdornment={
397
+ <>
398
+ <Tooltip title={textToSpeechEnabled ? 'Text-to-speech enabled' : 'Text-to-speech disabled'}>
399
+ <IconButton
400
+ size="small"
401
+ onClick={(e) => {
402
+ e.stopPropagation();
403
+ setTextToSpeechEnabled(!textToSpeechEnabled);
404
+ }}
405
+ css={css`
406
+ padding: 4px !important;
407
+ margin-right: 4px !important;
408
+ color: ${textToSpeechEnabled ? '#1976d2' : '#9e9e9e'} !important;
409
+ `}
410
+ >
411
+ {textToSpeechEnabled ? <VolumeUpIcon fontSize="small" /> : <VolumeOffIcon fontSize="small" />}
412
+ </IconButton>
413
+ </Tooltip>
414
+ {isMicEnabled && (
415
+ <Tooltip title={autoSendEnabled ? 'Auto send enabled' : 'Auto send disabled'}>
416
+ <IconButton
417
+ size="small"
418
+ onClick={(e) => {
419
+ e.stopPropagation();
420
+ setAutoSendEnabled(!autoSendEnabled);
421
+ }}
422
+ css={css`
423
+ padding: 4px !important;
424
+ margin-right: 4px !important;
425
+ color: ${autoSendEnabled ? '#1976d2' : '#9e9e9e'} !important;
426
+ `}
427
+ >
428
+ <SendIcon fontSize="small" />
429
+ </IconButton>
430
+ </Tooltip>
431
+ )}
432
+ <IconButton onClick={() => (isMicEnabled ? stopRecording() : startRecording())}>
433
+ {isMicEnabled ? <MicIcon /> : <MicOffIcon />}
434
+ </IconButton>
435
+ </>
436
+ }
437
+ css={css`
438
+ flex: 1 !important;
439
+ padding: 10px !important;
440
+ border: 1px solid #ddd !important;
441
+ border-radius: 20px !important;
442
+ outline: none !important;
443
+ font-size: 14px !important;
444
+ background-color: #fff !important;
445
+ height: 40px !important;
446
+ box-sizing: border-box !important;
447
+
448
+ @media (max-width: 768px) {
449
+ font-size: 16px !important;
450
+ & input {
451
+ font-size: 16px !important;
452
+ }
453
+ }
454
+ `}
455
+ />
456
+ <button
457
+ onClick={handleSubmitWithAttachments}
458
+ disabled={isDisabled || !hasContent()}
459
+ css={css`
460
+ ${actionButton}
461
+ padding: 10px 20px !important;
462
+ border-radius: 20px !important;
463
+ position: relative !important;
464
+ display: flex !important;
465
+ align-items: center !important;
466
+ justify-content: center !important;
467
+ height: 40px !important;
468
+ box-sizing: border-box !important;
469
+
470
+ &:disabled {
471
+ background-color: #e9ecef !important;
472
+ color: #6c757d !important;
473
+ cursor: not-allowed !important;
474
+ }
475
+
476
+ &:hover:not(:disabled) {
477
+ background-color: #0056b3 !important;
478
+ }
479
+ `}
480
+ >
481
+ Send
482
+ {(isLoading || isAITLoading || isUploading) && (
483
+ <span
484
+ css={css`
485
+ margin-left: 8px !important;
486
+ display: flex !important;
487
+ align-items: center !important;
488
+ `}
489
+ >
490
+ <svg width="16" height="16" viewBox="0 0 50 50">
491
+ <circle
492
+ cx="25"
493
+ cy="25"
494
+ r="20"
495
+ fill="none"
496
+ stroke="#fff"
497
+ strokeWidth="4"
498
+ strokeDasharray="31.4 31.4"
499
+ strokeLinecap="round"
500
+ >
501
+ <animateTransform
502
+ attributeName="transform"
503
+ type="rotate"
504
+ from="0 25 25"
505
+ to="360 25 25"
506
+ dur="1s"
507
+ repeatCount="indefinite"
508
+ />
509
+ </circle>
510
+ </svg>
511
+ </span>
512
+ )}
513
+ </button>
514
+ </div>
515
+ </>
516
+ );
517
+ };