@stack-spot/ai-chat-widget 1.24.4 → 1.25.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 (108) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/app-metadata.json +3 -3
  3. package/dist/chat-interceptors/send-message.js +3 -3
  4. package/dist/chat-interceptors/send-message.js.map +1 -1
  5. package/dist/components/FileDescription.d.ts +10 -0
  6. package/dist/components/FileDescription.d.ts.map +1 -0
  7. package/dist/components/FileDescription.js +85 -0
  8. package/dist/components/FileDescription.js.map +1 -0
  9. package/dist/state/ChatEntry.d.ts +9 -0
  10. package/dist/state/ChatEntry.d.ts.map +1 -1
  11. package/dist/state/ChatEntry.js.map +1 -1
  12. package/dist/state/ChatState.d.ts +5 -0
  13. package/dist/state/ChatState.d.ts.map +1 -1
  14. package/dist/state/ChatState.js +6 -0
  15. package/dist/state/ChatState.js.map +1 -1
  16. package/dist/state/constants.d.ts +5 -0
  17. package/dist/state/constants.d.ts.map +1 -0
  18. package/dist/state/constants.js +9 -0
  19. package/dist/state/constants.js.map +1 -0
  20. package/dist/state/types.d.ts +4 -0
  21. package/dist/state/types.d.ts.map +1 -1
  22. package/dist/utils/chat.d.ts +2 -1
  23. package/dist/utils/chat.d.ts.map +1 -1
  24. package/dist/utils/chat.js +2 -1
  25. package/dist/utils/chat.js.map +1 -1
  26. package/dist/utils/upload/FileUpload.d.ts +21 -0
  27. package/dist/utils/upload/FileUpload.d.ts.map +1 -0
  28. package/dist/utils/upload/FileUpload.js +55 -0
  29. package/dist/utils/upload/FileUpload.js.map +1 -0
  30. package/dist/utils/upload/UploadManager.d.ts +40 -0
  31. package/dist/utils/upload/UploadManager.d.ts.map +1 -0
  32. package/dist/utils/upload/UploadManager.js +131 -0
  33. package/dist/utils/upload/UploadManager.js.map +1 -0
  34. package/dist/utils/upload/context.d.ts +15 -0
  35. package/dist/utils/upload/context.d.ts.map +1 -0
  36. package/dist/utils/upload/context.js +37 -0
  37. package/dist/utils/upload/context.js.map +1 -0
  38. package/dist/utils/upload/errors.d.ts +17 -0
  39. package/dist/utils/upload/errors.d.ts.map +1 -0
  40. package/dist/utils/upload/errors.js +27 -0
  41. package/dist/utils/upload/errors.js.map +1 -0
  42. package/dist/utils/upload/types.d.ts +7 -0
  43. package/dist/utils/upload/types.d.ts.map +1 -0
  44. package/dist/utils/upload/types.js +2 -0
  45. package/dist/utils/upload/types.js.map +1 -0
  46. package/dist/utils/upload/utils.d.ts +4 -0
  47. package/dist/utils/upload/utils.d.ts.map +1 -0
  48. package/dist/utils/upload/utils.js +10 -0
  49. package/dist/utils/upload/utils.js.map +1 -0
  50. package/dist/views/Chat/AgentInfo.d.ts +2 -1
  51. package/dist/views/Chat/AgentInfo.d.ts.map +1 -1
  52. package/dist/views/Chat/AgentInfo.js +2 -2
  53. package/dist/views/Chat/AgentInfo.js.map +1 -1
  54. package/dist/views/Chat/ChatMessage.d.ts +1 -1
  55. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  56. package/dist/views/Chat/ChatMessage.js +24 -5
  57. package/dist/views/Chat/ChatMessage.js.map +1 -1
  58. package/dist/views/Chat/styled.d.ts.map +1 -1
  59. package/dist/views/Chat/styled.js +15 -1
  60. package/dist/views/Chat/styled.js.map +1 -1
  61. package/dist/views/MessageInput/{InfoBar.d.ts → ContextBar.d.ts} +2 -2
  62. package/dist/views/MessageInput/ContextBar.d.ts.map +1 -0
  63. package/dist/views/MessageInput/{InfoBar.js → ContextBar.js} +5 -5
  64. package/dist/views/MessageInput/ContextBar.js.map +1 -0
  65. package/dist/views/MessageInput/SelectContent.d.ts.map +1 -1
  66. package/dist/views/MessageInput/SelectContent.js +14 -17
  67. package/dist/views/MessageInput/SelectContent.js.map +1 -1
  68. package/dist/views/MessageInput/UploadBar.d.ts +2 -0
  69. package/dist/views/MessageInput/UploadBar.d.ts.map +1 -0
  70. package/dist/views/MessageInput/UploadBar.js +47 -0
  71. package/dist/views/MessageInput/UploadBar.js.map +1 -0
  72. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  73. package/dist/views/MessageInput/dictionary.d.ts.map +1 -1
  74. package/dist/views/MessageInput/dictionary.js +18 -4
  75. package/dist/views/MessageInput/dictionary.js.map +1 -1
  76. package/dist/views/MessageInput/index.d.ts.map +1 -1
  77. package/dist/views/MessageInput/index.js +46 -5
  78. package/dist/views/MessageInput/index.js.map +1 -1
  79. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  80. package/dist/views/MessageInput/styled.js +56 -27
  81. package/dist/views/MessageInput/styled.js.map +1 -1
  82. package/dist/views/Steps/dictionary.d.ts +1 -1
  83. package/package.json +2 -2
  84. package/src/app-metadata.json +3 -3
  85. package/src/chat-interceptors/send-message.ts +3 -3
  86. package/src/components/FileDescription.tsx +114 -0
  87. package/src/state/ChatEntry.ts +10 -0
  88. package/src/state/ChatState.ts +6 -0
  89. package/src/state/constants.ts +12 -0
  90. package/src/state/types.ts +5 -0
  91. package/src/utils/chat.ts +3 -1
  92. package/src/utils/upload/FileUpload.ts +64 -0
  93. package/src/utils/upload/UploadManager.ts +147 -0
  94. package/src/utils/upload/context.tsx +44 -0
  95. package/src/utils/upload/errors.ts +34 -0
  96. package/src/utils/upload/types.ts +7 -0
  97. package/src/utils/upload/utils.ts +12 -0
  98. package/src/views/Chat/AgentInfo.tsx +3 -2
  99. package/src/views/Chat/ChatMessage.tsx +48 -13
  100. package/src/views/Chat/styled.ts +15 -1
  101. package/src/views/MessageInput/{InfoBar.tsx → ContextBar.tsx} +9 -9
  102. package/src/views/MessageInput/SelectContent.tsx +17 -21
  103. package/src/views/MessageInput/UploadBar.tsx +69 -0
  104. package/src/views/MessageInput/dictionary.ts +18 -4
  105. package/src/views/MessageInput/index.tsx +77 -32
  106. package/src/views/MessageInput/styled.ts +56 -27
  107. package/dist/views/MessageInput/InfoBar.d.ts.map +0 -1
  108. package/dist/views/MessageInput/InfoBar.js.map +0 -1
