@stack-spot/ai-chat-widget 1.24.3 → 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 +14 -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 +26 -7
- 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 +23 -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 +139 -113
- package/src/views/Chat/styled.ts +23 -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)
|
|
@@ -259,9 +268,26 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
|
|
|
259
268
|
setTimeout(() => setCopied(false), 2000)
|
|
260
269
|
}
|
|
261
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
|
+
|
|
262
287
|
const renderContent = () => {
|
|
263
288
|
if (entry.type === 'md') {
|
|
264
289
|
return <>
|
|
290
|
+
{renderUploads()}
|
|
265
291
|
<Markdown onCopyCode={(code) => onCopyCode(code, entry.messageId ?? '', chat)}>{entry.content}</Markdown>
|
|
266
292
|
{renderActions()}
|
|
267
293
|
</>
|
|
@@ -269,6 +295,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
|
|
|
269
295
|
if (entry.type === 'text') {
|
|
270
296
|
return <>
|
|
271
297
|
<p className="plain-text">{entry.content}</p>
|
|
298
|
+
{renderUploads()}
|
|
272
299
|
{renderActions()}
|
|
273
300
|
</>
|
|
274
301
|
}
|
|
@@ -287,133 +314,132 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage }: Pr
|
|
|
287
314
|
}
|
|
288
315
|
|
|
289
316
|
|
|
290
|
-
return (entry.content || entry.error || !!entry.steps?.length) && (
|
|
291
|
-
<li key={entry.messageId} className={entry.agentType} ref={ref}
|
|
292
|
-
|
|
293
|
-
<div className="chat-message" ref={chatRef} onKeyDown={handleKeyDown} tabIndex={0}
|
|
294
|
-
onMouseEnter={entry.agentType === 'user' ? () => setShowUserButtonCopy(true) : undefined}
|
|
295
|
-
onMouseLeave={entry.agentType === 'user' ? () => setShowUserButtonCopy(false) : undefined}>
|
|
296
|
-
<div className={`user-info ${entry.agentType}`}>{userInfo}</div>
|
|
297
|
-
{beforeMessage && createElement(beforeMessage, { message })}
|
|
298
|
-
{(entry.content || entry.steps) && <div className={listToClass(['message-content', entry.card && 'card', entry.type])}>
|
|
299
|
-
{!!entry.badges?.length && <div className="badges">
|
|
300
|
-
{entry.badges.map((b, index) => <Badge key={index} palette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
|
|
301
|
-
</div>}
|
|
302
|
-
{renderContent()}
|
|
303
|
-
|
|
304
|
-
{!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
|
|
305
|
-
</div>}
|
|
306
|
-
|
|
307
|
-
{entry.error && (
|
|
308
|
-
<div className="error">
|
|
309
|
-
<IconBox size="xs"><TimesCircle /></IconBox>
|
|
310
|
-
<Text appearance="microtext1">{entry.error}</Text>
|
|
311
|
-
</div>
|
|
312
|
-
)}
|
|
313
|
-
</div>
|
|
314
|
-
{afterMessage && createElement(afterMessage, { message })}
|
|
315
|
-
{!!entry.tools?.length && <StackedBadge
|
|
316
|
-
aria-label={t.openToolsPanel}
|
|
317
|
-
title={t.openToolsPanel}
|
|
318
|
-
tabIndex={0}
|
|
319
|
-
role="button"
|
|
320
|
-
className="tools-badge"
|
|
321
|
-
label={t.tools}
|
|
322
|
-
images={entry.tools.slice(0, 3).map((id) => {
|
|
323
|
-
const tool = toolById(id, agent?.toolkits)
|
|
324
|
-
return { key: id, name: tool?.name || id, icon: <Cog />, url: tool?.image }
|
|
325
|
-
})}
|
|
326
|
-
onClick={openToolsPanel}
|
|
327
|
-
/>}
|
|
328
|
-
{!!entry.knowledgeSources?.length && <div className="ks-box">
|
|
329
|
-
<Text appearance="microtext1" colorScheme="light.700">Knowledge Sources:</Text>
|
|
330
|
-
<ul>{entry.knowledgeSources.map((ks, index) => (
|
|
331
|
-
<li key={index}>
|
|
332
|
-
<Button size="sm" colorScheme="light" onClick={() => detailKS(ks)}>{ks.name}</Button>
|
|
333
|
-
</li>
|
|
334
|
-
))}</ul>
|
|
335
|
-
</div>}
|
|
336
|
-
|
|
337
|
-
{shouldShowFooter && <div className="message-footer"
|
|
317
|
+
return (entry.content || entry.error || !!entry.steps?.length || entry.upload?.length) && (
|
|
318
|
+
<li key={entry.messageId} className={entry.agentType} ref={ref}>
|
|
319
|
+
<div className="chat-message-container"
|
|
338
320
|
onMouseEnter={entry.agentType === 'user' ? () => setShowUserButtonCopy(true) : undefined}
|
|
339
321
|
onMouseLeave={entry.agentType === 'user' ? () => setShowUserButtonCopy(false) : undefined}>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
{entry.
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
>
|
|
353
|
-
<Check />
|
|
354
|
-
</IconButton>
|
|
355
|
-
</Tooltip>
|
|
356
|
-
) : (
|
|
357
|
-
<IconButton
|
|
358
|
-
appearance="square"
|
|
359
|
-
color="light"
|
|
360
|
-
title={t.copy}
|
|
361
|
-
aria-label={t.copy}
|
|
362
|
-
onClick={handleCopy}
|
|
363
|
-
>
|
|
364
|
-
<Copy />
|
|
365
|
-
</IconButton>
|
|
366
|
-
)
|
|
322
|
+
<div className="chat-message" ref={chatRef} onKeyDown={handleKeyDown} tabIndex={0}>
|
|
323
|
+
<div className={`user-info ${entry.agentType}`}><UserInfo entry={entry} /></div>
|
|
324
|
+
{beforeMessage && createElement(beforeMessage, { message })}
|
|
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>
|
|
367
334
|
)}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
</
|
|
373
|
-
|
|
374
|
-
{liked === false ? <DislikeFill /> : <Dislike />}
|
|
375
|
-
</IconButton>
|
|
376
|
-
</>
|
|
335
|
+
|
|
336
|
+
{entry.error && (
|
|
337
|
+
<div className="error">
|
|
338
|
+
<IconBox size="xs"><TimesCircle /></IconBox>
|
|
339
|
+
<Text appearance="microtext1">{entry.error}</Text>
|
|
340
|
+
</div>
|
|
377
341
|
)}
|
|
342
|
+
</div>
|
|
343
|
+
{afterMessage && createElement(afterMessage, { message })}
|
|
344
|
+
{!!entry.tools?.length && <StackedBadge
|
|
345
|
+
aria-label={t.openToolsPanel}
|
|
346
|
+
title={t.openToolsPanel}
|
|
347
|
+
tabIndex={0}
|
|
348
|
+
role="button"
|
|
349
|
+
className="tools-badge"
|
|
350
|
+
label={t.tools}
|
|
351
|
+
images={entry.tools.slice(0, 3).map((id) => {
|
|
352
|
+
const tool = toolById(id, agent?.toolkits)
|
|
353
|
+
return { key: id, name: tool?.name || id, icon: <Cog />, url: tool?.image }
|
|
354
|
+
})}
|
|
355
|
+
onClick={openToolsPanel}
|
|
356
|
+
/>}
|
|
357
|
+
{!!entry.knowledgeSources?.length && <div className="ks-box">
|
|
358
|
+
<Text appearance="microtext1" colorScheme="light.700">Knowledge Sources:</Text>
|
|
359
|
+
<ul>{entry.knowledgeSources.map((ks, index) => (
|
|
360
|
+
<li key={index}>
|
|
361
|
+
<Button size="sm" colorScheme="light" onClick={() => detailKS(ks)}>{ks.name}</Button>
|
|
362
|
+
</li>
|
|
363
|
+
))}</ul>
|
|
378
364
|
</div>}
|
|
379
|
-
|
|
380
|
-
{
|
|
381
|
-
<div className="message-actions">
|
|
382
|
-
{
|
|
383
|
-
|
|
365
|
+
|
|
366
|
+
{shouldShowFooter && <div className="message-footer">
|
|
367
|
+
{entry.agentType === 'bot' && !entry.error && <div className="message-actions">
|
|
368
|
+
{entry.type === 'md' && (
|
|
369
|
+
copied ? (
|
|
370
|
+
<Tooltip text={t.copied} position={'right'}>
|
|
371
|
+
<IconButton
|
|
372
|
+
appearance="square"
|
|
373
|
+
colorBg="light"
|
|
374
|
+
title={t.copied}
|
|
375
|
+
aria-label={t.copied}
|
|
376
|
+
onClick={handleCopy}
|
|
377
|
+
>
|
|
378
|
+
<Check />
|
|
379
|
+
</IconButton>
|
|
380
|
+
</Tooltip>
|
|
381
|
+
) : (
|
|
384
382
|
<IconButton
|
|
385
383
|
appearance="square"
|
|
386
|
-
|
|
387
|
-
title={t.
|
|
388
|
-
aria-label={t.
|
|
389
|
-
|
|
384
|
+
color="light"
|
|
385
|
+
title={t.copy}
|
|
386
|
+
aria-label={t.copy}
|
|
387
|
+
onClick={handleCopy}
|
|
390
388
|
>
|
|
391
|
-
<
|
|
389
|
+
<Copy />
|
|
390
|
+
</IconButton>
|
|
391
|
+
)
|
|
392
|
+
)}
|
|
393
|
+
{entry.messageId && (
|
|
394
|
+
<>
|
|
395
|
+
<IconButton appearance="square" color="light" title={t.like} aria-label={t.like} onClick={like}>
|
|
396
|
+
{liked === true ? <LikeFill /> : <Like />}
|
|
397
|
+
</IconButton>
|
|
398
|
+
<IconButton appearance="square" color="light" title={t.dislike} aria-label={t.dislike} onClick={dislike}>
|
|
399
|
+
{liked === false ? <DislikeFill /> : <Dislike />}
|
|
392
400
|
</IconButton>
|
|
393
|
-
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
|
|
401
|
+
</>
|
|
402
|
+
)}
|
|
403
|
+
</div>}
|
|
404
|
+
|
|
405
|
+
{entry.agentType === 'user' && (
|
|
406
|
+
<div className="message-actions">
|
|
407
|
+
{copied ? (
|
|
408
|
+
<Tooltip text={t.copied} position={'left'}>
|
|
397
409
|
<IconButton
|
|
398
410
|
appearance="square"
|
|
399
|
-
|
|
400
|
-
title={t.
|
|
401
|
-
aria-label={t.
|
|
402
|
-
onClick={handleCopy}
|
|
411
|
+
colorBg="light"
|
|
412
|
+
title={t.copied}
|
|
413
|
+
aria-label={t.copied}
|
|
403
414
|
size="sm"
|
|
404
415
|
>
|
|
405
|
-
<
|
|
416
|
+
<Check />
|
|
406
417
|
</IconButton>
|
|
407
|
-
</
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
418
|
+
</Tooltip>
|
|
419
|
+
) : (
|
|
420
|
+
showUserButtonCopy && (
|
|
421
|
+
<div className="action-bar">
|
|
422
|
+
<IconButton
|
|
423
|
+
appearance="square"
|
|
424
|
+
color="light"
|
|
425
|
+
title={t.copy}
|
|
426
|
+
aria-label={t.copy}
|
|
427
|
+
onClick={handleCopy}
|
|
428
|
+
size="sm"
|
|
429
|
+
>
|
|
430
|
+
<Copy className="copy-btn"/>
|
|
431
|
+
</IconButton>
|
|
432
|
+
</div>
|
|
433
|
+
)
|
|
434
|
+
)}
|
|
435
|
+
</div>
|
|
436
|
+
)}
|
|
412
437
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
438
|
+
<Text as="label" appearance="microtext1" className="chat-date">
|
|
439
|
+
{dateFormatter.formatForChatMessage(date)}
|
|
440
|
+
</Text>
|
|
441
|
+
</div>}
|
|
442
|
+
</div>
|
|
417
443
|
</li>
|
|
418
444
|
)
|
|
419
445
|
}
|
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
|
}
|
|
@@ -63,6 +63,7 @@ export const ChatList: IStyledComponentBase<
|
|
|
63
63
|
flex-direction: row;
|
|
64
64
|
align-items: center;
|
|
65
65
|
gap: 8px;
|
|
66
|
+
margin-top: 10px;
|
|
66
67
|
|
|
67
68
|
.message-actions {
|
|
68
69
|
display: flex;
|
|
@@ -138,12 +139,33 @@ export const ChatList: IStyledComponentBase<
|
|
|
138
139
|
background: linear-gradient(180deg, ${theme.color.blue[500]} 0%, ${theme.color.indigo[500]} 100%);
|
|
139
140
|
}
|
|
140
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
|
+
}
|
|
141
156
|
}
|
|
142
157
|
|
|
143
158
|
&.user {
|
|
144
159
|
align-items: end;
|
|
145
160
|
margin-left: 40px;
|
|
146
161
|
|
|
162
|
+
.chat-message-container {
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-direction: column;
|
|
165
|
+
max-width: 100%;
|
|
166
|
+
align-items: end;
|
|
167
|
+
}
|
|
168
|
+
|
|
147
169
|
.chat-message {
|
|
148
170
|
display: flex;
|
|
149
171
|
flex-direction: row;
|
|
@@ -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
|
|