@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.
Files changed (164) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/app-metadata.json +3 -3
  3. package/dist/chat-interceptors/send-message.js +3 -3
  4. package/dist/chat-interceptors/send-message.js.map +1 -1
  5. package/dist/components/AgentCard/dictionary.d.ts +4 -2
  6. package/dist/components/AgentCard/dictionary.d.ts.map +1 -1
  7. package/dist/components/AgentCard/dictionary.js +4 -2
  8. package/dist/components/AgentCard/dictionary.js.map +1 -1
  9. package/dist/components/FileDescription.d.ts +10 -0
  10. package/dist/components/FileDescription.d.ts.map +1 -0
  11. package/dist/components/FileDescription.js +85 -0
  12. package/dist/components/FileDescription.js.map +1 -0
  13. package/dist/components/Selector/index.d.ts +2 -2
  14. package/dist/components/Selector/index.d.ts.map +1 -1
  15. package/dist/components/Selector/index.js +2 -2
  16. package/dist/components/Selector/index.js.map +1 -1
  17. package/dist/state/ChatEntry.d.ts +9 -0
  18. package/dist/state/ChatEntry.d.ts.map +1 -1
  19. package/dist/state/ChatEntry.js.map +1 -1
  20. package/dist/state/ChatState.d.ts +5 -0
  21. package/dist/state/ChatState.d.ts.map +1 -1
  22. package/dist/state/ChatState.js +6 -0
  23. package/dist/state/ChatState.js.map +1 -1
  24. package/dist/state/constants.d.ts +5 -0
  25. package/dist/state/constants.d.ts.map +1 -0
  26. package/dist/state/constants.js +9 -0
  27. package/dist/state/constants.js.map +1 -0
  28. package/dist/state/types.d.ts +5 -1
  29. package/dist/state/types.d.ts.map +1 -1
  30. package/dist/utils/chat.d.ts +2 -1
  31. package/dist/utils/chat.d.ts.map +1 -1
  32. package/dist/utils/chat.js +2 -1
  33. package/dist/utils/chat.js.map +1 -1
  34. package/dist/utils/tools.d.ts +2 -2
  35. package/dist/utils/tools.d.ts.map +1 -1
  36. package/dist/utils/tools.js +3 -6
  37. package/dist/utils/tools.js.map +1 -1
  38. package/dist/utils/upload/FileUpload.d.ts +21 -0
  39. package/dist/utils/upload/FileUpload.d.ts.map +1 -0
  40. package/dist/utils/upload/FileUpload.js +55 -0
  41. package/dist/utils/upload/FileUpload.js.map +1 -0
  42. package/dist/utils/upload/UploadManager.d.ts +40 -0
  43. package/dist/utils/upload/UploadManager.d.ts.map +1 -0
  44. package/dist/utils/upload/UploadManager.js +131 -0
  45. package/dist/utils/upload/UploadManager.js.map +1 -0
  46. package/dist/utils/upload/context.d.ts +15 -0
  47. package/dist/utils/upload/context.d.ts.map +1 -0
  48. package/dist/utils/upload/context.js +37 -0
  49. package/dist/utils/upload/context.js.map +1 -0
  50. package/dist/utils/upload/errors.d.ts +17 -0
  51. package/dist/utils/upload/errors.d.ts.map +1 -0
  52. package/dist/utils/upload/errors.js +27 -0
  53. package/dist/utils/upload/errors.js.map +1 -0
  54. package/dist/utils/upload/types.d.ts +7 -0
  55. package/dist/utils/upload/types.d.ts.map +1 -0
  56. package/dist/utils/upload/types.js +2 -0
  57. package/dist/utils/upload/types.js.map +1 -0
  58. package/dist/utils/upload/utils.d.ts +4 -0
  59. package/dist/utils/upload/utils.d.ts.map +1 -0
  60. package/dist/utils/upload/utils.js +10 -0
  61. package/dist/utils/upload/utils.js.map +1 -0
  62. package/dist/views/Agents/AgentDescription.d.ts +2 -9
  63. package/dist/views/Agents/AgentDescription.d.ts.map +1 -1
  64. package/dist/views/Agents/AgentDescription.js +11 -9
  65. package/dist/views/Agents/AgentDescription.js.map +1 -1
  66. package/dist/views/Agents/AgentsPanel.d.ts.map +1 -1
  67. package/dist/views/Agents/AgentsPanel.js +11 -11
  68. package/dist/views/Agents/AgentsPanel.js.map +1 -1
  69. package/dist/views/Agents/AgentsTab.d.ts +2 -2
  70. package/dist/views/Agents/AgentsTab.d.ts.map +1 -1
  71. package/dist/views/Agents/AgentsTab.js +4 -4
  72. package/dist/views/Agents/AgentsTab.js.map +1 -1
  73. package/dist/views/Agents/useAgentFavorites.d.ts +1 -1
  74. package/dist/views/Agents/useAgentFavorites.js +4 -4
  75. package/dist/views/Agents/useAgentFavorites.js.map +1 -1
  76. package/dist/views/Chat/AgentInfo.d.ts +2 -1
  77. package/dist/views/Chat/AgentInfo.d.ts.map +1 -1
  78. package/dist/views/Chat/AgentInfo.js +2 -2
  79. package/dist/views/Chat/AgentInfo.js.map +1 -1
  80. package/dist/views/Chat/ChatMessage.d.ts +1 -1
  81. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  82. package/dist/views/Chat/ChatMessage.js +27 -8
  83. package/dist/views/Chat/ChatMessage.js.map +1 -1
  84. package/dist/views/Chat/styled.d.ts.map +1 -1
  85. package/dist/views/Chat/styled.js +15 -1
  86. package/dist/views/Chat/styled.js.map +1 -1
  87. package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -1
  88. package/dist/views/ChatHistory/HistoryItem.js +8 -5
  89. package/dist/views/ChatHistory/HistoryItem.js.map +1 -1
  90. package/dist/views/ChatHistory/utils.d.ts +0 -6
  91. package/dist/views/ChatHistory/utils.d.ts.map +1 -1
  92. package/dist/views/ChatHistory/utils.js +1 -16
  93. package/dist/views/ChatHistory/utils.js.map +1 -1
  94. package/dist/views/Home/CustomAgent.js +3 -3
  95. package/dist/views/Home/CustomAgent.js.map +1 -1
  96. package/dist/views/MessageInput/AgentSelector.js +4 -4
  97. package/dist/views/MessageInput/AgentSelector.js.map +1 -1
  98. package/dist/views/MessageInput/ButtonAgent.js +2 -2
  99. package/dist/views/MessageInput/ButtonAgent.js.map +1 -1
  100. package/dist/views/MessageInput/{InfoBar.d.ts → ContextBar.d.ts} +2 -2
  101. package/dist/views/MessageInput/ContextBar.d.ts.map +1 -0
  102. package/dist/views/MessageInput/{InfoBar.js → ContextBar.js} +5 -5
  103. package/dist/views/MessageInput/ContextBar.js.map +1 -0
  104. package/dist/views/MessageInput/SelectContent.d.ts.map +1 -1
  105. package/dist/views/MessageInput/SelectContent.js +14 -17
  106. package/dist/views/MessageInput/SelectContent.js.map +1 -1
  107. package/dist/views/MessageInput/UploadBar.d.ts +2 -0
  108. package/dist/views/MessageInput/UploadBar.d.ts.map +1 -0
  109. package/dist/views/MessageInput/UploadBar.js +47 -0
  110. package/dist/views/MessageInput/UploadBar.js.map +1 -0
  111. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  112. package/dist/views/MessageInput/dictionary.d.ts.map +1 -1
  113. package/dist/views/MessageInput/dictionary.js +18 -4
  114. package/dist/views/MessageInput/dictionary.js.map +1 -1
  115. package/dist/views/MessageInput/index.d.ts.map +1 -1
  116. package/dist/views/MessageInput/index.js +46 -5
  117. package/dist/views/MessageInput/index.js.map +1 -1
  118. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  119. package/dist/views/MessageInput/styled.js +56 -27
  120. package/dist/views/MessageInput/styled.js.map +1 -1
  121. package/dist/views/Steps/dictionary.d.ts +1 -1
  122. package/dist/views/Tools.js +3 -3
  123. package/dist/views/Tools.js.map +1 -1
  124. package/dist/views/Workspaces/WorkspacesTab.js +1 -1
  125. package/package.json +2 -2
  126. package/src/app-metadata.json +3 -3
  127. package/src/chat-interceptors/send-message.ts +3 -3
  128. package/src/components/AgentCard/dictionary.ts +4 -2
  129. package/src/components/FileDescription.tsx +114 -0
  130. package/src/components/Selector/index.tsx +4 -5
  131. package/src/state/ChatEntry.ts +10 -0
  132. package/src/state/ChatState.ts +6 -0
  133. package/src/state/constants.ts +12 -0
  134. package/src/state/types.ts +6 -1
  135. package/src/utils/chat.ts +3 -1
  136. package/src/utils/tools.ts +5 -7
  137. package/src/utils/upload/FileUpload.ts +64 -0
  138. package/src/utils/upload/UploadManager.ts +147 -0
  139. package/src/utils/upload/context.tsx +44 -0
  140. package/src/utils/upload/errors.ts +34 -0
  141. package/src/utils/upload/types.ts +7 -0
  142. package/src/utils/upload/utils.ts +12 -0
  143. package/src/views/Agents/AgentDescription.tsx +18 -25
  144. package/src/views/Agents/AgentsPanel.tsx +11 -12
  145. package/src/views/Agents/AgentsTab.tsx +8 -16
  146. package/src/views/Agents/useAgentFavorites.ts +4 -4
  147. package/src/views/Chat/AgentInfo.tsx +3 -2
  148. package/src/views/Chat/ChatMessage.tsx +51 -16
  149. package/src/views/Chat/styled.ts +15 -1
  150. package/src/views/ChatHistory/HistoryItem.tsx +10 -5
  151. package/src/views/ChatHistory/utils.ts +1 -18
  152. package/src/views/Home/CustomAgent.tsx +4 -4
  153. package/src/views/MessageInput/AgentSelector.tsx +4 -4
  154. package/src/views/MessageInput/ButtonAgent.tsx +2 -2
  155. package/src/views/MessageInput/{InfoBar.tsx → ContextBar.tsx} +9 -9
  156. package/src/views/MessageInput/SelectContent.tsx +17 -21
  157. package/src/views/MessageInput/UploadBar.tsx +69 -0
  158. package/src/views/MessageInput/dictionary.ts +18 -4
  159. package/src/views/MessageInput/index.tsx +77 -32
  160. package/src/views/MessageInput/styled.ts +56 -27
  161. package/src/views/Tools.tsx +4 -3
  162. package/src/views/Workspaces/WorkspacesTab.tsx +1 -1
  163. package/dist/views/MessageInput/InfoBar.d.ts.map +0 -1
  164. 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.24.4",
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": "^0.128.1",
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",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stack-spot/ai-chat-widget",
3
- "version": "1.24.4",
4
- "date": "Fri Jun 06 2025 22:00:04 GMT+0000 (Coordinated Universal Time)",
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.128.1(@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)"
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
- builtin: 'Built-in',
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
- builtin: 'Embutido',
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 { VisibilityLevelEnum } from '@stack-spot/portal-network/api/ai'
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 = VisibilityLevelEnum | 'built-in'
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
- 'built-in': 'Built-in',
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
- 'built-in': 'Embutido',
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.',
@@ -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
@@ -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
@@ -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,
@@ -1,11 +1,9 @@
1
- import { BuiltinToolResponse, ToolkitsInAgentResponse } from '@stack-spot/portal-network/api/agent'
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: ToolkitsInAgentResponse | undefined): ToolWithImage | undefined {
6
- for (const toolkit of toolkits?.builtins ?? []) {
7
- for (const tool of toolkit.tools ?? []) {
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,7 @@
1
+ export interface FileDescription {
2
+ id: string,
3
+ name: string,
4
+ extension: string,
5
+ }
6
+
7
+ export type FileUploadStatus = 'pending' | 'success' | 'error'
@@ -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
+ }