@@ -4,9 +4,11 @@ import { Badge, IconButton, Tooltip } from '@citric/ui'
4
4
  import { agentClient } from '@stack-spot/portal-network'
5
5
  import { listToClass } from '@stack-spot/portal-theme'
6
6
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
7
+ import { groupBy } from 'lodash'
7
8
  import { createElement, Dispatch, useCallback, useMemo, useRef, useState } from 'react'
8
9
  import { PhoneInput } from 'react-international-phone'
9
10
  import 'react-international-phone/style.css'
11
+ import { FileDescription } from '../../components/FileDescription'
10
12
  import { Markdown } from '../../components/Markdown'
11
13
  import { StackedBadge } from '../../components/StackedBadge'
12
14
  import { useChatEntry, useCurrentChat, useWidget } from '../../context/hooks'
@@ -166,6 +168,14 @@ const RenderInputsEntry = ({ isLast, entry, value, setValue, labels, setLabels }
166
168
  </Flex>
167
169
  }
168
170
 
171
+ const UserInfo = ({ entry }: { entry: TextChatEntry }) => {
172
+ switch (entry.agentType) {
173
+ case 'user': return
174
+ case 'bot': return <AgentInfo agent={entry.agent} />
175
+ case 'system': return <AgentInfo agent={{ id: 'system', label: 'System' }} icon={<Cog />} />
176
+ }
177
+ }
178
+
169
179
  /**
170
180
  * Renders a message (ChatEntry) in the chat.
171
181
  */
@@ -176,7 +186,6 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
176
186
  const [labels, setLabels] = useState<string[]>(message.getValue()?.initialValue ?? [])
177
187
  const entry = useChatEntry(message)
178
188
  const dateFormatter = useDateFormatter()
179
- const userInfo = entry.agentType === 'user' ? <></> : <AgentInfo agent={entry.agent} />
180
189
  const date = new Date(entry.updated ?? '')
