@stack-spot/ai-chat-widget 1.6.0 → 1.7.0-beta.1
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/dist/app-metadata.json +7 -7
- package/dist/components/Selector/index.d.ts +20 -0
- package/dist/components/Selector/index.d.ts.map +1 -0
- package/dist/components/Selector/index.js +134 -0
- package/dist/components/Selector/index.js.map +1 -0
- package/dist/components/Selector/styled.d.ts +2 -0
- package/dist/components/Selector/styled.d.ts.map +1 -0
- package/dist/components/Selector/styled.js +144 -0
- package/dist/components/Selector/styled.js.map +1 -0
- package/dist/regex.d.ts +1 -0
- package/dist/regex.d.ts.map +1 -1
- package/dist/regex.js +1 -0
- package/dist/regex.js.map +1 -1
- package/dist/views/Agents/AgentsTab.js +4 -4
- package/dist/views/Agents/AgentsTab.js.map +1 -1
- package/dist/views/Chat/AgentInfo.js +1 -1
- package/dist/views/Home/CustomAgent.js +3 -3
- package/dist/views/Home/CustomAgent.js.map +1 -1
- package/dist/views/Home/styled.js +1 -1
- package/dist/views/MessageInput/AgentSelector.d.ts +4 -0
- package/dist/views/MessageInput/AgentSelector.d.ts.map +1 -0
- package/dist/views/MessageInput/AgentSelector.js +31 -0
- package/dist/views/MessageInput/AgentSelector.js.map +1 -0
- package/dist/views/MessageInput/ButtonAgent.d.ts +2 -0
- package/dist/views/MessageInput/ButtonAgent.d.ts.map +1 -0
- package/dist/views/MessageInput/ButtonAgent.js +17 -0
- package/dist/views/MessageInput/ButtonAgent.js.map +1 -0
- package/dist/views/MessageInput/ButtonGroup.d.ts +1 -1
- package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
- package/dist/views/MessageInput/ButtonGroup.js +4 -6
- package/dist/views/MessageInput/ButtonGroup.js.map +1 -1
- package/dist/views/MessageInput/QuickCommandSelector.d.ts +2 -11
- package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -1
- package/dist/views/MessageInput/QuickCommandSelector.js +17 -130
- package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -1
- 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 +4 -2
- 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 +4 -1
- 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 +51 -144
- package/dist/views/MessageInput/styled.js.map +1 -1
- package/package.json +1 -1
- package/src/app-metadata.json +7 -7
- package/src/components/Selector/index.tsx +245 -0
- package/src/components/Selector/styled.ts +145 -0
- package/src/regex.ts +1 -0
- package/src/views/Agents/AgentsTab.tsx +4 -4
- package/src/views/Chat/AgentInfo.tsx +1 -1
- package/src/views/Home/CustomAgent.tsx +3 -3
- package/src/views/Home/styled.ts +1 -1
- package/src/views/MessageInput/AgentSelector.tsx +35 -0
- package/src/views/MessageInput/ButtonAgent.tsx +36 -0
- package/src/views/MessageInput/ButtonGroup.tsx +3 -10
- package/src/views/MessageInput/QuickCommandSelector.tsx +21 -205
- package/src/views/MessageInput/dictionary.ts +4 -2
- package/src/views/MessageInput/index.tsx +8 -3
- package/src/views/MessageInput/styled.ts +51 -144
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": "Tue Jan
|
|
3
|
+
"version": "1.7.0-beta.1",
|
|
4
|
+
"date": "Tue Jan 21 2025 10:16:13 GMT-0300 (Brasilia Standard Time)",
|
|
5
5
|
"dependencies": [
|
|
6
6
|
{
|
|
7
7
|
"name": "@stack-spot/app-metadata",
|
|
@@ -89,15 +89,15 @@
|
|
|
89
89
|
},
|
|
90
90
|
{
|
|
91
91
|
"name": "@citric/core",
|
|
92
|
-
"version": "6.
|
|
92
|
+
"version": "6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"name": "@citric/icons",
|
|
96
|
-
"version": "5.
|
|
96
|
+
"version": "5.9.0(react@18.2.0)"
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
99
|
"name": "@citric/ui",
|
|
100
|
-
"version": "6.
|
|
100
|
+
"version": "6.5.5(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.9.0(react@18.2.0))(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
|
|
101
101
|
},
|
|
102
102
|
{
|
|
103
103
|
"name": "@monaco-editor/react",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
"name": "@stack-spot/portal-components",
|
|
108
|
-
"version": "2.8.1(@citric/core@6.
|
|
108
|
+
"version": "2.8.1(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.9.0(react@18.2.0))(@citric/ui@6.5.5(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.9.0(react@18.2.0))(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@stack-spot/portal-theme@1.1.0(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(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))(@types/react@18.3.11)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
|
|
109
109
|
},
|
|
110
110
|
{
|
|
111
111
|
"name": "@stack-spot/portal-network",
|
|
@@ -113,7 +113,7 @@
|
|
|
113
113
|
},
|
|
114
114
|
{
|
|
115
115
|
"name": "@stack-spot/portal-theme",
|
|
116
|
-
"version": "1.1.0(@citric/core@6.
|
|
116
|
+
"version": "1.1.0(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
"name": "@stack-spot/portal-translate",
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { IconBox, Image, Text } from '@citric/core'
|
|
2
|
+
import { Agent, ExternalLink } from '@citric/icons'
|
|
3
|
+
import { IconButton } from '@citric/ui'
|
|
4
|
+
import { useKeyboardControls } from '@stack-spot/portal-components'
|
|
5
|
+
import { VisibilityLevelEnum } from '@stack-spot/portal-network/api/ai'
|
|
6
|
+
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
7
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
8
|
+
import { useCurrentChatState } from '../../context/hooks'
|
|
9
|
+
import { getUrlToStackSpotAI } from '../../utils/url'
|
|
10
|
+
import { Fading } from '../Fading'
|
|
11
|
+
import { FallbackBoundary } from '../FallbackBoundary'
|
|
12
|
+
import { SelectorBox } from './styled'
|
|
13
|
+
|
|
14
|
+
const sections = [undefined, 'personal', 'workspace', 'account', 'shared'] as const
|
|
15
|
+
|
|
16
|
+
type SelectorShortcut = '/' | '@'
|
|
17
|
+
|
|
18
|
+
interface Item {
|
|
19
|
+
id: string,
|
|
20
|
+
slug: string,
|
|
21
|
+
description: string,
|
|
22
|
+
visibility_level: VisibilityLevelEnum,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ListProps<T> {
|
|
26
|
+
filter?: string,
|
|
27
|
+
visibility?: VisibilityLevelEnum,
|
|
28
|
+
selectorConfig: SelectorConfig<T>,
|
|
29
|
+
onSelect: (item: T) => void,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface ListItemProps<T> extends ListProps<T> {
|
|
33
|
+
item: T,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ContentProps<T> {
|
|
37
|
+
filter?: string,
|
|
38
|
+
onClose: () => void,
|
|
39
|
+
inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
|
|
40
|
+
selectorConfig: SelectorConfig<T>,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SelectorConfig<T> {
|
|
44
|
+
resourceName: string,
|
|
45
|
+
shortcut: SelectorShortcut,
|
|
46
|
+
icon: React.ReactElement,
|
|
47
|
+
regex: RegExp,
|
|
48
|
+
url: string,
|
|
49
|
+
imageProp?: keyof T,
|
|
50
|
+
data: () => T[],
|
|
51
|
+
isEnabled: boolean,
|
|
52
|
+
onSelect: (item: T) => void,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SelectorProps<T> {
|
|
56
|
+
selectorConfig: SelectorConfig<T>,
|
|
57
|
+
inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const ListItem = <T extends Item>({ item, selectorConfig, onSelect }: ListItemProps<T>) => {
|
|
61
|
+
const t = useTranslate(dictionary)
|
|
62
|
+
const { shortcut, url } = selectorConfig
|
|
63
|
+
const hasImage = !!selectorConfig.imageProp
|
|
64
|
+
const image = item[selectorConfig.imageProp!] as string
|
|
65
|
+
const linkTitle = t.open.replace('%s', item.slug)
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<li>
|
|
69
|
+
{!!hasImage && (image ? <Image width="32" height="32" radius="full" src={image} /> : <IconBox size="md"><Agent /></IconBox>)}
|
|
70
|
+
<button
|
|
71
|
+
className="selector"
|
|
72
|
+
onClick={() => onSelect(item)}
|
|
73
|
+
onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
|
|
74
|
+
onFocus={(e) => e.target.closest('li')?.classList.add('focus')}
|
|
75
|
+
onBlur={(e) => e.target.closest('li')?.classList.remove('focus')}
|
|
76
|
+
>
|
|
77
|
+
<p className="selector-title">{!hasImage ? shortcut : ''}{item.slug}</p>
|
|
78
|
+
<p className="selector-description">{item.description}</p>
|
|
79
|
+
</button>
|
|
80
|
+
<IconButton as="a" title={linkTitle} aria-label={linkTitle} href={`${getUrlToStackSpotAI()}${url}${item.slug}`} target="_blank">
|
|
81
|
+
<ExternalLink />
|
|
82
|
+
</IconButton>
|
|
83
|
+
</li>
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const List = <T extends Item>({ selectorConfig, filter, visibility, onSelect }: ListProps<T>) => {
|
|
88
|
+
const t = useTranslate(dictionary)
|
|
89
|
+
const items = selectorConfig.data()
|
|
90
|
+
const filtered = useMemo(() => {
|
|
91
|
+
if (!filter && !visibility) return items
|
|
92
|
+
const lowerFilter = filter?.toLowerCase() ?? ''
|
|
93
|
+
|
|
94
|
+
return items.filter(item =>
|
|
95
|
+
item.slug.toLocaleLowerCase().startsWith(lowerFilter) && (!visibility || item?.visibility_level?.toLowerCase() === visibility),
|
|
96
|
+
)
|
|
97
|
+
}, [filter, items, visibility])
|
|
98
|
+
|
|
99
|
+
if (!items.length) return <Text className="empty" colorScheme="light.700">{t.noData.replace('%', selectorConfig.resourceName)}</Text>
|
|
100
|
+
if (!filtered.length)
|
|
101
|
+
<Text className="empty" colorScheme="light.700">{t.noResults.replace('%', selectorConfig.resourceName)}</Text>
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<ul className="selector-list">
|
|
105
|
+
{filtered.map((item) => (
|
|
106
|
+
<ListItem key={item.id} item={item} selectorConfig={selectorConfig} onSelect={onSelect} />
|
|
107
|
+
))}
|
|
108
|
+
</ul>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const SelectorContent = ({ filter, onClose, selectorConfig }: ContentProps<any>) => {
|
|
113
|
+
const t = useTranslate(dictionary)
|
|
114
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
115
|
+
const [visibility, setVisibility] = useState<VisibilityLevelEnum | undefined>()
|
|
116
|
+
const { resourceName, icon, onSelect } = selectorConfig
|
|
117
|
+
const onSelectItem = useCallback((slug: string) => {
|
|
118
|
+
onSelect(slug)
|
|
119
|
+
onClose()
|
|
120
|
+
}, [])
|
|
121
|
+
|
|
122
|
+
useKeyboardControls({
|
|
123
|
+
querySelectors: '.tabs button, button.selector',
|
|
124
|
+
disableTabBehavior: true,
|
|
125
|
+
onPressEscape: onClose,
|
|
126
|
+
onPressArrowLeft: () => (ref.current?.querySelector('.tabs button.active') as HTMLElement)?.focus(),
|
|
127
|
+
onPressArrowRight: () => (ref.current?.querySelector('button.selector') as HTMLElement)?.focus(),
|
|
128
|
+
ref,
|
|
129
|
+
}, [])
|
|
130
|
+
|
|
131
|
+
function createSectionItem(action: VisibilityLevelEnum | undefined) {
|
|
132
|
+
return (
|
|
133
|
+
<li key={action ?? 'all'}>
|
|
134
|
+
<button className={visibility === action ? 'active' : ''} onFocus={() => setVisibility(action)}>
|
|
135
|
+
{t[action || 'all']}
|
|
136
|
+
</button>
|
|
137
|
+
</li>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div ref={ref}>
|
|
143
|
+
<header>
|
|
144
|
+
<IconBox>{icon}</IconBox>
|
|
145
|
+
<Text as="h3" className="uppercase"> {resourceName} </Text>
|
|
146
|
+
</header>
|
|
147
|
+
<div className="body">
|
|
148
|
+
<ul className="tabs">{sections.map(createSectionItem)}</ul>
|
|
149
|
+
<FallbackBoundary message={t.error.replace('%s', selectorConfig.resourceName)} mini>
|
|
150
|
+
<List filter={filter} visibility={visibility} selectorConfig={selectorConfig} onSelect={onSelectItem} />
|
|
151
|
+
</FallbackBoundary>
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export const Selector = <T,>({ inputRef, selectorConfig }: SelectorProps<T>) => {
|
|
158
|
+
const { shortcut, regex, isEnabled } = selectorConfig
|
|
159
|
+
const [isClosed, setClosed] = useState(false)
|
|
160
|
+
const selectorRef = useRef<HTMLDivElement>(null)
|
|
161
|
+
const value = useCurrentChatState('nextMessage') ?? ''
|
|
162
|
+
|
|
163
|
+
const filter = useMemo(() => value === shortcut || regex.test(value) ? value.substring(1) : undefined, [value])
|
|
164
|
+
const shouldRender = isEnabled && filter !== undefined && !isClosed
|
|
165
|
+
|
|
166
|
+
// Resets the closed state whenever the message input is cleared
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (!value) setClosed(false)
|
|
169
|
+
}, [value])
|
|
170
|
+
|
|
171
|
+
// Creates the following behavior while the user types in the message input:
|
|
172
|
+
// auto-complete on tab; move focus to the resource panel on press up or down; and close the resource panel on esc.
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
function getFirst() {
|
|
175
|
+
return selectorRef.current?.querySelector('.selector') as HTMLElement | null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function onKeyDown(event: Event) {
|
|
179
|
+
const key = (event as KeyboardEvent).key
|
|
180
|
+
if (!selectorRef.current) return
|
|
181
|
+
if (key === 'Tab') {
|
|
182
|
+
getFirst()?.click()
|
|
183
|
+
event.preventDefault()
|
|
184
|
+
} else if (key === 'ArrowDown' || key === 'ArrowUp') {
|
|
185
|
+
getFirst()?.focus()
|
|
186
|
+
event.preventDefault()
|
|
187
|
+
}
|
|
188
|
+
if (key === 'Escape') {
|
|
189
|
+
setClosed(true)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
inputRef.current?.addEventListener('keydown', onKeyDown)
|
|
194
|
+
return () => inputRef.current?.removeEventListener('keydown', onKeyDown)
|
|
195
|
+
}, [])
|
|
196
|
+
|
|
197
|
+
// Closes the panel when the user clicks outside the panel or the message input.
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (!shouldRender) return
|
|
200
|
+
function onClickOut(e: Event) {
|
|
201
|
+
const target = e.target as HTMLElement | null
|
|
202
|
+
if (!selectorRef.current?.contains(target) && !inputRef.current?.contains(target)) setClosed(true)
|
|
203
|
+
}
|
|
204
|
+
document.addEventListener('click', onClickOut)
|
|
205
|
+
return () => document.removeEventListener('click', onClickOut)
|
|
206
|
+
}, [shouldRender])
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<SelectorBox>
|
|
210
|
+
<Fading visible={shouldRender} ref={selectorRef} className="box-selector">
|
|
211
|
+
<SelectorContent
|
|
212
|
+
selectorConfig={selectorConfig}
|
|
213
|
+
filter={filter}
|
|
214
|
+
onClose={() => setClosed(true)}
|
|
215
|
+
inputRef={inputRef}
|
|
216
|
+
/>
|
|
217
|
+
</Fading>
|
|
218
|
+
</SelectorBox>
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const dictionary = {
|
|
223
|
+
en: {
|
|
224
|
+
all: 'All',
|
|
225
|
+
personal: 'Personal',
|
|
226
|
+
account: 'Account',
|
|
227
|
+
shared: 'Shared',
|
|
228
|
+
workspace: 'Workspace',
|
|
229
|
+
error: 'Could not load the %ss.',
|
|
230
|
+
noData: 'You don\'t have any %s yet.',
|
|
231
|
+
noResults: 'There are no %ss to show here.',
|
|
232
|
+
open: 'Open this %s settings in a new tab.',
|
|
233
|
+
},
|
|
234
|
+
pt: {
|
|
235
|
+
all: 'Todos',
|
|
236
|
+
personal: 'Pessoal',
|
|
237
|
+
account: 'Conta',
|
|
238
|
+
shared: 'Compartilhado',
|
|
239
|
+
workspace: 'Workspace',
|
|
240
|
+
error: 'Não foi possível carregar os %ss.',
|
|
241
|
+
noData: 'Você ainda não possui %ss.',
|
|
242
|
+
noResults: 'Não há %ss para mostrar aqui.',
|
|
243
|
+
open: 'Abra as configurações deste %s em uma nova aba.',
|
|
244
|
+
},
|
|
245
|
+
} satisfies Dictionary
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { theme } from '@stack-spot/portal-theme'
|
|
2
|
+
import { styled } from 'styled-components'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export const SelectorBox = styled.div`
|
|
6
|
+
.box-selector {
|
|
7
|
+
position: absolute;
|
|
8
|
+
border-radius: 4px;
|
|
9
|
+
border: 1px solid ${theme.color.light[600]};
|
|
10
|
+
background-color: ${theme.color.light[400]};
|
|
11
|
+
box-shadow: 0px 2px 16px 0px #0000005C;
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
width: 480px;
|
|
15
|
+
bottom: 74px;
|
|
16
|
+
|
|
17
|
+
.loading, .error {
|
|
18
|
+
padding-bottom: 26px;
|
|
19
|
+
p {
|
|
20
|
+
width: 200px;
|
|
21
|
+
text-align: center;
|
|
22
|
+
line-height: 20px;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.empty {
|
|
27
|
+
padding-bottom: 26px;
|
|
28
|
+
width: 200px;
|
|
29
|
+
text-align: center;
|
|
30
|
+
line-height: 20px;
|
|
31
|
+
margin: auto;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
header {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: row;
|
|
37
|
+
gap: 8px;
|
|
38
|
+
align-items: center;
|
|
39
|
+
padding: 8px;
|
|
40
|
+
margin-bottom: 4px;
|
|
41
|
+
font-family: 'San Francisco';
|
|
42
|
+
text-transform: uppercase;
|
|
43
|
+
font-weight: 500;
|
|
44
|
+
font-size: 11px;
|
|
45
|
+
background-color: ${theme.color.light[500]};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.body {
|
|
49
|
+
display: flex;
|
|
50
|
+
flex-direction: row;
|
|
51
|
+
align-items: center;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ul {
|
|
55
|
+
margin: 0;
|
|
56
|
+
padding: 0;
|
|
57
|
+
list-style: none;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
ul.tabs {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
|
|
64
|
+
li {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
button {
|
|
70
|
+
box-sizing: border-box;
|
|
71
|
+
color: ${theme.color.light[700]};
|
|
72
|
+
text-align: left;
|
|
73
|
+
padding: 10px;
|
|
74
|
+
font-weight: 600;
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
transition: background-color 0.3s;
|
|
77
|
+
border-left: 1px solid transparent;
|
|
78
|
+
border-top-right-radius: 4px;
|
|
79
|
+
border-bottom-right-radius: 4px;
|
|
80
|
+
background-color: transparent;
|
|
81
|
+
border: none;
|
|
82
|
+
cursor: pointer;
|
|
83
|
+
outline: none;
|
|
84
|
+
|
|
85
|
+
&:hover, &.active, &:focus {
|
|
86
|
+
background-color: ${theme.color.light[600]};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&.active {
|
|
90
|
+
border-left: 1px solid ${theme.color.light.contrastText};
|
|
91
|
+
color: ${theme.color.light.contrastText};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ul.selector-list {
|
|
97
|
+
align-self: stretch;
|
|
98
|
+
display: flex;
|
|
99
|
+
flex-direction: column;
|
|
100
|
+
gap: 2px;
|
|
101
|
+
overflow-y: auto;
|
|
102
|
+
flex: 1;
|
|
103
|
+
max-height: 170px;
|
|
104
|
+
|
|
105
|
+
li {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: row;
|
|
108
|
+
align-items: center;
|
|
109
|
+
gap: 8px;
|
|
110
|
+
padding: 8px;
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
|
|
113
|
+
&:hover, &.focus {
|
|
114
|
+
background-color: ${theme.color.light[600]};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
button.selector {
|
|
118
|
+
flex: 1;
|
|
119
|
+
border: none;
|
|
120
|
+
text-align: left;
|
|
121
|
+
background-color: transparent;
|
|
122
|
+
text-align: left;
|
|
123
|
+
outline: none;
|
|
124
|
+
overflow: hidden;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
|
|
127
|
+
.selector-title {
|
|
128
|
+
font-size: 11px;
|
|
129
|
+
margin: 0 0 4px 0;
|
|
130
|
+
color: ${theme.color.light.contrastText};
|
|
131
|
+
text-transform: uppercase;
|
|
132
|
+
text-overflow: ellipsis;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.selector-description {
|
|
137
|
+
color: ${theme.color.light[700]};
|
|
138
|
+
font-size: 12px;
|
|
139
|
+
margin: 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
`
|
package/src/regex.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Button, Text } from '@citric/core'
|
|
2
|
-
import { Search } from '@citric/icons'
|
|
1
|
+
import { Button, IconBox, Text } from '@citric/core'
|
|
2
|
+
import { Agent, Search } from '@citric/icons'
|
|
3
3
|
import { Placeholder } from '@stack-spot/portal-components/Placeholder'
|
|
4
4
|
import { MiniLogo } from '@stack-spot/portal-components/svg'
|
|
5
5
|
import { agentClient } from '@stack-spot/portal-network'
|
|
@@ -20,7 +20,7 @@ export const AgentsTab = ({ visibility }: { visibility: VisibilityLevel | 'BUILT
|
|
|
20
20
|
const [filter, setFilter] = useState('')
|
|
21
21
|
const defaultAgent = useMemo(() => ({
|
|
22
22
|
id: '',
|
|
23
|
-
name: '
|
|
23
|
+
name: 'StackSpot AI',
|
|
24
24
|
description: t.defaultAgentDescription,
|
|
25
25
|
llm_config: { model_slug: 'gpt4o' },
|
|
26
26
|
} as AgentResponse), [])
|
|
@@ -54,7 +54,7 @@ export const AgentsTab = ({ visibility }: { visibility: VisibilityLevel | 'BUILT
|
|
|
54
54
|
onChange={setValue}
|
|
55
55
|
renderLabel={({ name, avatar, id }) => (
|
|
56
56
|
<AgentLabel>
|
|
57
|
-
{id ? (avatar
|
|
57
|
+
{id ? (avatar ? <img src={avatar} /> : <IconBox size="xs"><Agent /></IconBox>) : <MiniLogo />}
|
|
58
58
|
<Text>{name}</Text>
|
|
59
59
|
</AgentLabel>
|
|
60
60
|
)}
|
|
@@ -15,6 +15,6 @@ export const AgentInfo = ({ agent }: Props) => (
|
|
|
15
15
|
? <img src={agent.image} className="custom-agent-image" />
|
|
16
16
|
: <div className="default-image-wrapper"><MiniLogo className="agent-image" /></div>
|
|
17
17
|
}
|
|
18
|
-
<Text appearance="body2">{agent?.label || '
|
|
18
|
+
<Text appearance="body2">{agent?.label || 'StackSpot AI'}</Text>
|
|
19
19
|
</>
|
|
20
20
|
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Text } from '@citric/core'
|
|
2
|
-
import {
|
|
1
|
+
import { IconBox, Text } from '@citric/core'
|
|
2
|
+
import { Agent } from '@citric/icons'
|
|
3
3
|
import { agentClient } from '@stack-spot/portal-network'
|
|
4
4
|
import { theme } from '@stack-spot/portal-theme'
|
|
5
5
|
import { useMemo } from 'react'
|
|
@@ -31,7 +31,7 @@ export const CustomAgent = () => {
|
|
|
31
31
|
|
|
32
32
|
return (
|
|
33
33
|
<HomeBox className="home-page custom-agent">
|
|
34
|
-
{image ? <img src={image} className="avatar" /> : <
|
|
34
|
+
{image ? <img src={image} className="avatar" /> : <IconBox className="avatar"><Agent /></IconBox>}
|
|
35
35
|
<Text appearance="h3">{label}</Text>
|
|
36
36
|
<div className="shortcuts">{suggestions?.length ? suggestions : null}</div>
|
|
37
37
|
</HomeBox>
|
package/src/views/Home/styled.ts
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Agent } from '@citric/icons'
|
|
2
|
+
import { agentClient } from '@stack-spot/portal-network'
|
|
3
|
+
import { AgentResponse } from '@stack-spot/portal-network/api/agent'
|
|
4
|
+
import { useCallback } from 'react'
|
|
5
|
+
import { Selector } from '../../components/Selector'
|
|
6
|
+
import { useCurrentChat, useCurrentChatState } from '../../context/hooks'
|
|
7
|
+
import { agentRegex } from '../../regex'
|
|
8
|
+
|
|
9
|
+
export const AgentSelector = ({ inputRef }: { inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement> }) => {
|
|
10
|
+
const chat = useCurrentChat()
|
|
11
|
+
const onSelectItem = useCallback((agent: AgentResponse) => {
|
|
12
|
+
const newValue = `@${agent.slug}`
|
|
13
|
+
chat.set('nextMessage', undefined)
|
|
14
|
+
chat.set('agent', { id: agent.id, label: agent.slug, image: agent.avatar, builtIn: false })
|
|
15
|
+
|
|
16
|
+
if (!inputRef.current) return
|
|
17
|
+
inputRef.current.value = newValue
|
|
18
|
+
inputRef.current.focus()
|
|
19
|
+
}, [])
|
|
20
|
+
|
|
21
|
+
return <Selector
|
|
22
|
+
inputRef={inputRef}
|
|
23
|
+
selectorConfig={{
|
|
24
|
+
resourceName: 'Agent',
|
|
25
|
+
shortcut: '@',
|
|
26
|
+
icon: <Agent />,
|
|
27
|
+
imageProp: 'avatar',
|
|
28
|
+
regex: agentRegex,
|
|
29
|
+
url: '/agent/',
|
|
30
|
+
data: () => agentClient.agents.useQuery({}),
|
|
31
|
+
isEnabled: useCurrentChatState('features').agent,
|
|
32
|
+
onSelect: onSelectItem,
|
|
33
|
+
}}
|
|
34
|
+
/>
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Flex, IconBox } from '@citric/core'
|
|
2
|
+
import { Agent, TimesMini } from '@citric/icons'
|
|
3
|
+
import { IconButton, Tooltip } from '@citric/ui'
|
|
4
|
+
import { MiniLogo } from '@stack-spot/portal-components/svg'
|
|
5
|
+
import { useCurrentChat, useCurrentChatState, useWidget } from '../../context/hooks'
|
|
6
|
+
import { useMessageInputDictionary } from './dictionary'
|
|
7
|
+
|
|
8
|
+
export const ButtonAgent = () => {
|
|
9
|
+
const t = useMessageInputDictionary()
|
|
10
|
+
const widget = useWidget()
|
|
11
|
+
const chat = useCurrentChat()
|
|
12
|
+
const agent = useCurrentChatState('agent')
|
|
13
|
+
const features = useCurrentChatState('features')
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="button-group">
|
|
17
|
+
{features.agent && (
|
|
18
|
+
<div className="group-agent">
|
|
19
|
+
<IconButton aria-label={t.agent} title={t.agent} className="agent" onClick={() => widget.set('panel', 'agent')}>
|
|
20
|
+
<MiniLogo />
|
|
21
|
+
</IconButton>
|
|
22
|
+
{agent?.id &&
|
|
23
|
+
<Tooltip text={t.remove} >
|
|
24
|
+
<IconButton aria-label={t.remove} className="agent agent-selected" onClick={() => chat.set('agent', undefined)}>
|
|
25
|
+
{agent?.image ? <img src={agent.image} className="image" /> : <IconBox className="image" size="xs"><Agent /></IconBox>}
|
|
26
|
+
<Flex className="icon-remove" alignContent="center" justifyContent="center">
|
|
27
|
+
<TimesMini />
|
|
28
|
+
</Flex>
|
|
29
|
+
</IconButton>
|
|
30
|
+
</Tooltip>
|
|
31
|
+
}
|
|
32
|
+
</div>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ChevronRight, Code, KnowledgeSource, Send, Stack, Times, Workspace } from '@citric/icons'
|
|
2
2
|
import { IconButton } from '@citric/ui'
|
|
3
|
-
import { MiniLogo } from '@stack-spot/portal-components/svg'
|
|
4
3
|
import { listToClass } from '@stack-spot/portal-theme'
|
|
5
4
|
import { useEffect, useRef } from 'react'
|
|
6
5
|
import { useCurrentChatState, useWidget } from '../../context/hooks'
|
|
@@ -31,16 +30,15 @@ interface ButtonGroupProps {
|
|
|
31
30
|
|
|
32
31
|
/**
|
|
33
32
|
* Renders the button group at right bottom side of the message input. This includes the send button as well as the buttons to open the
|
|
34
|
-
* editor, change the
|
|
33
|
+
* editor, change the stack, etc.
|
|
35
34
|
*/
|
|
36
35
|
export const ButtonGroup = ({ onSend, onCancel, expanded, setExpanded, isLoading }: ButtonGroupProps) => {
|
|
37
36
|
const t = useMessageInputDictionary()
|
|
38
37
|
const widget = useWidget()
|
|
39
38
|
const featureButtonsWidth = useRef<number | undefined>()
|
|
40
39
|
const featureButtons = useRef<HTMLDivElement>(null)
|
|
41
|
-
const agent = useCurrentChatState('agent')
|
|
42
40
|
const features = useCurrentChatState('features')
|
|
43
|
-
const hasFeatureButtons = features.
|
|
41
|
+
const hasFeatureButtons = features.workspace || features.knowledgeSource || features.stack || features.editor
|
|
44
42
|
|
|
45
43
|
useEffect(
|
|
46
44
|
() => {
|
|
@@ -52,7 +50,7 @@ export const ButtonGroup = ({ onSend, onCancel, expanded, setExpanded, isLoading
|
|
|
52
50
|
else featureButtons.current.style.width = `${featureButtonsWidth.current}px`
|
|
53
51
|
},
|
|
54
52
|
// don't use the whole features object here, it would make every chat tab change rerun this effect.
|
|
55
|
-
[features.
|
|
53
|
+
[features.workspace, features.knowledgeSource, features.stack, features.editor],
|
|
56
54
|
)
|
|
57
55
|
|
|
58
56
|
return (
|
|
@@ -63,11 +61,6 @@ export const ButtonGroup = ({ onSend, onCancel, expanded, setExpanded, isLoading
|
|
|
63
61
|
className={listToClass(['feature-buttons', expanded && 'expanded'])}
|
|
64
62
|
style={{ width: expanded ? featureButtonsWidth.current : 0 }}
|
|
65
63
|
>
|
|
66
|
-
{features.agent && (
|
|
67
|
-
<IconButton aria-label={t.agent} title={t.agent} className="agent" onClick={() => widget.set('panel', 'agent')}>
|
|
68
|
-
{agent?.image ? <img src={agent.image} /> : <MiniLogo />}
|
|
69
|
-
</IconButton>
|
|
70
|
-
)}
|
|
71
64
|
{features.workspace && (
|
|
72
65
|
<IconButton aria-label={t.workspace} title={t.workspace} onClick={() => widget.set('panel', 'workspace')}>
|
|
73
66
|
<Workspace />
|