@stack-spot/ai-chat-widget 3.0.0-beta.1 → 3.0.2-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/CHANGELOG.md +85 -19
- package/dist/app-metadata.json +6 -6
- package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
- package/dist/chat-interceptors/quick-commands.js +10 -3
- package/dist/chat-interceptors/quick-commands.js.map +1 -1
- package/dist/chat-interceptors/send-message.d.ts.map +1 -1
- package/dist/chat-interceptors/send-message.js +76 -15
- package/dist/chat-interceptors/send-message.js.map +1 -1
- package/dist/layout.css +6 -0
- package/dist/state/ChatEntry.d.ts +1 -1
- package/dist/state/ChatEntry.d.ts.map +1 -1
- package/dist/state/ChatEntry.js +2 -1
- package/dist/state/ChatEntry.js.map +1 -1
- package/dist/state/ChatState.d.ts +4 -0
- package/dist/state/ChatState.d.ts.map +1 -1
- package/dist/state/ChatState.js.map +1 -1
- package/dist/utils/chat.d.ts.map +1 -1
- package/dist/utils/chat.js +1 -0
- package/dist/utils/chat.js.map +1 -1
- package/dist/utils/knowledge-source.d.ts +2 -2
- package/dist/utils/planning-tool.d.ts +3 -0
- package/dist/utils/planning-tool.d.ts.map +1 -1
- package/dist/utils/planning-tool.js +7 -0
- package/dist/utils/planning-tool.js.map +1 -1
- package/dist/views/Agents/styled.d.ts.map +1 -1
- package/dist/views/Agents/styled.js +1 -2
- package/dist/views/Agents/styled.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 +23 -6
- package/dist/views/Chat/ChatMessage.js.map +1 -1
- package/dist/views/Chat/StepsList.d.ts +8 -3
- package/dist/views/Chat/StepsList.d.ts.map +1 -1
- package/dist/views/Chat/StepsList.js +67 -34
- package/dist/views/Chat/StepsList.js.map +1 -1
- package/dist/views/Chat/styled.d.ts.map +1 -1
- package/dist/views/Chat/styled.js +8 -12
- package/dist/views/Chat/styled.js.map +1 -1
- package/dist/views/MessageInput/ButtonBar.d.ts.map +1 -1
- package/dist/views/MessageInput/ButtonBar.js +2 -1
- package/dist/views/MessageInput/ButtonBar.js.map +1 -1
- package/dist/views/MessageInput/ModelSwitcher/index.d.ts +2 -0
- package/dist/views/MessageInput/ModelSwitcher/index.d.ts.map +1 -0
- package/dist/views/MessageInput/ModelSwitcher/index.js +25 -0
- package/dist/views/MessageInput/ModelSwitcher/index.js.map +1 -0
- package/dist/views/MessageInput/ModelSwitcher/utils.d.ts +30 -0
- package/dist/views/MessageInput/ModelSwitcher/utils.d.ts.map +1 -0
- package/dist/views/MessageInput/ModelSwitcher/utils.js +91 -0
- package/dist/views/MessageInput/ModelSwitcher/utils.js.map +1 -0
- package/dist/views/MessageInput/SelectContent.js +1 -1
- package/dist/views/MessageInput/SelectContent.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 +6 -0
- package/dist/views/MessageInput/dictionary.js.map +1 -1
- package/dist/views/MessageInput/styled.d.ts +12 -0
- package/dist/views/MessageInput/styled.d.ts.map +1 -1
- package/dist/views/MessageInput/styled.js +35 -0
- package/dist/views/MessageInput/styled.js.map +1 -1
- package/dist/views/Resources.js +12 -5
- package/dist/views/Resources.js.map +1 -1
- package/package.json +4 -4
- package/src/app-metadata.json +6 -6
- package/src/chat-interceptors/quick-commands.ts +18 -7
- package/src/chat-interceptors/send-message.ts +82 -18
- package/src/layout.css +6 -0
- package/src/state/ChatEntry.ts +2 -1
- package/src/state/ChatState.ts +4 -0
- package/src/utils/chat.ts +1 -0
- package/src/utils/knowledge-source.ts +2 -2
- package/src/utils/planning-tool.ts +9 -0
- package/src/views/Agents/styled.ts +1 -2
- package/src/views/Chat/ChatMessage.tsx +63 -57
- package/src/views/Chat/StepsList.tsx +115 -72
- package/src/views/Chat/styled.ts +8 -12
- package/src/views/MessageInput/ButtonBar.tsx +2 -0
- package/src/views/MessageInput/ModelSwitcher/index.tsx +68 -0
- package/src/views/MessageInput/ModelSwitcher/utils.tsx +143 -0
- package/src/views/MessageInput/SelectContent.tsx +1 -1
- package/src/views/MessageInput/dictionary.ts +6 -0
- package/src/views/MessageInput/styled.ts +37 -0
- package/src/views/Resources.tsx +18 -10
|
@@ -3,11 +3,11 @@ import { AnimatedHeight } from '@stack-spot/portal-components/AnimatedHeight'
|
|
|
3
3
|
import { ChatStep, StepChatStep, ToolChatStep } from '@stack-spot/portal-network'
|
|
4
4
|
import { theme } from '@stack-spot/portal-theme'
|
|
5
5
|
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
6
|
-
import { findLastIndex } from 'lodash'
|
|
7
|
-
import React, { useMemo } from 'react'
|
|
6
|
+
import { findLast, findLastIndex } from 'lodash'
|
|
7
|
+
import React, { useEffect, useMemo } from 'react'
|
|
8
8
|
import styled from 'styled-components'
|
|
9
9
|
import { Markdown } from '../../components/Markdown'
|
|
10
|
-
import { useCurrentChat, useCurrentChatMessages, useWidget } from '../../context/hooks'
|
|
10
|
+
import { useChat, useChatMessages, useCurrentChat, useCurrentChatMessages, useWidget } from '../../context/hooks'
|
|
11
11
|
import { ChatEntry } from '../../state/ChatEntry'
|
|
12
12
|
import { planningToolDictionaryHelper } from '../../utils/planning-tool'
|
|
13
13
|
import { updateToolStep } from '../../utils/update-tool-step'
|
|
@@ -17,6 +17,7 @@ interface Props {
|
|
|
17
17
|
steps: ChatStep[],
|
|
18
18
|
messageId: number,
|
|
19
19
|
chatId: string,
|
|
20
|
+
userHasAlreadyAnswered?: boolean,
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
interface StepChatStepWithTarget extends Omit<StepChatStep, 'status' | 'id' | 'type'> {
|
|
@@ -52,14 +53,14 @@ const StepAccordionHeader = ({ step, index, expand }: Pick<StepProps, 'step' | '
|
|
|
52
53
|
<Badge colorScheme="inverse" appearance="square">
|
|
53
54
|
{t.step} {index}
|
|
54
55
|
</Badge>}
|
|
55
|
-
<Text className="step-title" appearance="body2"
|
|
56
|
+
<Text className="step-title" appearance="body2">
|
|
56
57
|
{step.input}
|
|
57
58
|
</Text>
|
|
58
|
-
{step.status === 'awaiting_approval' &&
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
{step.status === 'awaiting_approval' &&
|
|
60
|
+
<Badge appearance="square" style={{ backgroundColor: theme.color.gray[800], color: theme.color.gray[50] }}>
|
|
61
|
+
<Icon icon="Security" />
|
|
62
|
+
{t.pendingReview}
|
|
63
|
+
</Badge>}
|
|
63
64
|
</Row>
|
|
64
65
|
}
|
|
65
66
|
|
|
@@ -72,12 +73,13 @@ const StyledCard = styled(Card)`
|
|
|
72
73
|
const Step = ({ step, index, onClick, total, totalTools, isAllDone }: StepProps) => {
|
|
73
74
|
const t = useTranslate(dictionary)
|
|
74
75
|
const status = getStatusIcon(step.status, isAllDone)
|
|
76
|
+
const hasTools = step.attempts?.[0]?.tools && step.attempts?.[0]?.tools?.length > 0
|
|
75
77
|
|
|
76
78
|
return (
|
|
77
79
|
<li tabIndex={onClick ? 0 : undefined} onClick={onClick} role={onClick ? 'button' : 'listitem'}>
|
|
78
80
|
<Row gap="4px" alignItems="center">
|
|
79
81
|
<div className="step-status-icon">{status}</div>
|
|
80
|
-
<StyledCard p="8px" w="
|
|
82
|
+
<StyledCard p="8px" w="80%">
|
|
81
83
|
<Accordion header={expand => <StepAccordionHeader step={step} index={index} expand={expand} />}>
|
|
82
84
|
<Column pt="12px">
|
|
83
85
|
{total ?
|
|
@@ -90,27 +92,32 @@ const Step = ({ step, index, onClick, total, totalTools, isAllDone }: StepProps)
|
|
|
90
92
|
<Row gap="4px">
|
|
91
93
|
<Icon icon="BorderRadius" size="sm" color="light.700" />
|
|
92
94
|
<Text color="light.700">{t.totalTools}</Text>
|
|
93
|
-
{totalTools}
|
|
95
|
+
{totalTools ?? 0}
|
|
94
96
|
</Row>
|
|
95
97
|
</Row>
|
|
96
98
|
: <>
|
|
97
|
-
<Row pb="8px"
|
|
99
|
+
<Row pb="8px">
|
|
98
100
|
<Icon icon="Target" size="sm" color="light.700" />
|
|
99
|
-
<Text color="light.700">{t.stepGoal}:</Text>
|
|
100
|
-
<Text>
|
|
101
|
+
<Text color="light.700" tag="span" style={{ margin: '0 4px' }}>{t.stepGoal}:</Text>
|
|
102
|
+
<Text tag="span">
|
|
101
103
|
{step.input}
|
|
102
104
|
</Text>
|
|
103
105
|
</Row>
|
|
104
|
-
|
|
106
|
+
{hasTools ? <>
|
|
107
|
+
<Row gap="4px">
|
|
108
|
+
<Icon icon="BorderRadius" size="sm" color="light.700" />
|
|
109
|
+
<Text color="light.700">{t.toolsToBeExecuted}:</Text>
|
|
110
|
+
</Row>
|
|
111
|
+
<ul className="tools-list">
|
|
112
|
+
{step.attempts?.[0]?.tools?.map((tool, index) => (<li key={`${tool.id}-${index}`}>
|
|
113
|
+
<Text>{tool.name}: {tool.goal}</Text>
|
|
114
|
+
</li>))}
|
|
115
|
+
</ul>
|
|
116
|
+
</> : <Row gap="4px">
|
|
105
117
|
<Icon icon="BorderRadius" size="sm" color="light.700" />
|
|
106
|
-
<Text color="light.700">{t.
|
|
118
|
+
<Text color="light.700">{t.noToolToBeUsed}</Text>
|
|
107
119
|
</Row>
|
|
108
|
-
|
|
109
|
-
{step.attempts?.[0]?.tools?.map((tool) => (<li key={tool.id}>
|
|
110
|
-
<Text>{tool.name}: {tool.goal}</Text>
|
|
111
|
-
</li>),
|
|
112
|
-
)}
|
|
113
|
-
</ul>
|
|
120
|
+
}
|
|
114
121
|
</>
|
|
115
122
|
}
|
|
116
123
|
</Column>
|
|
@@ -140,12 +147,12 @@ export const StepsPlaceholder = () => {
|
|
|
140
147
|
</Card>
|
|
141
148
|
}
|
|
142
149
|
|
|
143
|
-
const AwaitingApproval = ({ customApproveText }: { customApproveText?: string }) => {
|
|
150
|
+
const AwaitingApproval = ({ customApproveText, chatId }: { chatId: string, customApproveText?: string }) => {
|
|
144
151
|
const t = useTranslate(dictionary)
|
|
145
|
-
const chat =
|
|
152
|
+
const chat = useChat(chatId)
|
|
146
153
|
|
|
147
154
|
const onAnswer = (response: string) => {
|
|
148
|
-
chat.pushMessage(ChatEntry.createUserEntry(response))
|
|
155
|
+
chat.pushMessage(ChatEntry.createUserEntry('', false, undefined, undefined, response))
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
return <>
|
|
@@ -167,31 +174,30 @@ const AwaitingApproval = ({ customApproveText }: { customApproveText?: string })
|
|
|
167
174
|
</>
|
|
168
175
|
}
|
|
169
176
|
|
|
170
|
-
export const ToolStepsList = ({
|
|
177
|
+
export const ToolStepsList = ({ toolStep, messageId, chatId }: { toolStep: ToolChatStep, messageId: number, chatId: string }) => {
|
|
171
178
|
const t = useTranslate(dictionary)
|
|
172
|
-
const toolsAwaiting = steps
|
|
173
179
|
const chat = useCurrentChat()
|
|
174
180
|
const messages = useCurrentChatMessages()
|
|
175
181
|
const inputParsed = `\`\`\`json
|
|
176
|
-
${JSON.stringify(
|
|
182
|
+
${JSON.stringify(toolStep?.input, null, 2)}
|
|
177
183
|
\`\`\``
|
|
178
184
|
|
|
179
185
|
const tool = useMemo(() => {
|
|
180
|
-
if (!
|
|
181
|
-
return
|
|
182
|
-
}, [
|
|
186
|
+
if (!toolStep) return undefined
|
|
187
|
+
return toolStep.attempts?.[0].tools?.[0]
|
|
188
|
+
}, [toolStep])
|
|
183
189
|
|
|
184
|
-
|
|
185
|
-
if (!
|
|
186
|
-
const executionId =
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!toolStep) return undefined
|
|
192
|
+
const executionId = toolStep.attempts?.[0]?.tools?.[0]?.executionId
|
|
187
193
|
if (!executionId) return
|
|
188
194
|
|
|
189
|
-
updateToolStep(messages, executionId,
|
|
190
|
-
|
|
191
|
-
}, [messages,
|
|
195
|
+
updateToolStep(messages, executionId, toolStep.status)
|
|
196
|
+
|
|
197
|
+
}, [messages, toolStep, toolStep.status])
|
|
192
198
|
|
|
193
199
|
return <>
|
|
194
|
-
{
|
|
200
|
+
{toolStep && tool ? <AnimatedHeight>
|
|
195
201
|
<div className="steps">
|
|
196
202
|
<Badge colorPalette="yellow" appearance="square">
|
|
197
203
|
<Icon icon="StopWatch" />
|
|
@@ -204,7 +210,7 @@ export const ToolStepsList = ({ steps, messageId }: { steps: ToolChatStep, messa
|
|
|
204
210
|
</Row>
|
|
205
211
|
|
|
206
212
|
<Text>
|
|
207
|
-
{
|
|
213
|
+
{toolStep.user_question}
|
|
208
214
|
</Text>
|
|
209
215
|
|
|
210
216
|
<Accordion header={expand => <Row gap="8px" mb="4px">
|
|
@@ -216,54 +222,62 @@ export const ToolStepsList = ({ steps, messageId }: { steps: ToolChatStep, messa
|
|
|
216
222
|
</Markdown>
|
|
217
223
|
</Accordion>
|
|
218
224
|
|
|
219
|
-
|
|
220
|
-
|
|
225
|
+
<AwaitingApproval customApproveText={t.approveTool} chatId={chatId} />
|
|
221
226
|
</Card>
|
|
222
227
|
</div>
|
|
223
228
|
</AnimatedHeight> : null}
|
|
224
229
|
</>
|
|
225
230
|
}
|
|
226
231
|
|
|
227
|
-
export const StepsList = ({ steps, chatId,
|
|
232
|
+
export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }: Props) => {
|
|
228
233
|
const t = useTranslate(dictionary)
|
|
229
|
-
|
|
234
|
+
|
|
235
|
+
const filteredSteps = steps.filter(s => s.type === 'step')
|
|
236
|
+
const actualSteps = useMemo(() => filteredSteps.filter((item) => {
|
|
237
|
+
const messageIdForStep = planningToolDictionaryHelper.getMessageIdFromStepId(item.id)
|
|
238
|
+
if (!messageIdForStep) {
|
|
239
|
+
planningToolDictionaryHelper.setMessageIdForStepId(messageId, item.id)
|
|
240
|
+
return true
|
|
241
|
+
} else if (messageIdForStep === messageId) {
|
|
242
|
+
return true
|
|
243
|
+
}
|
|
244
|
+
// If a step is from a planning and it is already inserted in the planningToolDictionaryHelper it means the step is already in a
|
|
245
|
+
// previous message and we do not want to show it again (for instance, when required a approval in the planning, we will have
|
|
246
|
+
// two messages with the same step id one for the planning awaiting and one for the planning end, so we want to show only one)
|
|
247
|
+
return false
|
|
248
|
+
}), [filteredSteps])
|
|
249
|
+
|
|
230
250
|
const planning = steps.filter(s => s.type === 'planning')
|
|
231
|
-
|
|
232
|
-
|
|
251
|
+
|
|
252
|
+
useEffect(() => {
|
|
233
253
|
actualSteps.map((item) => {
|
|
234
|
-
const executionId = item.attempts[0]?.tools?.[0]
|
|
254
|
+
const executionId = item.attempts[0]?.tools?.[0]?.executionId
|
|
235
255
|
if (executionId) {
|
|
236
256
|
planningToolDictionaryHelper.setMessageIdPlanningStepToolExecutionId(`${messageId}`, executionId)
|
|
237
257
|
}
|
|
238
258
|
})
|
|
239
259
|
}, [actualSteps, messageId])
|
|
240
260
|
|
|
241
|
-
const toolsStep = steps
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const executionId = toolsStep?.
|
|
261
|
+
const toolsStep = findLast(steps, s => s.type === 'tool')
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const executionId = toolsStep?.attempts?.[0]?.tools?.[0]?.executionId
|
|
245
265
|
if (executionId) {
|
|
246
266
|
planningToolDictionaryHelper.setMessageIdToolStepToolExecutionId(`${messageId}`, executionId)
|
|
247
267
|
}
|
|
248
268
|
}, [toolsStep, messageId])
|
|
249
269
|
|
|
250
|
-
const planningGoal =
|
|
270
|
+
const planningGoal = planning?.[0]?.goal
|
|
251
271
|
const isLastStepDone = actualSteps[actualSteps.length - 1]?.status !== 'running' &&
|
|
252
272
|
actualSteps[actualSteps.length - 1]?.status !== 'pending'
|
|
253
273
|
const totalTools = useMemo(() => actualSteps?.reduce((sum, step) => {
|
|
254
|
-
const firstAttempt = step
|
|
255
|
-
const toolsCount = firstAttempt
|
|
274
|
+
const firstAttempt = step?.attempts && step.attempts[0]
|
|
275
|
+
const toolsCount = firstAttempt?.tools?.length ?? 0
|
|
256
276
|
return sum + toolsCount
|
|
257
277
|
}, 0), [steps])
|
|
258
278
|
|
|
259
279
|
let currentStepIndex = findLastIndex(actualSteps, s => s.status === 'running' || s.status === 'success')
|
|
260
280
|
if (currentStepIndex === -1) currentStepIndex = 0
|
|
261
|
-
const widget = useWidget()
|
|
262
|
-
|
|
263
|
-
function openToolsPanel() {
|
|
264
|
-
widget.set('currentMessageInPanel', { chatId, messageId })
|
|
265
|
-
widget.set('panel', 'steps')
|
|
266
|
-
}
|
|
267
281
|
|
|
268
282
|
return (<>
|
|
269
283
|
{actualSteps.length > 0 ? <AnimatedHeight>
|
|
@@ -272,12 +286,10 @@ export const StepsList = ({ steps, chatId, messageId }: Props) => {
|
|
|
272
286
|
<Icon icon="Target" size="sm" color="light.600" />
|
|
273
287
|
<Text>{t.executionPlan}</Text>
|
|
274
288
|
</Row>
|
|
289
|
+
|
|
275
290
|
<ul className="steps-list">
|
|
276
|
-
<Step
|
|
277
|
-
|
|
278
|
-
index={currentStepIndex + 1}
|
|
279
|
-
/>
|
|
280
|
-
|
|
291
|
+
{actualSteps.map((s, i) => <Step step={s} key={i} index={i + 1} />)}
|
|
292
|
+
|
|
281
293
|
<Step
|
|
282
294
|
step={{ status: 'target', input: planningGoal, attempts: [] }}
|
|
283
295
|
index={currentStepIndex + 1}
|
|
@@ -290,23 +302,52 @@ export const StepsList = ({ steps, chatId, messageId }: Props) => {
|
|
|
290
302
|
<Column gap="8px" mt="8px">
|
|
291
303
|
<Divider colorScheme="light" />
|
|
292
304
|
<Text color="light.700">{planning?.[0]?.user_question}</Text>
|
|
293
|
-
{planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval />}
|
|
305
|
+
{!userHasAlreadyAnswered && planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval chatId={chatId} />}
|
|
294
306
|
</Column>
|
|
295
307
|
|
|
296
|
-
{
|
|
297
|
-
|
|
298
|
-
<Icon group="fill" icon="Play" size="xs" />
|
|
299
|
-
{t.detailed}
|
|
300
|
-
</Button>
|
|
301
|
-
</div>}
|
|
308
|
+
{userHasAlreadyAnswered && planning?.[0]?.status === 'success' && <ViewToolsDetails chatId={chatId} />}
|
|
309
|
+
|
|
302
310
|
</div>
|
|
303
311
|
</AnimatedHeight> : null}
|
|
304
312
|
|
|
305
|
-
{toolsStep
|
|
313
|
+
{toolsStep && toolsStep.status === 'awaiting_approval' && !userHasAlreadyAnswered &&
|
|
314
|
+
<ToolStepsList toolStep={toolsStep} messageId={messageId} chatId={chatId} />}
|
|
306
315
|
</>
|
|
307
316
|
)
|
|
308
317
|
}
|
|
309
318
|
|
|
319
|
+
export const ViewToolsDetails = ({ chatId }: { chatId: string }) => {
|
|
320
|
+
const t = useTranslate(dictionary)
|
|
321
|
+
const messages = useChatMessages(chatId)
|
|
322
|
+
const widget = useWidget()
|
|
323
|
+
const getPlanningMessageId = () => {
|
|
324
|
+
const messageWithPlanning = findLast(messages, (message) => {
|
|
325
|
+
const steps = message.getValue().steps
|
|
326
|
+
const planningStep = steps?.find((step) => step.type === 'planning' && step.status === 'success')
|
|
327
|
+
return planningStep ? true : false
|
|
328
|
+
})
|
|
329
|
+
return messageWithPlanning?.id
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function openToolsPanel() {
|
|
333
|
+
const messageId = getPlanningMessageId()
|
|
334
|
+
|
|
335
|
+
if (messageId) {
|
|
336
|
+
widget.set('currentMessageInPanel', { chatId, messageId })
|
|
337
|
+
widget.set('panel', 'steps')
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return <>
|
|
342
|
+
<div className="step-actions">
|
|
343
|
+
<Button colorScheme="light" size="sm" className="icon-button details" onClick={openToolsPanel}>
|
|
344
|
+
<Icon group="fill" icon="Play" size="xs" />
|
|
345
|
+
{t.detailed}
|
|
346
|
+
</Button>
|
|
347
|
+
</div>
|
|
348
|
+
</>
|
|
349
|
+
}
|
|
350
|
+
|
|
310
351
|
const dictionary = {
|
|
311
352
|
en: {
|
|
312
353
|
step: 'Step',
|
|
@@ -326,6 +367,7 @@ const dictionary = {
|
|
|
326
367
|
viewDetails: 'View details',
|
|
327
368
|
approveTool: 'Approve execution',
|
|
328
369
|
pendingReview: 'Pending review',
|
|
370
|
+
noToolToBeUsed: 'No tool will be needed',
|
|
329
371
|
},
|
|
330
372
|
pt: {
|
|
331
373
|
step: 'Passo',
|
|
@@ -345,5 +387,6 @@ const dictionary = {
|
|
|
345
387
|
viewDetails: 'Ver detalhes',
|
|
346
388
|
approveTool: 'Aprovar execução',
|
|
347
389
|
pendingReview: 'Revisão pendente',
|
|
390
|
+
noToolToBeUsed: 'Nenhuma tool será necessária',
|
|
348
391
|
},
|
|
349
392
|
} satisfies Dictionary
|
package/src/views/Chat/styled.ts
CHANGED
|
@@ -220,7 +220,7 @@ export const ChatList: IStyledComponentBase<
|
|
|
220
220
|
|
|
221
221
|
.tools-box {
|
|
222
222
|
|
|
223
|
-
>
|
|
223
|
+
> button {
|
|
224
224
|
display: flex;
|
|
225
225
|
flex-direction: row;
|
|
226
226
|
flex-wrap: wrap;
|
|
@@ -229,15 +229,11 @@ export const ChatList: IStyledComponentBase<
|
|
|
229
229
|
margin-top: 8px;
|
|
230
230
|
padding: 0;
|
|
231
231
|
list-style: none;
|
|
232
|
-
gap:
|
|
232
|
+
gap: 0px;
|
|
233
233
|
|
|
234
234
|
&:hover{
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
.agent-info-avatar-resource {
|
|
238
|
-
transition: margin-left 0.3s ease-in;
|
|
239
|
-
margin-left: 0px;
|
|
240
|
-
}
|
|
235
|
+
transition: gap 0.3s ease-in;
|
|
236
|
+
gap: 6px;
|
|
241
237
|
}
|
|
242
238
|
}
|
|
243
239
|
}
|
|
@@ -282,7 +278,6 @@ export const ChatList: IStyledComponentBase<
|
|
|
282
278
|
}
|
|
283
279
|
|
|
284
280
|
.step-title {
|
|
285
|
-
line-height: 0.75rem;
|
|
286
281
|
overflow: hidden;
|
|
287
282
|
text-overflow: ellipsis;
|
|
288
283
|
display: -webkit-box;
|
|
@@ -292,8 +287,10 @@ export const ChatList: IStyledComponentBase<
|
|
|
292
287
|
}
|
|
293
288
|
}
|
|
294
289
|
|
|
295
|
-
|
|
296
|
-
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.step-actions {
|
|
293
|
+
margin-top: 16px;
|
|
297
294
|
display: flex;
|
|
298
295
|
gap: 6px;
|
|
299
296
|
|
|
@@ -303,7 +300,6 @@ export const ChatList: IStyledComponentBase<
|
|
|
303
300
|
align-items: center;
|
|
304
301
|
}
|
|
305
302
|
}
|
|
306
|
-
}
|
|
307
303
|
|
|
308
304
|
.markdown img {
|
|
309
305
|
max-width: 70%;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IconButton, Row } from '@stack-spot/citric-react'
|
|
2
2
|
import { useCurrentChat, useCurrentChatState, useWidget } from '../../context/hooks'
|
|
3
3
|
import { useMessageInputDictionary } from './dictionary'
|
|
4
|
+
import { ModelSwitcher } from './ModelSwitcher'
|
|
4
5
|
import { SelectContent } from './SelectContent'
|
|
5
6
|
import { SelectionBarWrapper } from './styled'
|
|
6
7
|
|
|
@@ -29,6 +30,7 @@ export const ButtonBar = ({ onSend, isLoading }: SelectionBarProps) => {
|
|
|
29
30
|
<IconButton icon="Code" appearance="square" aria-label={t.code} title={t.code} onClick={() => widget.set('panel', 'editor')} />
|
|
30
31
|
)}
|
|
31
32
|
</Row>
|
|
33
|
+
<ModelSwitcher />
|
|
32
34
|
{isLoading ? (
|
|
33
35
|
<IconButton
|
|
34
36
|
icon="Stop"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CitricIconOutline, CitricIconSocial } from '@stack-spot/citric-icons'
|
|
2
|
+
import { AsyncContent, Button, Column, FieldGroup, Icon, Input, Row } from '@stack-spot/citric-react'
|
|
3
|
+
import { SelectionList } from '@stack-spot/portal-components/SelectionList'
|
|
4
|
+
import { agentToolsClient, genAiInferenceClient } from '@stack-spot/portal-network'
|
|
5
|
+
import { useMemo, useState } from 'react'
|
|
6
|
+
import { CSSProperties } from 'styled-components'
|
|
7
|
+
import { useCurrentChat, useCurrentChatState } from '../../../context/hooks'
|
|
8
|
+
import { useMessageInputDictionary } from '../dictionary'
|
|
9
|
+
import { RowWrapperStyled, stylesModelSwitcher } from '../styled'
|
|
10
|
+
import { getListModelsData, handleFilterTypeModel, providerIcon } from './utils'
|
|
11
|
+
|
|
12
|
+
export const ModelSwitcher = () => {
|
|
13
|
+
const t = useMessageInputDictionary()
|
|
14
|
+
const agentCurrentChat = useCurrentChatState('agent')
|
|
15
|
+
const chat = useCurrentChat()
|
|
16
|
+
const [filter, setFilter] = useState('')
|
|
17
|
+
const [visibleMenu, setVisibleMenu] = useState(false)
|
|
18
|
+
const [agentData, isLoadingAgentData] = agentToolsClient.agent.useStatefulQuery({ agentId: agentCurrentChat?.id || '' })
|
|
19
|
+
const [models, isLoadingModels] =
|
|
20
|
+
genAiInferenceClient.listModels.useStatefulQuery({ pageSize: 999, active: true })
|
|
21
|
+
|
|
22
|
+
const { modelName, modelProviderType, listItemsData } =
|
|
23
|
+
getListModelsData(chat, setVisibleMenu, agentData, models)
|
|
24
|
+
|
|
25
|
+
const data = useMemo(() => {
|
|
26
|
+
const items = listItemsData ?? []
|
|
27
|
+
return handleFilterTypeModel(items, t, filter)
|
|
28
|
+
}, [agentCurrentChat?.id, listItemsData, filter])
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<RowWrapperStyled>
|
|
32
|
+
<AsyncContent loading={isLoadingAgentData && isLoadingModels}>
|
|
33
|
+
<Button
|
|
34
|
+
className="button-select-model"
|
|
35
|
+
colorScheme="light"
|
|
36
|
+
size="sm"
|
|
37
|
+
aria-label={t.agent}
|
|
38
|
+
title={t.agent}
|
|
39
|
+
onClick={() => setVisibleMenu(state => !state)}
|
|
40
|
+
>
|
|
41
|
+
<Icon
|
|
42
|
+
icon={providerIcon[modelProviderType as CitricIconOutline | CitricIconSocial]}
|
|
43
|
+
group={modelProviderType === 'stackspot' ? 'outline' : 'social'}
|
|
44
|
+
/>
|
|
45
|
+
{modelName}
|
|
46
|
+
<Icon icon="ChevronDown" group="fill" size="sm" />
|
|
47
|
+
</Button>
|
|
48
|
+
</AsyncContent>
|
|
49
|
+
<SelectionList
|
|
50
|
+
id="menuModelSwitcher"
|
|
51
|
+
items={data || []}
|
|
52
|
+
visible={visibleMenu}
|
|
53
|
+
onHide={() => setVisibleMenu(false)}
|
|
54
|
+
showListAsCard
|
|
55
|
+
style={stylesModelSwitcher.selection as CSSProperties}
|
|
56
|
+
before={
|
|
57
|
+
<Column>
|
|
58
|
+
<FieldGroup fullWidth style={{ marginTop: '8px' }}>
|
|
59
|
+
<Icon icon="Search" />
|
|
60
|
+
<Input type="search" value={filter} onChange={(value) => (setFilter(value))} />
|
|
61
|
+
</FieldGroup>
|
|
62
|
+
{!data.length ? <Row m="16px 8px">{t.nothingFound}</Row> : undefined}
|
|
63
|
+
</Column>
|
|
64
|
+
}
|
|
65
|
+
/>
|
|
66
|
+
</RowWrapperStyled>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { CitricIconOutline, CitricIconSocial } from '@stack-spot/citric-icons'
|
|
2
|
+
import { IconBox } from '@stack-spot/citric-react'
|
|
3
|
+
import { AgentModel } from '@stack-spot/portal-network/api/agent-tools'
|
|
4
|
+
import { LlmModelsResponse, PaginatedResponseLlmModelsResponse } from '@stack-spot/portal-network/api/genAiInference'
|
|
5
|
+
import { theme } from '@stack-spot/portal-theme'
|
|
6
|
+
import { Dispatch, ReactElement } from 'react'
|
|
7
|
+
import { ChatState } from '../../../state/ChatState'
|
|
8
|
+
|
|
9
|
+
export interface ItemProps {
|
|
10
|
+
active?: boolean,
|
|
11
|
+
label?: string,
|
|
12
|
+
icon?: ReactElement,
|
|
13
|
+
self_hosted?: boolean,
|
|
14
|
+
onClick?: VoidFunction,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const providerIcon: Record<string, CitricIconSocial | CitricIconOutline> = {
|
|
18
|
+
openai: 'OpenAI',
|
|
19
|
+
bedrock: 'AWSBedrock',
|
|
20
|
+
azure: 'Azure',
|
|
21
|
+
stackspot: 'StackSpot',
|
|
22
|
+
gemini: 'Gemini',
|
|
23
|
+
deepseek: 'DeepSeek',
|
|
24
|
+
anthropic: 'Anthropic',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getListModelsData(
|
|
28
|
+
chat: ChatState,
|
|
29
|
+
setVisibleMenu: Dispatch<React.SetStateAction<boolean>>,
|
|
30
|
+
agent?: AgentModel,
|
|
31
|
+
models?: PaginatedResponseLlmModelsResponse) {
|
|
32
|
+
|
|
33
|
+
const chatSelectedModelId = chat.get('selected_model_id')
|
|
34
|
+
|
|
35
|
+
const listModelsToShow = agent?.visibility_level !== 'built_in' && !!agent?.model_id ?
|
|
36
|
+
models?.items.filter((model) => agent?.available_llm_models?.find((modelAvailable) => modelAvailable.model_id === model.id)) :
|
|
37
|
+
models?.items
|
|
38
|
+
|
|
39
|
+
const modelAvailableDefault = agent?.available_llm_models?.find((model) => (model.is_default || model.model_id === agent.model_id))
|
|
40
|
+
const modelListData = parseModelList(chat, setVisibleMenu, listModelsToShow, modelAvailableDefault?.model_id)
|
|
41
|
+
|
|
42
|
+
if (agent?.visibility_level === 'built_in' || !agent?.model_id) {
|
|
43
|
+
|
|
44
|
+
const modelDefaultProviderType = models?.items.find((modelAccount) =>
|
|
45
|
+
chatSelectedModelId ? modelAccount.id === chatSelectedModelId :
|
|
46
|
+
modelAccount.resources.find((resource) => resource.is_default))?.model_configuration.provider.provider_type
|
|
47
|
+
|
|
48
|
+
const modelDefaultActive = modelListData.find((model) => model?.active)
|
|
49
|
+
|
|
50
|
+
return { modelName: modelDefaultActive?.label, modelProviderType: modelDefaultProviderType, listItemsData: modelListData }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const modelAccount = models?.items.find((modelAccount) => modelAccount.id === (chatSelectedModelId || agent.model_id))
|
|
54
|
+
const modelProviderType = modelAccount?.model_configuration.provider.provider_type
|
|
55
|
+
const modelSelectedName = modelAccount?.display_name || modelAvailableDefault?.model_name || agent?.model_name
|
|
56
|
+
|
|
57
|
+
const listItemsData =
|
|
58
|
+
modelListData && modelListData?.length > 0 ? modelListData :
|
|
59
|
+
[{
|
|
60
|
+
active: true,
|
|
61
|
+
label: modelSelectedName || '',
|
|
62
|
+
icon: <IconBox icon={providerIcon[modelProviderType || 'stackspot']}
|
|
63
|
+
appearance="square"
|
|
64
|
+
group={modelProviderType === 'stackspot' ? 'outline' : 'social'} />,
|
|
65
|
+
self_hosted: modelAccount?.self_hosted || false,
|
|
66
|
+
onClick: () => {
|
|
67
|
+
chat.set('selected_model_id', modelAvailableDefault?.model_id)
|
|
68
|
+
setVisibleMenu(false)
|
|
69
|
+
},
|
|
70
|
+
}]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
return { modelName: modelSelectedName, modelProviderType, listItemsData }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseModelList(
|
|
77
|
+
chat: ChatState,
|
|
78
|
+
setVisibleMenu: Dispatch<React.SetStateAction<boolean>>,
|
|
79
|
+
listModel?: LlmModelsResponse[],
|
|
80
|
+
modelAvailableDefaultId?: string,
|
|
81
|
+
) {
|
|
82
|
+
|
|
83
|
+
const data = Array<ItemProps>()
|
|
84
|
+
const chatModelId = chat.get('selected_model_id')
|
|
85
|
+
|
|
86
|
+
listModel?.forEach((model) => {
|
|
87
|
+
data.push({
|
|
88
|
+
active: chatModelId ? chatModelId === model.id : modelAvailableDefaultId === model.id ||
|
|
89
|
+
model.resources?.some((resource) => resource.is_default && resource.name === 'agents'),
|
|
90
|
+
label: model?.display_name || 'LLM',
|
|
91
|
+
icon: <IconBox
|
|
92
|
+
style={{ backgroundColor: theme.color.light[300] }}
|
|
93
|
+
appearance="square"
|
|
94
|
+
icon={providerIcon[model.model_configuration.provider.provider_type]}
|
|
95
|
+
group={model.model_configuration.provider.provider_type === 'stackspot' ? 'outline' : 'social'}
|
|
96
|
+
/>,
|
|
97
|
+
self_hosted: model.self_hosted,
|
|
98
|
+
onClick: () => {
|
|
99
|
+
chat.set('selected_model_id', model.id)
|
|
100
|
+
setVisibleMenu(false)
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
return data
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function handleFilterTypeModel(items: ItemProps[], t: Record<string, string>, filter?: string) {
|
|
108
|
+
const filterLower = filter?.toLocaleLowerCase()
|
|
109
|
+
const filteredItens = items.filter((item) => filterLower ? item?.label?.toLocaleLowerCase().includes(filterLower) : item)
|
|
110
|
+
|
|
111
|
+
const { selfHosted, hosted } = filteredItens.reduce(
|
|
112
|
+
(acc, model) => {
|
|
113
|
+
if (model?.self_hosted) {
|
|
114
|
+
acc.selfHosted.push(model)
|
|
115
|
+
} else {
|
|
116
|
+
acc.hosted.push(model)
|
|
117
|
+
}
|
|
118
|
+
return acc
|
|
119
|
+
},
|
|
120
|
+
{ selfHosted: [], hosted: [] } as { selfHosted: typeof items, hosted: typeof items },
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const sections = [
|
|
124
|
+
{
|
|
125
|
+
label: t.hosted,
|
|
126
|
+
children: hosted,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: t.selfHosted,
|
|
130
|
+
children: selfHosted,
|
|
131
|
+
},
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
const filteredByHavingItens = sections
|
|
135
|
+
.filter(section => section.children.length > 0)
|
|
136
|
+
.map(section => ({
|
|
137
|
+
type: 'section',
|
|
138
|
+
label: section.label,
|
|
139
|
+
children: section.children,
|
|
140
|
+
}))
|
|
141
|
+
|
|
142
|
+
return filteredByHavingItens
|
|
143
|
+
}
|
|
@@ -83,7 +83,7 @@ export const SelectContent = () => {
|
|
|
83
83
|
if (!hasFeatureButtons) return null
|
|
84
84
|
|
|
85
85
|
return itemConfigs.length > 1 ? (
|
|
86
|
-
<MenuOverlay items={listItems} position="
|
|
86
|
+
<MenuOverlay items={listItems} position="right" bgLevel={500} roundedItems menuClass="menu-citric-hr" alignment="end">
|
|
87
87
|
<IconButton icon="Plus" />
|
|
88
88
|
</MenuOverlay>
|
|
89
89
|
) : (
|
|
@@ -35,6 +35,9 @@ const dictionary = {
|
|
|
35
35
|
chatAgent: 'Agents',
|
|
36
36
|
uploadSuccessStatus: 'File sent successfully',
|
|
37
37
|
chatViewMenu: 'Chat options menu',
|
|
38
|
+
nothingFound: 'Nothing Found',
|
|
39
|
+
hosted: 'Hosted',
|
|
40
|
+
selfHosted: 'Self Hosted',
|
|
38
41
|
},
|
|
39
42
|
pt: {
|
|
40
43
|
stack: 'Selecionar stack',
|
|
@@ -70,6 +73,9 @@ const dictionary = {
|
|
|
70
73
|
chatAgent: 'Agentes',
|
|
71
74
|
uploadSuccessStatus: 'Arquivo anexado com sucesso',
|
|
72
75
|
chatViewMenu: 'Menu de opções do chat',
|
|
76
|
+
nothingFound: 'Nada encontrado',
|
|
77
|
+
hosted: 'Hospedado',
|
|
78
|
+
selfHosted: 'Auto-hospedado',
|
|
73
79
|
},
|
|
74
80
|
} satisfies Dictionary
|
|
75
81
|
|