181
190
  const shouldShowFooter = entry.updated && !isNaN(date.getTime())
182
191
  const ref = useRef<HTMLLIElement>(null)
@@ -187,6 +196,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
187
196
  const [agent] = agentClient.agentById.useStatefulQuery({ agentId, builtIn: !!entry?.agent?.builtIn }, { enabled: !!agentId })
188
197
  const [copied, setCopied] = useState(false)
189
198
  const [showUserButtonCopy, setShowUserButtonCopy] = useState(false)
199
+
190
200
 
191
201
  useChatScrollToBottomEffect(ref, [entry])
192
202
 
@@ -258,9 +268,26 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
258
268
  setTimeout(() => setCopied(false), 2000)
259
269
  }
260
270
 
271
+ const renderUploads = () => {
272
+ const groups = groupBy(entry.upload, f => f.image ? 'images' : 'documents')
273
+ const lists: React.ReactElement[] = []
274
+ if (groups.images?.length) {
275
+ lists.push(<ul className="image-uploads">{groups.images.map(f => <li key={f.id}><img src={f.image}></img></li>)}</ul>)
276
+ }
277
+ if (groups.documents?.length) {
278
+ lists.push(
279
+ <ul className="document-uploads">
280
+ {groups.documents.map(f => <li key={f.id}><FileDescription fileName={f.name} /></li>)}
281
+ </ul>,
282
+ )
283
+ }
284
+ return lists
285
+ }
286
+
261
287
  const renderContent = () => {
262
288
  if (entry.type === 'md') {
263
289
  return <>
290
+ {renderUploads()}
264
291
  <Markdown onCopyCode={(code) => onCopyCode(code, entry.messageId ?? '', chat)}>{entry.content}</Markdown>
265
292
  {renderActions()}
266
293
  </>
@@ -268,6 +295,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
268
295
  if (entry.type === 'text') {
269
296
  return <>
270
297
  <p className="plain-text">{entry.content}</p>
298
+ {renderUploads()}
271
299
  {renderActions()}
272
300
  </>
273
301
  }
@@ -285,21 +313,26 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
285
313
  widget.set('panel', 'tools')
286
314
  }
287
315
 
288
- return (entry.content || entry.error || !!entry.steps?.length) && (
316
+
317
+ return (entry.content || entry.error || !!entry.steps?.length || entry.upload?.length) && (
289
318
  <li key={entry.messageId} className={entry.agentType} ref={ref}>
290
319
  <div className="chat-message-container"
291
320
  onMouseEnter={entry.agentType === 'user' ? () => setShowUserButtonCopy(true) : undefined}
292
321
  onMouseLeave={entry.agentType === 'user' ? () => setShowUserButtonCopy(false) : undefined}>
293
322
  <div className="chat-message" ref={chatRef} onKeyDown={handleKeyDown} tabIndex={0}>
294
- <div className={`user-info ${entry.agentType}`}>{userInfo}</div>
323
+ <div className={`user-info ${entry.agentType}`}><UserInfo entry={entry} /></div>
295
324
  {beforeMessage && createElement(beforeMessage, { message })}
296
- {(entry.content || entry.steps) && <div className={listToClass(['message-content', entry.card && 'card', entry.type])}>
297
- {!!entry.badges?.length && <div className="badges">
298
- {entry.badges.map((b, index) => <Badge key={index} palette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
299
- </div>}
300
- {renderContent()}
301
- {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
302
- </div>}
325
+ {(entry.content || entry.steps || entry.upload?.length) && (
326
+ <div className={listToClass(['message-content', entry.card && 'card', entry.type])}>
327
+ {!!entry.badges?.length && <div className="badges">
328
+ {entry.badges.map((b, index) => <Badge key={index} palette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
329
+ </div>}
330
+ {renderContent()}
331
+
332
+ {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
333
+ </div>
334
+ )}
335
+
303
336
  {entry.error && (
304
337
  <div className="error">
305
338
  <IconBox size="xs"><TimesCircle /></IconBox>
@@ -329,7 +362,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
329
362
  </li>
330
363
  ))}</ul>
331
364
  </div>}
332
-
365
+
333
366
  {shouldShowFooter && <div className="message-footer">
334
367
  {entry.agentType === 'bot' && !entry.error && <div className="message-actions">
335
368
  {entry.type === 'md' && (
@@ -368,6 +401,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
368
401
  </>
369
402
  )}
370
403
  </div>}
404
+
371
405
  {entry.agentType === 'user' && (
372
406
  <div className="message-actions">
373
407
  {copied ? (
@@ -384,7 +418,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
384
418
  </Tooltip>
385
419
  ) : (
386
420
  showUserButtonCopy && (
387
- <div>
421
+ <div className="action-bar">
388
422
  <IconButton
389
423
  appearance="square"
390
424
  color="light"
@@ -393,13 +427,14 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
393
427
  onClick={handleCopy}
394
428
  size="sm"
395
429
  >
396
- <Copy/>
430
+ <Copy className="copy-btn"/>
397
431
  </IconButton>
398
432
  </div>
399
433
  )
400
434
  )}
401
435
  </div>
402
436
  )}
437
+
403
438
  <Text as="label" appearance="microtext1" className="chat-date">
404
439
  {dateFormatter.formatForChatMessage(date)}
405
440
  </Text>
@@ -39,7 +39,7 @@ export const ChatList: IStyledComponentBase<
39
39
  flex-direction: row;
40
40
  gap: 10px;
41
41
 
42
- &.bot {
42
+ &.bot, &.system {
43
43
  align-items: center;
44
44
  }
45
45
  }
@@ -139,6 +139,20 @@ export const ChatList: IStyledComponentBase<
139
139
  background: linear-gradient(180deg, ${theme.color.blue[500]} 0%, ${theme.color.indigo[500]} 100%);
140
140
  }
141
141
  }
142
+ .image-uploads, .document-uploads {
143
+ display: flex;
144
+ flex-direction: row;
145
+ flex-wrap: nowrap;
146
+ gap: 8px;
147
+ list-style: none;
148
+ margin: 0 0 4px 0;
149
+ padding: 0;
150
+
151
+ img {
152
+ max-width: 240px;
153
+ max-height: 240px;
154
+ }
155
+ }
142
156
  }
143
157
 
144
158
  &.user {
@@ -8,15 +8,15 @@ import { FadingOverflow } from '../../components/FadingOverflow'
8
8
  import { useCurrentChat, useCurrentChatState } from '../../context/hooks'
9
9
  import { useMessageInputDictionary } from './dictionary'
10
10
 
11
- interface InfoBadgeProps {
11
+ interface ContextBadgeProps {
12
12
  label: string,
13
13
  color: ColorPaletteName,
14
14
  dismiss: string,
15
15
  onDismiss?: () => void,
16
16
  }
17
17
 
18
- const InfoBadge = ({ label, color, dismiss, onDismiss }: InfoBadgeProps) => (
19
- <Badge appearance="square" palette={color} className="info-badge"
18
+ const ContextBadge = ({ label, color, dismiss, onDismiss }: ContextBadgeProps) => (
19
+ <Badge appearance="square" palette={color} className="context-badge"
20
20
  afterElement={
21
21
  onDismiss &&
22
22
  <IconButton appearance="square" colorIcon={`${color}.800`} onClick={onDismiss} title={dismiss} arial-label={dismiss}>
@@ -34,7 +34,7 @@ const InfoBadge = ({ label, color, dismiss, onDismiss }: InfoBadgeProps) => (
34
34
  * - which stack is being used;
35
35
  * - which knowledge sources are being used.
36
36
  */
37
- export const InfoBar = () => {
37
+ export const ContextBar = () => {
38
38
  const t = useMessageInputDictionary()
39
39
  const chat = useCurrentChat()
40
40
  const currentStack = useCurrentChatState('stack')
@@ -47,7 +47,7 @@ export const InfoBar = () => {
47
47
  const onDismiss = features.knowledgeSource
48
48
  ? (() => chat.set('knowledgeSources', currentKnowledgeSources.filter(({ id }) => id !== ks.id)))
49
49
  : undefined
50
- return <li key={ks.id}><InfoBadge label={ks.label} dismiss={t.removeKS} color="teal" onDismiss={onDismiss} /></li>
50
+ return <li key={ks.id}><ContextBadge label={ks.label} dismiss={t.removeKS} color="teal" onDismiss={onDismiss} /></li>
51
51
  }), [currentKnowledgeSources])
52
52
  const shouldRenderRemoveAllButton = (
53
53
  currentSelection
@@ -69,7 +69,7 @@ export const InfoBar = () => {
69
69
  }, [])
70
70
 
71
71
  return (
72
- <div className={listToClass(['info-bar', visible && 'visible'])}>
72
+ <div className={listToClass(['info-bar', 'context-bar', visible && 'visible'])}>
73
73
  <div className="space"></div>
74
74
  <div className="content">
75
75
  {shouldRenderRemoveAllButton && (
@@ -83,12 +83,12 @@ export const InfoBar = () => {
83
83
  <ul>
84
84
  {currentSelection && (
85
85
  <li>
86
- <InfoBadge label={t.selected} dismiss={t.removeSelection} color="blue" onDismiss={removeCodeSelection} />
86
+ <ContextBadge label={t.selected} dismiss={t.removeSelection} color="blue" onDismiss={removeCodeSelection} />
87
87
  </li>
88
88
  )}
89
89
  {currentStack && (
90
90
  <li>
91
- <InfoBadge
91
+ <ContextBadge
92
92
  label={currentStack.label}
93
93
  dismiss={t.removeStack}
94
94
  color="cyan"
@@ -98,7 +98,7 @@ export const InfoBar = () => {
98
98
  )}
99
99
  {currentWorkspace && (
100
100
  <li>
101
- <InfoBadge
101
+ <ContextBadge
102
102
  label={currentWorkspace.label}
103
103
  dismiss={t.removeWorkspace}
104
104
  color="pink"
@@ -1,9 +1,10 @@
1
- import { KnowledgeSource, Plus, Spaces, Stack } from '@citric/icons'
1
+ import { DocumentUpload, KnowledgeSource, Plus, Spaces, Stack } from '@citric/icons'
2
2
  import { IconButton } from '@citric/ui'
3
3
  import { SelectionList } from '@stack-spot/portal-components/SelectionList'
4
- import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
5
4
  import { useMemo, useState } from 'react'
6
5
  import { useCurrentChatState, useWidget } from '../../context/hooks'
6
+ import { useUploadManager } from '../../utils/upload/context'
7
+ import { useMessageInputDictionary } from './dictionary'
7
8
 
8
9
  type chatFeatures = 'workspace' | 'knowledgeSource' | 'stack'
9
10
  type chatPanel = 'ks' | 'workspace' | 'stack'
@@ -12,8 +13,9 @@ export const SelectContent = () => {
12
13
  const widget = useWidget()
13
14
  const [visibleMenu, setVisibleMenu] = useState(false)
14
15
  const features = useCurrentChatState('features')
16
+ const t = useMessageInputDictionary()
17
+ const uploadManager = useUploadManager()
15
18
  const hasFeatureButtons = features.workspace || features.knowledgeSource || features.stack
16
- const t = useTranslate(dictionary)
17
19
 
18
20
  const itemConfigs = [
19
21
  {
@@ -59,31 +61,25 @@ export const SelectContent = () => {
59
61
  title={visibleMenu ? t.collapse : t.expand}
60
62
  data-test-hint="button-options"
61
63
  aria-label={visibleMenu ? t.collapse : t.expand}
64
+ aria-controls="chatMessageMenu"
62
65
  onClick={() => setVisibleMenu(state => !state)}>
63
66
  <Plus />
64
67
  </IconButton>
65
68
  <SelectionList
66
- style={{
67
- position: 'absolute',
68
- top: '-140px',
69
- }}
70
- id="menuConfig"
69
+ className="message-menu"
70
+ id="chatMessageMenu"
71
71
  visible={visibleMenu}
72
72
  onHide={() => setVisibleMenu(false)}
73
- items={listItems}
73
+ items={[
74
+ ...listItems,
75
+ {
76
+ label: t.upload,
77
+ onClick: () => uploadManager.open(),
78
+ className: 'upload-item',
79
+ icon: <DocumentUpload />,
80
+ },
81
+ ]}
74
82
  />
75
83
  </>
76
84
  )
77
85
  }
78
-
79
- const dictionary = {
80
- en: {
81
- expand: 'Expand options',
82
- collapse: 'Collapse options',
83
- },
84
- pt: {
85
- expand: 'Mostrar opções',
86
- collapse: 'Esconder opções',
87
- },
88
- } satisfies Dictionary
89
-
@@ -0,0 +1,69 @@
1
+ import { listToClass } from '@stack-spot/portal-theme'
2
+ import { interpolate } from '@stack-spot/portal-translate'
3
+ import { useMemo } from 'react'
4
+ import { FadingOverflow } from '../../components/FadingOverflow'
5
+ import { FileDescription } from '../../components/FileDescription'
6
+ import { useCurrentChat } from '../../context/hooks'
7
+ import { ChatEntry } from '../../state/ChatEntry'
8
+ import { useUploadErrorEffect, useUploadManager, useUploads, useUploadStatus } from '../../utils/upload/context'
9
+ import { FileIsTooLarge, MaxFilesReached } from '../../utils/upload/errors'
10
+ import { FileUpload } from '../../utils/upload/FileUpload'
11
+ import { useMessageInputDictionary } from './dictionary'
12
+
13
+ interface UploadedFileProps {
14
+ upload: FileUpload,
15
+ }
16
+
17
+ function createImageFromFile(file: File) {
18
+ return <img src={URL.createObjectURL(file)} alt={file.name} />
19
+ }
20
+
21
+ const UploadItem = ({ upload }: UploadedFileProps) => {
22
+ const uploadManager = useUploadManager()
23
+ const icon = upload.file.type.toLowerCase().startsWith('image/') ? createImageFromFile(upload.file) : undefined
24
+ const status = useUploadStatus(upload)
25
+ return <FileDescription
26
+ fileName={upload.file.name}
27
+ icon={icon}
28
+ onRemove={() => uploadManager.remove(upload)}
29
+ onRetry={() => upload.retry()}
30
+ status={status}
31
+ />
32
+ }
33
+
34
+ export const UploadBar = () => {
35
+ const uploads = useUploads()
36
+ const listItems = useMemo(() => uploads.map((up) => <li key={up.id}><UploadItem upload={up} /></li>), [uploads])
37
+ const t = useMessageInputDictionary()
38
+ const chat = useCurrentChat()
39
+ const visible = !!uploads.length
40
+
41
+ useUploadErrorEffect((errors) => {
42
+ const sizeErrors = errors.filter(e => e instanceof FileIsTooLarge)
43
+ const maxItemsErrors = errors.filter(e => e instanceof MaxFilesReached)
44
+ const maxSize = sizeErrors[0]?.maxSize
45
+ const maxItems = maxItemsErrors[0]?.maxFiles
46
+ const sizeErrorsNames = sizeErrors.map(e => e.fileName)
47
+ const maxItemsErrorsNames = maxItemsErrors.map(e => e.fileName)
48
+ const lines: string[] = []
49
+ if (sizeErrors.length) {
50
+ lines.push(`${interpolate(t.uploadSizeError, `${maxSize.value} ${maxSize.unit}`)}\n- ${sizeErrorsNames.join('\n- ')}`)
51
+ }
52
+ if (maxItemsErrors.length) {
53
+ lines.push(`${interpolate(t.uploadItemLimitError, maxItems)}\n- ${maxItemsErrorsNames.join('\n- ')}`)
54
+ }
55
+ if (!lines.length) return
56
+ chat.pushMessage(new ChatEntry({ agentType: 'system', type: 'md', content: lines.join('\n\n') }))
57
+ })
58
+
59
+ return (
60
+ <div className={listToClass(['info-bar', 'upload-bar', visible && 'visible'])}>
61
+ <div className="space"></div>
62
+ <div className="content">
63
+ <FadingOverflow className="list-overflow" scroll="arrows" enableHorizontalScrollWithVerticalWheel>
64
+ <ul>{listItems}</ul>
65
+ </FadingOverflow>
66
+ </div>
67
+ </div>
68
+ )
69
+ }
@@ -7,8 +7,6 @@ const dictionary = {
7
7
  spot: 'Select spot',
8
8
  knowledgeSource: 'Select knowledge sources',
9
9
  agent: 'Select agent',
10
- collapse: 'Hide buttons',
11
- expand: 'Show buttons',
12
10
  send: 'Send message',
13
11
  placeholder: 'Message to $0 or use / or @.',
14
12
  cancel: 'Cancel',
@@ -19,6 +17,14 @@ const dictionary = {
19
17
  selected: 'Selected',
20
18
  removeSelection: 'Remove current code selection',
21
19
  remove: 'Remove',
20
+ upload: 'Upload file',
21
+ expand: 'Expand options',
22
+ collapse: 'Collapse options',
23
+ uploadSizeError: 'The following files were not added to the upload list because they\'re larger than $1:',
24
+ uploadItemLimitError: 'The following files were not added because no more than $1 items may be uploaded at a time:',
25
+ cantSendBecauseOfUploadError: 'Can\'t send the message because one of the files in the upload list could not be uploaded. Please, retry it or remove it from the list.',
26
+ cantSendBecauseOfUploadProgress: 'Please wait until all files are uploaded before sending the message. You can also cancel the upload by removing it from the list of uploads.',
27
+ cantSendBecauseOfEmptyContent: 'You can\'t send empty messages. Please write some text or upload a file.',
22
28
  },
23
29
  pt: {
24
30
  stack: 'Selecionar stack',
@@ -26,8 +32,6 @@ const dictionary = {
26
32
  spot: 'Selecionar spot',
27
33
  knowledgeSource: 'Selecionar knowledge sources',
28
34
  agent: 'Selecionar agente',
29
- collapse: 'Esconder botões',
30
- expand: 'Mostrar botões',
31
35
  send: 'Enviar mensagem',
32
36
  placeholder: 'Mensagem para $0 ou use / ou @.',
33
37
  cancel: 'Cancelar',
@@ -38,6 +42,16 @@ const dictionary = {
38
42
  selected: 'Selecionado',
39
43
  removeSelection: 'Desfazer seleção de código',
40
44
  remove: 'Remover',
45
+ upload: 'Enviar arquivo',
46
+ expand: 'Mostrar opções',
47
+ collapse: 'Esconder opções',
48
+ uploadSizeError: 'Os seguintes arquivos não foram adicionados à lista de upload porque eles são maiores que $0:',
49
+ uploadItemLimitError: 'Os seguintes arquivos não foram adicionados à lista de upload porque é permitido enviar no máximo $0 arquivos por vez:',
50
+ uploadError: 'Ocorreu um erro ao enviar o arquivo "$0".',
51
+ unknownUploadError: 'Ocorreu um erro ao enviar os arquivos.',
52
+ cantSendBecauseOfUploadError: 'Não é possível enviar a mensagem, pois um dos arquivos na lista de uploads não pôde ser enviado. Por favor, tente enviá-lo novamente ou remova-o da lista.',
53
+ cantSendBecauseOfUploadProgress: 'Por favor aguarde todos os uploads de arquivos antes de enviar a mensagem. Você pode cancelar o upload removendo o arquivo da lista de uploads.',
54
+ cantSendBecauseOfEmptyContent: 'Não é possível enviar mensagens vazia. Por favor, escreva algum texto ou envie um arquivo.',
41
55
  },
42
56
  } satisfies Dictionary
43
57
 
@@ -7,14 +7,16 @@ import { useCurrentChat, useCurrentChatState, useWidgetState } from '../../conte
7
7
  import { quickCommandRegex } from '../../regex'
8
8
  import { ChatEntry } from '../../state/ChatEntry'
9
9
  import { checkIsTrial } from '../../utils/check-is-trial'
10
+ import { UploadProvider } from '../../utils/upload/context'
10
11
  import { AgentSelector } from './AgentSelector'
11
12
  import { ButtonAgent } from './ButtonAgent'
12
13
  import { ButtonBar } from './ButtonBar'
13
14
  import { useUserEntryHistoryShortcut } from './chat-entry-history'
15
+ import { ContextBar } from './ContextBar'
14
16
  import { useMessageInputDictionary } from './dictionary'
15
- import { InfoBar } from './InfoBar'
16
17
  import { QuickCommandSelector } from './QuickCommandSelector'
17
18
  import { MAX_INPUT_HEIGHT, MessageInputBox, MIN_INPUT_HEIGHT } from './styled'
19
+ import { UploadBar } from './UploadBar'
18
20
 
19
21
  /**
20
22
  * This renders the MessageInput part of the layout which includes the progress bar, the actual textarea, the badges telling what is
@@ -35,14 +37,56 @@ export const MessageInput = () => {
35
37
  const { handleKeyDown, handleKeyUp } = useUserEntryHistoryShortcut()
36
38
  const isTrial = checkIsTrial()
37
39
 
40
+ const checkSendRequirements = useCallback(() => {
41
+ if (chat.uploadManager.status === 'error') {
42
+ chat.pushMessage(new ChatEntry({
43
+ agentType: 'system',
44
+ type: 'text',
45
+ content: t.cantSendBecauseOfUploadError,
46
+ }))
47
+ return false
48
+ }
49
+ if (chat.uploadManager.status === 'uploading') {
50
+ chat.pushMessage(new ChatEntry({
51
+ agentType: 'system',
52
+ type: 'text',
53
+ content: t.cantSendBecauseOfUploadProgress,
54
+ }))
55
+ return false
56
+ }
57
+ if (!chat.get('nextMessage') && !chat.uploadManager.get().length) {
58
+ chat.pushMessage(new ChatEntry({
59
+ agentType: 'system',
60
+ type: 'text',
61
+ content: t.cantSendBecauseOfEmptyContent,
62
+ }))
63
+ return false
64
+ }
65
+ return true
66
+ }, [chat])
67
+
38
68
  const onSend = useCallback(async () => {
39
- const message = chat.get('nextMessage')
40
- if (!message) return
69
+ const message = chat.get('nextMessage')
70
+ const canSend = checkSendRequirements()
71
+ if (!canSend) return
41
72
  const code = chat.get('codeSelection')
42
73
  const language = chat.get('codeLanguage')
43
- const prompt = code && !quickCommandRegex.test(message) ? `${message}\n\`\`\`${language}\n${code}\n\`\`\`` : message
44
- chat.pushMessage(ChatEntry.createUserEntry(prompt, true))
74
+ const prompt = code && !quickCommandRegex.test(message ?? '') ? `${message}\n\`\`\`${language}\n${code}\n\`\`\`` : message
75
+ chat.pushMessage(new ChatEntry({
76
+ type: 'md',
77
+ agentType: 'user',
78
+ content: prompt || '',
79
+ upload: chat.uploadManager.get().map(
80
+ up => ({
81
+ id: up.uploadId!, // we know that all files have been uploaded, so they have an id
82
+ name: up.file.name,
83
+ image: up.file.type.toLowerCase().startsWith('image/') ? URL.createObjectURL(up.file) : undefined,
84
+ }),
85
+ ),
86
+ updated: new Date().toISOString(),
87
+ }))
45
88
  chat.set('nextMessage', '')
89
+ chat.uploadManager.reset()
46
90
  }, [chat])
47
91
 
48
92
  const onKeyDown = useCallback((event: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -59,33 +103,34 @@ export const MessageInput = () => {
59
103
  }, [isLoading])
60
104
 
61
105
  return (
62
- <MessageInputBox aria-busy={isLoading} className="message-input" $inputFocused={focused}>
63
- <div className="wrapper-action">
64
- <QuickCommandSelector inputRef={textAreaRef} isTrial={isTrial} />
65
- <AgentSelector inputRef={textAreaRef} isTrial={isTrial} />
66
- <div className={listToClass(['action-box', focused && 'focused', isLoading && 'disabled'])}>
67
- <ButtonAgent />
68
- <AdaptiveTextArea
69
- ref={textAreaRef}
70
- placeholder={agentLabel && interpolate(t.placeholder, agentLabel)}
71
- onChange={e => chat.set('nextMessage', e.target.value)}
72
- value={value}
73
- onFocus={() => setFocused(true)}
74
- onBlur={() => setFocused(false)}
75
- onKeyDown={onKeyDown}
76
- onKeyUp={handleKeyUp}
77
- onIncreaseSize={() => setExpanded(false)}
78
- onResetSize={() => !expansionLocked.current && setExpanded(true)}
79
- maxHeight={isMinimized ? MIN_INPUT_HEIGHT : MAX_INPUT_HEIGHT}
80
- />
106
+ <UploadProvider value={chat.uploadManager}>
107
+ <MessageInputBox aria-busy={isLoading} className="message-input" $inputFocused={focused}>
108
+ <div className="wrapper-action">
109
+ <QuickCommandSelector inputRef={textAreaRef} isTrial={isTrial} />
110
+ <AgentSelector inputRef={textAreaRef} isTrial={isTrial} />
111
+ <div className={listToClass(['action-box', focused && 'focused', isLoading && 'disabled'])}>
112
+ <ButtonAgent />
113
+ <AdaptiveTextArea
114
+ ref={textAreaRef}
115
+ placeholder={agentLabel && interpolate(t.placeholder, agentLabel)}
116
+ onChange={e => chat.set('nextMessage', e.target.value)}
117
+ value={value}
118
+ onFocus={() => setFocused(true)}
119
+ onBlur={() => setFocused(false)}
120
+ onKeyDown={onKeyDown}
121
+ onKeyUp={handleKeyUp}
122
+ onIncreaseSize={() => setExpanded(false)}
123
+ onResetSize={() => !expansionLocked.current && setExpanded(true)}
124
+ maxHeight={isMinimized ? MIN_INPUT_HEIGHT : MAX_INPUT_HEIGHT}
125
+ />
126
+ </div>
81
127
  </div>
82
- </div>
83
- <ProgressBar visible={true} animate={isLoading}
84
- backgroundColor={isLoading || !focused ? theme.color.light[500] : theme.color.primary[500]} />
85
- <InfoBar />
86
- <ButtonBar onSend={onSend} isLoading={isLoading} />
87
- </MessageInputBox>
128
+ <ProgressBar visible={true} animate={isLoading}
129
+ backgroundColor={isLoading || !focused ? theme.color.light[500] : theme.color.primary[500]} />
130
+ <ContextBar />
131
+ <UploadBar />
132
+ <ButtonBar onSend={onSend} isLoading={isLoading} />
133
+ </MessageInputBox>
134
+ </UploadProvider>
88
135
  )
89
136
  }
90
-
91
-