@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.
- package/CHANGELOG.md +7 -0
- package/dist/app-metadata.json +3 -3
- package/dist/chat-interceptors/send-message.js +3 -3
- package/dist/chat-interceptors/send-message.js.map +1 -1
- package/dist/components/FileDescription.d.ts +10 -0
- package/dist/components/FileDescription.d.ts.map +1 -0
- package/dist/components/FileDescription.js +85 -0
- package/dist/components/FileDescription.js.map +1 -0
- package/dist/state/ChatEntry.d.ts +9 -0
- package/dist/state/ChatEntry.d.ts.map +1 -1
- package/dist/state/ChatEntry.js.map +1 -1
- package/dist/state/ChatState.d.ts +5 -0
- package/dist/state/ChatState.d.ts.map +1 -1
- package/dist/state/ChatState.js +6 -0
- package/dist/state/ChatState.js.map +1 -1
- package/dist/state/constants.d.ts +5 -0
- package/dist/state/constants.d.ts.map +1 -0
- package/dist/state/constants.js +9 -0
- package/dist/state/constants.js.map +1 -0
- package/dist/state/types.d.ts +4 -0
- package/dist/state/types.d.ts.map +1 -1
- package/dist/utils/chat.d.ts +2 -1
- package/dist/utils/chat.d.ts.map +1 -1
- package/dist/utils/chat.js +2 -1
- package/dist/utils/chat.js.map +1 -1
- package/dist/utils/upload/FileUpload.d.ts +21 -0
- package/dist/utils/upload/FileUpload.d.ts.map +1 -0
- package/dist/utils/upload/FileUpload.js +55 -0
- package/dist/utils/upload/FileUpload.js.map +1 -0
- package/dist/utils/upload/UploadManager.d.ts +40 -0
- package/dist/utils/upload/UploadManager.d.ts.map +1 -0
- package/dist/utils/upload/UploadManager.js +131 -0
- package/dist/utils/upload/UploadManager.js.map +1 -0
- package/dist/utils/upload/context.d.ts +15 -0
- package/dist/utils/upload/context.d.ts.map +1 -0
- package/dist/utils/upload/context.js +37 -0
- package/dist/utils/upload/context.js.map +1 -0
- package/dist/utils/upload/errors.d.ts +17 -0
- package/dist/utils/upload/errors.d.ts.map +1 -0
- package/dist/utils/upload/errors.js +27 -0
- package/dist/utils/upload/errors.js.map +1 -0
- package/dist/utils/upload/types.d.ts +7 -0
- package/dist/utils/upload/types.d.ts.map +1 -0
- package/dist/utils/upload/types.js +2 -0
- package/dist/utils/upload/types.js.map +1 -0
- package/dist/utils/upload/utils.d.ts +4 -0
- package/dist/utils/upload/utils.d.ts.map +1 -0
- package/dist/utils/upload/utils.js +10 -0
- package/dist/utils/upload/utils.js.map +1 -0
- package/dist/views/Chat/AgentInfo.d.ts +2 -1
- package/dist/views/Chat/AgentInfo.d.ts.map +1 -1
- package/dist/views/Chat/AgentInfo.js +2 -2
- package/dist/views/Chat/AgentInfo.js.map +1 -1
- package/dist/views/Chat/ChatMessage.d.ts +1 -1
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +24 -5
- package/dist/views/Chat/ChatMessage.js.map +1 -1
- package/dist/views/Chat/styled.d.ts.map +1 -1
- package/dist/views/Chat/styled.js +15 -1
- package/dist/views/Chat/styled.js.map +1 -1
- package/dist/views/MessageInput/{InfoBar.d.ts → ContextBar.d.ts} +2 -2
- package/dist/views/MessageInput/ContextBar.d.ts.map +1 -0
- package/dist/views/MessageInput/{InfoBar.js → ContextBar.js} +5 -5
- package/dist/views/MessageInput/ContextBar.js.map +1 -0
- package/dist/views/MessageInput/SelectContent.d.ts.map +1 -1
- package/dist/views/MessageInput/SelectContent.js +14 -17
- package/dist/views/MessageInput/SelectContent.js.map +1 -1
- package/dist/views/MessageInput/UploadBar.d.ts +2 -0
- package/dist/views/MessageInput/UploadBar.d.ts.map +1 -0
- package/dist/views/MessageInput/UploadBar.js +47 -0
- package/dist/views/MessageInput/UploadBar.js.map +1 -0
- package/dist/views/MessageInput/dictionary.d.ts +1 -1
- package/dist/views/MessageInput/dictionary.d.ts.map +1 -1
- package/dist/views/MessageInput/dictionary.js +18 -4
- package/dist/views/MessageInput/dictionary.js.map +1 -1
- package/dist/views/MessageInput/index.d.ts.map +1 -1
- package/dist/views/MessageInput/index.js +46 -5
- package/dist/views/MessageInput/index.js.map +1 -1
- package/dist/views/MessageInput/styled.d.ts.map +1 -1
- package/dist/views/MessageInput/styled.js +56 -27
- package/dist/views/MessageInput/styled.js.map +1 -1
- package/dist/views/Steps/dictionary.d.ts +1 -1
- package/package.json +2 -2
- package/src/app-metadata.json +3 -3
- package/src/chat-interceptors/send-message.ts +3 -3
- package/src/components/FileDescription.tsx +114 -0
- package/src/state/ChatEntry.ts +10 -0
- package/src/state/ChatState.ts +6 -0
- package/src/state/constants.ts +12 -0
- package/src/state/types.ts +5 -0
- package/src/utils/chat.ts +3 -1
- package/src/utils/upload/FileUpload.ts +64 -0
- package/src/utils/upload/UploadManager.ts +147 -0
- package/src/utils/upload/context.tsx +44 -0
- package/src/utils/upload/errors.ts +34 -0
- package/src/utils/upload/types.ts +7 -0
- package/src/utils/upload/utils.ts +12 -0
- package/src/views/Chat/AgentInfo.tsx +3 -2
- package/src/views/Chat/ChatMessage.tsx +48 -13
- package/src/views/Chat/styled.ts +15 -1
- package/src/views/MessageInput/{InfoBar.tsx → ContextBar.tsx} +9 -9
- package/src/views/MessageInput/SelectContent.tsx +17 -21
- package/src/views/MessageInput/UploadBar.tsx +69 -0
- package/src/views/MessageInput/dictionary.ts +18 -4
- package/src/views/MessageInput/index.tsx +77 -32
- package/src/views/MessageInput/styled.ts +56 -27
- package/dist/views/MessageInput/InfoBar.d.ts.map +0 -1
- 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
|
-
|
|
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}`}
|
|
323
|
+
<div className={`user-info ${entry.agentType}`}><UserInfo entry={entry} /></div>
|
|
295
324
|
{beforeMessage && createElement(beforeMessage, { message })}
|
|
296
|
-
{(entry.content || entry.steps
|
|
297
|
-
{
|
|
298
|
-
{entry.badges
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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>
|
package/src/views/Chat/styled.ts
CHANGED
|
@@ -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
|
|
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
|
|
19
|
-
<Badge appearance="square" palette={color} className="
|
|
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
|
|
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}><
|
|
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
|
-
<
|
|
86
|
+
<ContextBadge label={t.selected} dismiss={t.removeSelection} color="blue" onDismiss={removeCodeSelection} />
|
|
87
87
|
</li>
|
|
88
88
|
)}
|
|
89
89
|
{currentStack && (
|
|
90
90
|
<li>
|
|
91
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
67
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
|
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
|
-
<
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|