@stack-spot/ai-chat-widget 1.24.4 → 1.25.1-beta.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/AgentCard/dictionary.d.ts +4 -2
- package/dist/components/AgentCard/dictionary.d.ts.map +1 -1
- package/dist/components/AgentCard/dictionary.js +4 -2
- package/dist/components/AgentCard/dictionary.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/components/Selector/index.d.ts +2 -2
- package/dist/components/Selector/index.d.ts.map +1 -1
- package/dist/components/Selector/index.js +2 -2
- package/dist/components/Selector/index.js.map +1 -1
- 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 +5 -1
- 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/tools.d.ts +2 -2
- package/dist/utils/tools.d.ts.map +1 -1
- package/dist/utils/tools.js +3 -6
- package/dist/utils/tools.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/Agents/AgentDescription.d.ts +2 -9
- package/dist/views/Agents/AgentDescription.d.ts.map +1 -1
- package/dist/views/Agents/AgentDescription.js +11 -9
- package/dist/views/Agents/AgentDescription.js.map +1 -1
- package/dist/views/Agents/AgentsPanel.d.ts.map +1 -1
- package/dist/views/Agents/AgentsPanel.js +11 -11
- package/dist/views/Agents/AgentsPanel.js.map +1 -1
- package/dist/views/Agents/AgentsTab.d.ts +2 -2
- package/dist/views/Agents/AgentsTab.d.ts.map +1 -1
- package/dist/views/Agents/AgentsTab.js +4 -4
- package/dist/views/Agents/AgentsTab.js.map +1 -1
- package/dist/views/Agents/useAgentFavorites.d.ts +1 -1
- package/dist/views/Agents/useAgentFavorites.js +4 -4
- package/dist/views/Agents/useAgentFavorites.js.map +1 -1
- 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 +27 -8
- 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/ChatHistory/HistoryItem.d.ts.map +1 -1
- package/dist/views/ChatHistory/HistoryItem.js +8 -5
- package/dist/views/ChatHistory/HistoryItem.js.map +1 -1
- package/dist/views/ChatHistory/utils.d.ts +0 -6
- package/dist/views/ChatHistory/utils.d.ts.map +1 -1
- package/dist/views/ChatHistory/utils.js +1 -16
- package/dist/views/ChatHistory/utils.js.map +1 -1
- package/dist/views/Home/CustomAgent.js +3 -3
- package/dist/views/Home/CustomAgent.js.map +1 -1
- package/dist/views/MessageInput/AgentSelector.js +4 -4
- package/dist/views/MessageInput/AgentSelector.js.map +1 -1
- package/dist/views/MessageInput/ButtonAgent.js +2 -2
- package/dist/views/MessageInput/ButtonAgent.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/dist/views/Tools.js +3 -3
- package/dist/views/Tools.js.map +1 -1
- package/dist/views/Workspaces/WorkspacesTab.js +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/AgentCard/dictionary.ts +4 -2
- package/src/components/FileDescription.tsx +114 -0
- package/src/components/Selector/index.tsx +4 -5
- 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 +6 -1
- package/src/utils/chat.ts +3 -1
- package/src/utils/tools.ts +5 -7
- 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/Agents/AgentDescription.tsx +18 -25
- package/src/views/Agents/AgentsPanel.tsx +11 -12
- package/src/views/Agents/AgentsTab.tsx +8 -16
- package/src/views/Agents/useAgentFavorites.ts +4 -4
- package/src/views/Chat/AgentInfo.tsx +3 -2
- package/src/views/Chat/ChatMessage.tsx +51 -16
- package/src/views/Chat/styled.ts +15 -1
- package/src/views/ChatHistory/HistoryItem.tsx +10 -5
- package/src/views/ChatHistory/utils.ts +1 -18
- package/src/views/Home/CustomAgent.tsx +4 -4
- package/src/views/MessageInput/AgentSelector.tsx +4 -4
- package/src/views/MessageInput/ButtonAgent.tsx +2 -2
- 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/src/views/Tools.tsx +4 -3
- package/src/views/Workspaces/WorkspacesTab.tsx +1 -1
- package/dist/views/MessageInput/InfoBar.d.ts.map +0 -1
- package/dist/views/MessageInput/InfoBar.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stack-spot/ai-chat-widget",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.1-beta.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"@citric/core": "^6.4.0",
|
|
14
14
|
"@stack-spot/portal-components": "^2.22.1",
|
|
15
15
|
"@citric/icons": "^5.13.0",
|
|
16
|
-
"@stack-spot/portal-network": "^
|
|
16
|
+
"@stack-spot/portal-network": "^139.1-beta.0",
|
|
17
17
|
"@citric/ui": "^6.10.2",
|
|
18
18
|
"@stack-spot/portal-theme": "^1.0.0",
|
|
19
19
|
"@stack-spot/portal-translate": "^1.1.0",
|
package/src/app-metadata.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stack-spot/ai-chat-widget",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"date": "
|
|
3
|
+
"version": "1.25.1-beta.0",
|
|
4
|
+
"date": "Wed Jun 11 2025 07:19:45 GMT-0300 (Brasilia Standard Time)",
|
|
5
5
|
"dependencies": [
|
|
6
6
|
{
|
|
7
7
|
"name": "@stack-spot/app-metadata",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "@stack-spot/portal-network",
|
|
116
|
-
"version": "0.
|
|
116
|
+
"version": "0.139.0(@stack-spot/auth@5.3.2)(@stack-spot/opa@2.5.0(@stack-spot/auth@5.3.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@stack-spot/portal-translate@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@tanstack/react-query@5.59.16(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"name": "@stack-spot/portal-theme",
|
|
@@ -36,7 +36,7 @@ function createEntryValueFromChatResponse(
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function buildPrompt(content: string, data?: any) {
|
|
39
|
-
if (data === undefined || data === '' || data === null) return content
|
|
39
|
+
if (data === undefined || data === '' || data === null) return content || '.' // prevents empty strings
|
|
40
40
|
return typeof data === 'string' ? data : JSON.stringify(data)
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -53,12 +53,12 @@ function buildPrompt(content: string, data?: any) {
|
|
|
53
53
|
export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState, signal: AbortSignal) {
|
|
54
54
|
const { agentType, content, data } = entry.getValue()
|
|
55
55
|
if (agentType !== 'user') return
|
|
56
|
-
const context = buildConversationContext(chat)
|
|
56
|
+
const context = buildConversationContext(chat, entry)
|
|
57
57
|
chat.set('isLoading', true)
|
|
58
58
|
const untitled = chat.untitled
|
|
59
59
|
const isFirstMessage = chat.getMessages().length === 1
|
|
60
60
|
if (untitled) {
|
|
61
|
-
chat.set('label', content)
|
|
61
|
+
chat.set('label', content || entry.getValue().upload?.[0]?.name || 'Chat')
|
|
62
62
|
chat.untitled = false
|
|
63
63
|
}
|
|
64
64
|
const stream = aiClient.sendChatMessage({ context, user_prompt: buildPrompt(content, data) })
|
|
@@ -2,7 +2,8 @@ import { Dictionary } from '@stack-spot/portal-translate'
|
|
|
2
2
|
|
|
3
3
|
export const dictionary = {
|
|
4
4
|
en: {
|
|
5
|
-
|
|
5
|
+
'built_in': 'Embutido',
|
|
6
|
+
workspace: 'Spot',
|
|
6
7
|
personal: 'Personal',
|
|
7
8
|
account: 'Account',
|
|
8
9
|
shared: 'Shared',
|
|
@@ -10,7 +11,8 @@ export const dictionary = {
|
|
|
10
11
|
create: 'Create',
|
|
11
12
|
},
|
|
12
13
|
pt: {
|
|
13
|
-
|
|
14
|
+
'built_in': 'Embutido',
|
|
15
|
+
workspace: 'Spot',
|
|
14
16
|
personal: 'Personal',
|
|
15
17
|
account: 'Conta',
|
|
16
18
|
shared: 'Compartilhado',
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { IconBox, Text } from '@citric/core'
|
|
2
|
+
import { Document, Sync, TimesMini } from '@citric/icons'
|
|
3
|
+
import { IconButton, LoadingCircular } from '@citric/ui'
|
|
4
|
+
import { theme } from '@stack-spot/portal-theme'
|
|
5
|
+
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
6
|
+
import { styled } from 'styled-components'
|
|
7
|
+
import { FileUploadStatus } from '../utils/upload/types'
|
|
8
|
+
|
|
9
|
+
export interface FileDescriptionProps {
|
|
10
|
+
fileName: string,
|
|
11
|
+
icon?: React.ReactNode,
|
|
12
|
+
status?: FileUploadStatus,
|
|
13
|
+
onRemove?: () => void,
|
|
14
|
+
onRetry?: () => void,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const Styled = styled.div`
|
|
18
|
+
padding: 4px;
|
|
19
|
+
border-radius: 4px;
|
|
20
|
+
display: flex;
|
|
21
|
+
gap: 8px;
|
|
22
|
+
background-color: ${theme.color.light[600]};
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
&.error {
|
|
25
|
+
border: 1px solid ${theme.color.danger[500]};
|
|
26
|
+
}
|
|
27
|
+
&.pending .status {
|
|
28
|
+
cursor: progress;
|
|
29
|
+
}
|
|
30
|
+
.image {
|
|
31
|
+
border-radius: 4px;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
background-color: ${theme.color.light[500]};
|
|
34
|
+
width: 28px;
|
|
35
|
+
aspect-ratio: 1;
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: center;
|
|
39
|
+
position: relative;
|
|
40
|
+
img {
|
|
41
|
+
width: 100%;
|
|
42
|
+
}
|
|
43
|
+
.status {
|
|
44
|
+
position: absolute;
|
|
45
|
+
background-color: rgba(0, 0, 0, 0.3);
|
|
46
|
+
top: 0;
|
|
47
|
+
left: 0;
|
|
48
|
+
bottom: 0;
|
|
49
|
+
right: 0;
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
justify-content: center;
|
|
53
|
+
button {
|
|
54
|
+
padding: 2px;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
.details {
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
small {
|
|
63
|
+
max-width: 100px;
|
|
64
|
+
white-space: nowrap;
|
|
65
|
+
text-overflow: ellipsis;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
line-height: 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
button {
|
|
71
|
+
padding: 0;
|
|
72
|
+
margin: -2px -2px 0 0;
|
|
73
|
+
}
|
|
74
|
+
`
|
|
75
|
+
|
|
76
|
+
export const FileDescription = ({ fileName, icon, status, onRemove, onRetry }: FileDescriptionProps) => {
|
|
77
|
+
const t = useTranslate(dictionary)
|
|
78
|
+
const [, name, extension] = fileName.match(/(.+)\.([^.]+)$/) ?? ['', 'Unknown', 'Unknown']
|
|
79
|
+
return (
|
|
80
|
+
<Styled className={status} aria-busy={status === 'pending'}>
|
|
81
|
+
<div className="image">
|
|
82
|
+
{icon ?? <IconBox><Document /></IconBox>}
|
|
83
|
+
{status === 'pending' && <div className="status" aria-label={t.loading}>
|
|
84
|
+
<LoadingCircular size="xs" />
|
|
85
|
+
</div>}
|
|
86
|
+
{status === 'error' && <div className="status" aria-label={t.error}>
|
|
87
|
+
<IconButton onClick={onRetry}><Sync /></IconButton>
|
|
88
|
+
</div>}
|
|
89
|
+
</div>
|
|
90
|
+
<div className="details">
|
|
91
|
+
<Text appearance="microtext1" className="name" title={name}>{name}</Text>
|
|
92
|
+
<Text appearance="microtext1" colorScheme="light.700">{extension.toUpperCase()}</Text>
|
|
93
|
+
</div>
|
|
94
|
+
{onRemove && <IconButton onClick={onRemove} title={t.remove} arial-label={`${t.remove} ${name}`}>
|
|
95
|
+
<TimesMini />
|
|
96
|
+
</IconButton>}
|
|
97
|
+
</Styled>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const dictionary = {
|
|
102
|
+
en: {
|
|
103
|
+
loading: 'Uploading file',
|
|
104
|
+
retry: 'Retry',
|
|
105
|
+
remove: 'Remove',
|
|
106
|
+
error: 'Error while uploading file',
|
|
107
|
+
},
|
|
108
|
+
pt: {
|
|
109
|
+
loading: 'Carregando arquivo',
|
|
110
|
+
retry: 'Tentar novamente',
|
|
111
|
+
remove: 'Remover',
|
|
112
|
+
error: 'Erro ao carregar o arquivo',
|
|
113
|
+
},
|
|
114
|
+
} satisfies Dictionary
|
|
@@ -2,7 +2,7 @@ import { IconBox, Text } from '@citric/core'
|
|
|
2
2
|
import { ExternalLink } from '@citric/icons'
|
|
3
3
|
import { IconButton } from '@citric/ui'
|
|
4
4
|
import { useKeyboardControls } from '@stack-spot/portal-components'
|
|
5
|
-
import {
|
|
5
|
+
import { AgentVisibilityLevel } from '@stack-spot/portal-network'
|
|
6
6
|
import { Dictionary, interpolate, useTranslate } from '@stack-spot/portal-translate'
|
|
7
7
|
import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
8
8
|
import { useCurrentChatState } from '../../context/hooks'
|
|
@@ -12,8 +12,7 @@ import { Fading } from '../Fading'
|
|
|
12
12
|
import { FallbackBoundary } from '../FallbackBoundary'
|
|
13
13
|
import { SelectorBox } from './styled'
|
|
14
14
|
|
|
15
|
-
type SectionVisibility =
|
|
16
|
-
|
|
15
|
+
type SectionVisibility = AgentVisibilityLevel
|
|
17
16
|
type SelectorShortcut = '/' | '@'
|
|
18
17
|
|
|
19
18
|
interface Item {
|
|
@@ -268,7 +267,7 @@ const dictionary = {
|
|
|
268
267
|
personal: 'Personal',
|
|
269
268
|
account: 'Account',
|
|
270
269
|
shared: 'Shared',
|
|
271
|
-
'
|
|
270
|
+
'built_in': 'Built-in',
|
|
272
271
|
workspace: 'Spot',
|
|
273
272
|
error: 'Could not load the $0s.',
|
|
274
273
|
noData: 'You don\'t have any $0 yet.',
|
|
@@ -282,7 +281,7 @@ const dictionary = {
|
|
|
282
281
|
account: 'Conta',
|
|
283
282
|
shared: 'Compartilhado',
|
|
284
283
|
workspace: 'Spot',
|
|
285
|
-
'
|
|
284
|
+
'built_in': 'Embutido',
|
|
286
285
|
error: 'Não foi possível carregar os $0s.',
|
|
287
286
|
noData: 'Você ainda não possui $0s.',
|
|
288
287
|
noResults: 'Não $0s para mostrar aqui.',
|
package/src/state/ChatEntry.ts
CHANGED
|
@@ -65,6 +65,12 @@ export interface ChatTool {
|
|
|
65
65
|
description: string,
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export interface UploadedFile {
|
|
69
|
+
id: string,
|
|
70
|
+
name: string,
|
|
71
|
+
image?: string,
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
export interface TextChatEntry {
|
|
69
75
|
/**
|
|
70
76
|
* "text" for simple unformatted paragraphs. "md" for markdown.
|
|
@@ -165,6 +171,10 @@ export interface TextChatEntry {
|
|
|
165
171
|
* Valid for input types.
|
|
166
172
|
*/
|
|
167
173
|
initialValue?: string[],
|
|
174
|
+
/**
|
|
175
|
+
* If this chat entry contains any uploaded file, these are the files being uploaded along the message.
|
|
176
|
+
*/
|
|
177
|
+
upload?: UploadedFile[],
|
|
168
178
|
}
|
|
169
179
|
|
|
170
180
|
type ChatEntryListener = (value: TextChatEntry) => void
|
package/src/state/ChatState.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { dropRight, last, pull } from 'lodash'
|
|
|
2
2
|
import { ulid } from 'ulid'
|
|
3
3
|
import { AbortedError } from '../AbortedError'
|
|
4
4
|
import { ChatFeatures, getFeaturesWithDefaults } from '../features'
|
|
5
|
+
import { UploadManager } from '../utils/upload/UploadManager'
|
|
5
6
|
import { ChatEntry } from './ChatEntry'
|
|
7
|
+
import { acceptedFileTypes, maxFileSize, maxUploadItems } from './constants'
|
|
6
8
|
import { ObservableState } from './ObservableState'
|
|
7
9
|
import { Labeled, LabeledAgent } from './types'
|
|
8
10
|
|
|
@@ -124,6 +126,10 @@ export class ChatState extends ObservableState<ChatProperties> {
|
|
|
124
126
|
* Abort signals currently active.
|
|
125
127
|
*/
|
|
126
128
|
private abortions: AbortController[] = []
|
|
129
|
+
/**
|
|
130
|
+
* Used to manage uploads for this Chat.
|
|
131
|
+
*/
|
|
132
|
+
readonly uploadManager = new UploadManager({ maxSize: maxFileSize, accept: acceptedFileTypes, maxItems: maxUploadItems })
|
|
127
133
|
untitled: boolean
|
|
128
134
|
|
|
129
135
|
constructor({ id, initial, entries = [], interceptors = [], untitled = false }: Options) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FileSize } from './types'
|
|
2
|
+
|
|
3
|
+
export const acceptedFileTypes = [
|
|
4
|
+
'json', 'yaml', 'txt', 'md', 'json', 'yaml', 'pdf', 'xls', 'xlsx', 'csv', 'cbl', 'cpp', 'cxx', 'cc', 'c', 'hpp', 'hxx', 'hh', 'h', 'cs',
|
|
5
|
+
'go', 'html', 'htm', 'kt', 'kts', 'md', 'php', 'proto', 'py', 'java', 'js', 'jsx', 'ts', 'tsx', 'rst', 'rb', 'rs', 'scala', 'swift',
|
|
6
|
+
'sql', 'yaml', 'yml', 'tf', 'sh', 'ps1', 'psd1', 'psm1', 'bat', 'cmd', 'rego', 'f', 'for', 'r', 'pl', 'vb', 'dart', 'hs', 'lua',
|
|
7
|
+
'asm', 'groovy', 'gvy', 'gy', 'mat', 'clj', 'lisp', 'm', 'cls', 'css', 'scss', 'json', 'jpg', 'jpeg', 'png',
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export const maxFileSize: FileSize = { value: 10, unit: 'MB' }
|
|
11
|
+
|
|
12
|
+
export const maxUploadItems = 5
|
package/src/state/types.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface Labeled {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
export interface LabeledWithImage extends Labeled {
|
|
7
|
-
image?: string,
|
|
7
|
+
image?: string | undefined | null,
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export interface LabeledAgent extends LabeledWithImage {
|
|
@@ -12,3 +12,8 @@ export interface LabeledAgent extends LabeledWithImage {
|
|
|
12
12
|
slug?: string,
|
|
13
13
|
visibility_level?: string,
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
export interface FileSize {
|
|
17
|
+
value: number,
|
|
18
|
+
unit: 'B' | 'KB' | 'MB' | 'GB',
|
|
19
|
+
}
|
package/src/utils/chat.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FixedChatRequest } from '@stack-spot/portal-network'
|
|
2
2
|
import appData from '../app-metadata.json'
|
|
3
|
+
import { ChatEntry } from '../state/ChatEntry'
|
|
3
4
|
import { ChatState } from '../state/ChatState'
|
|
4
5
|
import { defaultLanguage } from './programming-languages'
|
|
5
6
|
|
|
@@ -11,13 +12,14 @@ import { defaultLanguage } from './programming-languages'
|
|
|
11
12
|
* @param state the ChatState to build the context from.
|
|
12
13
|
* @returns the conversation context ready to be sent to the backend.
|
|
13
14
|
*/
|
|
14
|
-
export function buildConversationContext(state: ChatState): FixedChatRequest['context'] {
|
|
15
|
+
export function buildConversationContext(state: ChatState, message?: ChatEntry): FixedChatRequest['context'] {
|
|
15
16
|
return {
|
|
16
17
|
workspace: state.get('workspace')?.id,
|
|
17
18
|
conversation_id: state.id,
|
|
18
19
|
stack_id: state.get('stack')?.id,
|
|
19
20
|
language: state.get('codeLanguage') || (state.get('codeSelection') ? defaultLanguage : undefined),
|
|
20
21
|
knowledge_sources: state.get('knowledgeSources')?.map(ks => ks.id),
|
|
22
|
+
upload_ids: message?.getValue().upload?.map(f => f.id),
|
|
21
23
|
agent_id: state.get('agent')?.id,
|
|
22
24
|
agent_built_in: state.get('agent')?.builtIn,
|
|
23
25
|
os: navigator.userAgent,
|
package/src/utils/tools.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BuiltinToolkitResponse, BuiltinToolResponse } from '@stack-spot/portal-network/api/agent'
|
|
2
|
+
import { keyBy } from 'lodash'
|
|
2
3
|
|
|
3
4
|
export type ToolWithImage = BuiltinToolResponse & { id: string, image?: string }
|
|
4
5
|
|
|
5
|
-
export function toolById(id: string, toolkits:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (tool.id === id) return { ...tool, id, image: toolkit.image_url }
|
|
9
|
-
}
|
|
10
|
-
}
|
|
6
|
+
export function toolById(id: string, toolkits: BuiltinToolkitResponse[] | undefined): ToolWithImage | undefined {
|
|
7
|
+
const tools = toolkits?.map(({ image_url, tools }) => tools?.map((tool) => ({ ...tool, id: tool.id!, image: image_url! }))).flat()
|
|
8
|
+
return keyBy(tools, 'id')[id]
|
|
11
9
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { dataIntegrationClient } from '@stack-spot/portal-network'
|
|
2
|
+
import { FileUploadType } from '@stack-spot/portal-network/api/dataIntegration'
|
|
3
|
+
import { pull } from 'lodash'
|
|
4
|
+
import { FileUploadStatus } from './types'
|
|
5
|
+
import { getFileId } from './utils'
|
|
6
|
+
|
|
7
|
+
export type FileUploadChangeListener = (status: FileUploadStatus) => void
|
|
8
|
+
|
|
9
|
+
export class FileUpload {
|
|
10
|
+
readonly file: File
|
|
11
|
+
status: FileUploadStatus = 'pending'
|
|
12
|
+
readonly id: string
|
|
13
|
+
readonly type: FileUploadType
|
|
14
|
+
error?: any
|
|
15
|
+
uploadId?: string
|
|
16
|
+
private listeners: FileUploadChangeListener[] = []
|
|
17
|
+
private abortController = new AbortController()
|
|
18
|
+
|
|
19
|
+
constructor(file: File, type: FileUploadType = 'CONTEXT') {
|
|
20
|
+
this.file = file
|
|
21
|
+
this.id = getFileId(file)
|
|
22
|
+
this.type = type
|
|
23
|
+
this.upload()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private runListeners() {
|
|
27
|
+
this.listeners.forEach(l => l(this.status))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private async upload() {
|
|
31
|
+
try {
|
|
32
|
+
const [id] = await dataIntegrationClient.uploadFiles.mutate({ files: [this.file], type: this.type }, this.abortController.signal)
|
|
33
|
+
this.uploadId = id
|
|
34
|
+
this.status = 'success'
|
|
35
|
+
} catch (error) {
|
|
36
|
+
this.error = error
|
|
37
|
+
this.status = 'error'
|
|
38
|
+
}
|
|
39
|
+
this.runListeners()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
stop() {
|
|
43
|
+
this.abortController.abort()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
retry() {
|
|
47
|
+
if (this.status != 'error') return
|
|
48
|
+
this.status = 'pending'
|
|
49
|
+
this.runListeners()
|
|
50
|
+
this.upload()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onChange(listener: FileUploadChangeListener) {
|
|
54
|
+
this.listeners.push(listener)
|
|
55
|
+
return () => {
|
|
56
|
+
pull(this.listeners, listener)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
destroy() {
|
|
61
|
+
this.listeners = []
|
|
62
|
+
this.stop()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { pull } from 'lodash'
|
|
2
|
+
import { FileSize } from '../../state/types'
|
|
3
|
+
import { FileAlreadyExists, FileIsTooLarge, MaxFilesReached, UploadError } from './errors'
|
|
4
|
+
import { FileUpload } from './FileUpload'
|
|
5
|
+
import { getFileId, unitPower } from './utils'
|
|
6
|
+
|
|
7
|
+
export type UploadChangeListener = (files: FileUpload[]) => void
|
|
8
|
+
export type UploadStatusChangeListener = (status: UploadManagerStatus) => void
|
|
9
|
+
export type UploadErrorListener = (errors: UploadError[]) => void
|
|
10
|
+
export type UploadManagerStatus = 'idle' | 'uploading' | 'error'
|
|
11
|
+
|
|
12
|
+
export class UploadManager {
|
|
13
|
+
private changeListeners: UploadChangeListener[] = []
|
|
14
|
+
private statusListeners: UploadStatusChangeListener[] = []
|
|
15
|
+
private errorListeners: UploadErrorListener[] = []
|
|
16
|
+
private input: HTMLInputElement | null = null
|
|
17
|
+
private value: FileUpload[] = []
|
|
18
|
+
private maxSize?: FileSize
|
|
19
|
+
private maxItems: number
|
|
20
|
+
private accept: string[]
|
|
21
|
+
private onDestroy: () => void = () => {}
|
|
22
|
+
status: UploadManagerStatus = 'idle'
|
|
23
|
+
|
|
24
|
+
constructor(options?: { maxSize?: FileSize, accept?: string[], maxItems?: number }) {
|
|
25
|
+
this.maxSize = options?.maxSize
|
|
26
|
+
this.accept = options?.accept ?? []
|
|
27
|
+
this.maxItems = options?.maxItems ?? 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private runChangeListeners() {
|
|
31
|
+
this.changeListeners.forEach(l => l(this.value))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private runStatusListeners() {
|
|
35
|
+
const status = this.computeStatus()
|
|
36
|
+
if (status === this.status) return
|
|
37
|
+
this.status = status
|
|
38
|
+
this.statusListeners.forEach(l => l(status))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private hasEquivalentFile(file: File) {
|
|
42
|
+
const id = getFileId(file)
|
|
43
|
+
return this.value.some(f => id === f.id)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private attachEventListeners() {
|
|
47
|
+
const handleChange = () => {
|
|
48
|
+
const newFiles: FileUpload[] = []
|
|
49
|
+
const errors: UploadError[] = []
|
|
50
|
+
for (const f of this.input?.files ?? []) {
|
|
51
|
+
if (this.maxSize && f.size > this.maxSize.value * Math.pow(1024, unitPower[this.maxSize?.unit])) {
|
|
52
|
+
errors.push(new FileIsTooLarge(f.name, this.maxSize))
|
|
53
|
+
} else if (this.maxItems && this.value.length + newFiles.length === this.maxItems) {
|
|
54
|
+
errors.push(new MaxFilesReached(f.name, this.maxItems))
|
|
55
|
+
} else if (this.hasEquivalentFile(f)) {
|
|
56
|
+
errors.push(new FileAlreadyExists(f.name))
|
|
57
|
+
} else {
|
|
58
|
+
const upload = new FileUpload(f)
|
|
59
|
+
newFiles.push(upload)
|
|
60
|
+
upload.onChange(() => this.runStatusListeners())
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (newFiles.length) {
|
|
64
|
+
this.value = [...this.value, ...newFiles]
|
|
65
|
+
this.runChangeListeners()
|
|
66
|
+
this.runStatusListeners()
|
|
67
|
+
}
|
|
68
|
+
if (errors.length) {
|
|
69
|
+
this.errorListeners.forEach(l => l(errors))
|
|
70
|
+
}
|
|
71
|
+
this.destroy()
|
|
72
|
+
}
|
|
73
|
+
this.onDestroy = () => this.input?.removeEventListener('change', handleChange)
|
|
74
|
+
this.input?.addEventListener('change', handleChange)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private createInput() {
|
|
78
|
+
this.input = document.createElement('input')
|
|
79
|
+
this.input.setAttribute('type', 'file')
|
|
80
|
+
this.input.setAttribute('multiple', '')
|
|
81
|
+
this.input.setAttribute('style', 'display: none')
|
|
82
|
+
this.input.setAttribute('accept', `.${this.accept.join(',.')}`)
|
|
83
|
+
document.body.append(this.input)
|
|
84
|
+
this.attachEventListeners()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private computeStatus(): UploadManagerStatus {
|
|
88
|
+
let uploading = false
|
|
89
|
+
for (const f of this.value) {
|
|
90
|
+
if (f.status === 'error') return 'error'
|
|
91
|
+
if (f.status === 'pending') uploading = true
|
|
92
|
+
}
|
|
93
|
+
return uploading ? 'uploading' : 'idle'
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
open() {
|
|
97
|
+
this.createInput()
|
|
98
|
+
this.input?.click()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get(): FileUpload[] {
|
|
102
|
+
return this.value
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
reset() {
|
|
106
|
+
this.value.forEach(f => f.destroy())
|
|
107
|
+
this.value = []
|
|
108
|
+
this.runChangeListeners()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
remove(file: FileUpload) {
|
|
112
|
+
file.destroy()
|
|
113
|
+
this.value = this.value.filter(f => f !== file)
|
|
114
|
+
this.runChangeListeners()
|
|
115
|
+
this.runStatusListeners()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
onChange(listener: UploadChangeListener) {
|
|
119
|
+
this.changeListeners.push(listener)
|
|
120
|
+
return () => {
|
|
121
|
+
pull(this.changeListeners, listener)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onChangeStatus(listener: UploadStatusChangeListener) {
|
|
126
|
+
this.statusListeners.push(listener)
|
|
127
|
+
return () => {
|
|
128
|
+
pull(this.statusListeners, listener)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onError(listener: UploadErrorListener) {
|
|
133
|
+
this.errorListeners.push(listener)
|
|
134
|
+
return () => {
|
|
135
|
+
pull(this.errorListeners, listener)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
isFull() {
|
|
140
|
+
return !!this.maxItems && this.value.length === this.maxItems
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
destroy() {
|
|
144
|
+
this.onDestroy()
|
|
145
|
+
this.input?.remove()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffectOnce } from '@stack-spot/portal-components'
|
|
2
|
+
import { createContext, ReactNode, useContext, useEffect, useState } from 'react'
|
|
3
|
+
import { UploadError } from './errors'
|
|
4
|
+
import { FileUpload } from './FileUpload'
|
|
5
|
+
import { FileUploadStatus } from './types'
|
|
6
|
+
import { UploadManager, UploadManagerStatus } from './UploadManager'
|
|
7
|
+
|
|
8
|
+
const context = createContext<UploadManager | undefined>(undefined)
|
|
9
|
+
|
|
10
|
+
export const UploadProvider = ({ value = new UploadManager(), children }: { value: UploadManager, children: ReactNode }) => {
|
|
11
|
+
useEffectOnce(() => () => value.destroy())
|
|
12
|
+
return <context.Provider value={value}>{children}</context.Provider>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useUploadManager() {
|
|
16
|
+
const manager = useContext(context)
|
|
17
|
+
if (!manager) throw new Error('"useUploadManager()" must be used only within the context of an <UploadProvider>.')
|
|
18
|
+
return manager
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useUploads(): FileUpload[] {
|
|
22
|
+
const manager = useUploadManager()
|
|
23
|
+
const [files, setFiles] = useState<FileUpload[]>([])
|
|
24
|
+
useEffect(() => manager.onChange(setFiles))
|
|
25
|
+
return files
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function useUploadErrorEffect(effect: (errors: UploadError[]) => void) {
|
|
29
|
+
const manager = useUploadManager()
|
|
30
|
+
useEffect(() => manager.onError(effect))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useUploadManagerStatus() {
|
|
34
|
+
const manager = useUploadManager()
|
|
35
|
+
const [status, setStatus] = useState<UploadManagerStatus>(manager.status)
|
|
36
|
+
useEffect(() => manager.onChangeStatus(setStatus))
|
|
37
|
+
return status
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function useUploadStatus(file: FileUpload) {
|
|
41
|
+
const [status, setStatus] = useState<FileUploadStatus>(file.status)
|
|
42
|
+
useEffect(() => file.onChange(setStatus))
|
|
43
|
+
return status
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { FileSize } from '../../state/types'
|
|
2
|
+
|
|
3
|
+
export class UploadError extends Error {
|
|
4
|
+
readonly fileName: string
|
|
5
|
+
|
|
6
|
+
constructor(fileName: string, message?: string) {
|
|
7
|
+
super(message || `There was an error while uploading ${fileName}`)
|
|
8
|
+
this.fileName = fileName
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class FileIsTooLarge extends UploadError {
|
|
13
|
+
readonly maxSize: FileSize
|
|
14
|
+
|
|
15
|
+
constructor(fileName: string, maxSize: FileSize) {
|
|
16
|
+
super(fileName, `Can't upload ${fileName} because it exceeds the maximum file size of ${maxSize.value} ${maxSize.unit}.`)
|
|
17
|
+
this.maxSize = maxSize
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class FileAlreadyExists extends UploadError {
|
|
22
|
+
constructor(fileName: string) {
|
|
23
|
+
super(fileName, `Can't upload ${fileName} because it is already on the list.`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class MaxFilesReached extends UploadError {
|
|
28
|
+
readonly maxFiles: number
|
|
29
|
+
|
|
30
|
+
constructor(fileName: string, maxFiles: number) {
|
|
31
|
+
super(fileName, `Can't upload ${fileName} because the maximum number of files (${maxFiles}) has been reached.`)
|
|
32
|
+
this.maxFiles = maxFiles
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { FileSize } from '../../state/types'
|
|
2
|
+
|
|
3
|
+
export function getFileId(file: File) {
|
|
4
|
+
return `${file.name}|${file.lastModified}|${file.size}`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const unitPower: Record<FileSize['unit'], number> = {
|
|
8
|
+
B: 0,
|
|
9
|
+
KB: 1,
|
|
10
|
+
MB: 2,
|
|
11
|
+
GB: 3,
|
|
12
|
+
}
|