@stack-spot/ai-chat-widget 2.2.0 → 2.2.1-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.
Files changed (77) hide show
  1. package/CHANGELOG.md +32 -4
  2. package/dist/app-metadata.json +5 -5
  3. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  4. package/dist/chat-interceptors/send-message.js +125 -1
  5. package/dist/chat-interceptors/send-message.js.map +1 -1
  6. package/dist/state/ChatEntry.d.ts +1 -1
  7. package/dist/state/ChatEntry.d.ts.map +1 -1
  8. package/dist/state/ChatEntry.js +2 -1
  9. package/dist/state/ChatEntry.js.map +1 -1
  10. package/dist/state/ChatState.d.ts +8 -0
  11. package/dist/state/ChatState.d.ts.map +1 -1
  12. package/dist/state/ChatState.js.map +1 -1
  13. package/dist/utils/chat.d.ts.map +1 -1
  14. package/dist/utils/chat.js +1 -0
  15. package/dist/utils/chat.js.map +1 -1
  16. package/dist/utils/planning-tool.d.ts +17 -0
  17. package/dist/utils/planning-tool.d.ts.map +1 -0
  18. package/dist/utils/planning-tool.js +32 -0
  19. package/dist/utils/planning-tool.js.map +1 -0
  20. package/dist/utils/update-tool-step.d.ts +3 -0
  21. package/dist/utils/update-tool-step.d.ts.map +1 -0
  22. package/dist/utils/update-tool-step.js +23 -0
  23. package/dist/utils/update-tool-step.js.map +1 -0
  24. package/dist/views/Chat/ChatMessage.d.ts +1 -1
  25. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  26. package/dist/views/Chat/ChatMessage.js +21 -6
  27. package/dist/views/Chat/ChatMessage.js.map +1 -1
  28. package/dist/views/Chat/StepsList.d.ts +12 -2
  29. package/dist/views/Chat/StepsList.d.ts.map +1 -1
  30. package/dist/views/Chat/StepsList.js +155 -18
  31. package/dist/views/Chat/StepsList.js.map +1 -1
  32. package/dist/views/Chat/styled.d.ts.map +1 -1
  33. package/dist/views/Chat/styled.js +17 -10
  34. package/dist/views/Chat/styled.js.map +1 -1
  35. package/dist/views/MessageInput/ButtonBar.d.ts.map +1 -1
  36. package/dist/views/MessageInput/ButtonBar.js +2 -1
  37. package/dist/views/MessageInput/ButtonBar.js.map +1 -1
  38. package/dist/views/MessageInput/ModelSwitcher.d.ts +2 -0
  39. package/dist/views/MessageInput/ModelSwitcher.d.ts.map +1 -0
  40. package/dist/views/MessageInput/ModelSwitcher.js +77 -0
  41. package/dist/views/MessageInput/ModelSwitcher.js.map +1 -0
  42. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  43. package/dist/views/MessageInput/styled.d.ts +3 -0
  44. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  45. package/dist/views/MessageInput/styled.js +12 -0
  46. package/dist/views/MessageInput/styled.js.map +1 -1
  47. package/dist/views/Steps/FlowChart/NodeStep.js +1 -1
  48. package/dist/views/Steps/FlowChart/NodeStep.js.map +1 -1
  49. package/dist/views/Steps/FlowChart/layout.d.ts +1 -1
  50. package/dist/views/Steps/FlowChart/layout.d.ts.map +1 -1
  51. package/dist/views/Steps/FlowChart/layout.js +1 -0
  52. package/dist/views/Steps/FlowChart/layout.js.map +1 -1
  53. package/dist/views/Steps/FlowChart/types.d.ts +1 -1
  54. package/dist/views/Steps/FlowChart/types.d.ts.map +1 -1
  55. package/dist/views/Steps/StepModal.js +2 -2
  56. package/dist/views/Steps/StepModal.js.map +1 -1
  57. package/dist/views/Steps/dictionary.d.ts +1 -1
  58. package/dist/views/Steps/utils.d.ts +1 -1
  59. package/dist/views/Steps/utils.d.ts.map +1 -1
  60. package/package.json +3 -3
  61. package/src/app-metadata.json +5 -5
  62. package/src/chat-interceptors/send-message.ts +137 -2
  63. package/src/state/ChatEntry.ts +2 -1
  64. package/src/state/ChatState.ts +8 -0
  65. package/src/utils/chat.ts +1 -0
  66. package/src/utils/planning-tool.ts +41 -0
  67. package/src/utils/update-tool-step.tsx +27 -0
  68. package/src/views/Chat/ChatMessage.tsx +25 -5
  69. package/src/views/Chat/StepsList.tsx +337 -44
  70. package/src/views/Chat/styled.ts +17 -10
  71. package/src/views/MessageInput/ButtonBar.tsx +2 -0
  72. package/src/views/MessageInput/ModelSwitcher.tsx +127 -0
  73. package/src/views/MessageInput/styled.ts +12 -0
  74. package/src/views/Steps/FlowChart/NodeStep.tsx +1 -1
  75. package/src/views/Steps/FlowChart/layout.ts +1 -0
  76. package/src/views/Steps/FlowChart/types.ts +1 -1
  77. package/src/views/Steps/StepModal.tsx +2 -2
@@ -1,96 +1,389 @@
1
- import { Icon } from '@stack-spot/citric-icons'
2
- import { Button, ProgressCircular, Text } from '@stack-spot/citric-react'
1
+ import { Accordion, Badge, Button, Card, Column, Divider, Icon, IconBox, ImageWithFallback, ProgressCircular, Row, Skeleton, Text } from '@stack-spot/citric-react'
3
2
  import { AnimatedHeight } from '@stack-spot/portal-components/AnimatedHeight'
4
- import { ChatStep, StepChatStep } from '@stack-spot/portal-network'
3
+ import { ChatStep, StepChatStep, ToolChatStep } from '@stack-spot/portal-network'
5
4
  import { theme } from '@stack-spot/portal-theme'
6
5
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
7
- import { findLastIndex } from 'lodash'
8
- import { useState } from 'react'
9
- import { useWidget } from '../../context/hooks'
6
+ import { findLast, findLastIndex } from 'lodash'
7
+ import React, { useEffect, useMemo } from 'react'
8
+ import styled from 'styled-components'
9
+ import { Markdown } from '../../components/Markdown'
10
+ import { useChat, useChatMessages, useCurrentChat, useCurrentChatMessages, useWidget } from '../../context/hooks'
11
+ import { ChatEntry } from '../../state/ChatEntry'
12
+ import { planningToolDictionaryHelper } from '../../utils/planning-tool'
13
+ import { updateToolStep } from '../../utils/update-tool-step'
14
+ import { onCopyCode } from './events'
10
15
 
11
16
  interface Props {
12
17
  steps: ChatStep[],
13
18
  messageId: number,
14
19
  chatId: string,
20
+ userHasAlreadyAnswered?: boolean,
21
+ }
22
+
23
+ interface StepChatStepWithTarget extends Omit<StepChatStep, 'status' | 'id' | 'type'> {
24
+ status: 'pending' | 'running' | 'success' | 'error' | 'target' | 'awaiting_approval',
15
25
  }
16
26
 
17
27
  interface StepProps {
18
- step: StepChatStep,
28
+ step: StepChatStepWithTarget,
19
29
  index: number,
20
- total: number,
30
+ total?: number,
31
+ totalTools?: number,
32
+ isAllDone?: boolean,
21
33
  onClick?: () => void,
22
34
  }
23
35
 
24
- function getStatusIcon(status: ChatStep['status']) {
36
+ function getStatusIcon(status: StepChatStepWithTarget['status'] | 'target', isDone?: boolean) {
25
37
  const iconProps = { style: { color: theme.color.light[700] }, size: 'xs' } as const
26
38
  switch (status) {
27
39
  case 'error': return <Icon group="fill" icon="TimesCircle" {...iconProps} />
28
40
  case 'success': return <Icon group="fill" icon="CheckCircle" {...iconProps} />
29
- case 'pending': return <Icon icon="Spaces" {...iconProps} />
41
+ case 'pending': return <Icon group="fill" icon="Circle" {...iconProps} style={{ color: theme.color.light[600] }} />
42
+ case 'awaiting_approval': return <Icon group="fill" icon="ExclamationTriangle" {...iconProps} />
43
+ case 'target': return <Icon icon="Target" {...iconProps} style={{ color: isDone ? theme.color.light[700] : theme.color.light[600] }} />
30
44
  case 'running': return <ProgressCircular className="loading" colorScheme="inverse" size="xs" />
31
45
  }
32
46
  }
33
47
 
34
- const Step = ({ step, index, total, onClick }: StepProps) => {
48
+ const StepAccordionHeader = ({ step, index, expand }: Pick<StepProps, 'step' | 'index'> & { expand: React.ReactElement }) => {
49
+ const t = useTranslate(dictionary)
50
+ return <Row gap="8px">
51
+ {expand}
52
+ {step.status === 'target' ? <Text className="step-title" appearance="body2" color="light.700">{t.planGoal}:</Text> :
53
+ <Badge colorScheme="inverse" appearance="square">
54
+ {t.step} {index}
55
+ </Badge>}
56
+ <Text className="step-title" appearance="body2">
57
+ {step.input}
58
+ </Text>
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>}
64
+ </Row>
65
+ }
66
+
67
+ const StyledCard = styled(Card)`
68
+ &:hover {
69
+ background-color: ${theme.color.light[500]}
70
+ }
71
+ `
72
+
73
+ const Step = ({ step, index, onClick, total, totalTools, isAllDone }: StepProps) => {
35
74
  const t = useTranslate(dictionary)
75
+ const status = getStatusIcon(step.status, isAllDone)
76
+ const hasTools = step.attempts?.[0]?.tools && step.attempts?.[0]?.tools?.length > 0
77
+
36
78
  return (
37
79
  <li tabIndex={onClick ? 0 : undefined} onClick={onClick} role={onClick ? 'button' : 'listitem'}>
38
- <div className="step-status-icon">{getStatusIcon(step.status)}</div>
39
- <Text className="step-title" appearance="microtext1" color="light.700">
40
- {t.step} {index}/{total}: {step.input}
41
- </Text>
80
+ <Row gap="4px" alignItems="center">
81
+ <div className="step-status-icon">{status}</div>
82
+ <StyledCard p="8px" w="80%">
83
+ <Accordion header={expand => <StepAccordionHeader step={step} index={index} expand={expand} />}>
84
+ <Column pt="12px">
85
+ {total ?
86
+ <Row gap="40px">
87
+ <Row gap="4px">
88
+ <Icon icon="Hashtag" size="sm" color="light.700" />
89
+ <Text color="light.700">{t.totalSteps}</Text>
90
+ {total}
91
+ </Row>
92
+ <Row gap="4px">
93
+ <Icon icon="BorderRadius" size="sm" color="light.700" />
94
+ <Text color="light.700">{t.totalTools}</Text>
95
+ {totalTools ?? 0}
96
+ </Row>
97
+ </Row>
98
+ : <>
99
+ <Row pb="8px">
100
+ <Icon icon="Target" size="sm" color="light.700" />
101
+ <Text color="light.700" tag="span" style={{ margin: '0 4px' }}>{t.stepGoal}:</Text>
102
+ <Text tag="span">
103
+ {step.input}
104
+ </Text>
105
+ </Row>
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">
117
+ <Icon icon="BorderRadius" size="sm" color="light.700" />
118
+ <Text color="light.700">{t.noToolToBeUsed}</Text>
119
+ </Row>
120
+ }
121
+ </>
122
+ }
123
+ </Column>
124
+ </Accordion>
125
+ </StyledCard>
126
+ </Row>
42
127
  </li>
43
128
  )
44
129
  }
45
130
 
46
- export const StepsList = ({ steps, chatId, messageId }: Props) => {
131
+ export const StepsPlaceholder = () => {
132
+ const t = useTranslate(dictionary)
133
+ return <Card gap="8px">
134
+ <Row gap="8px">
135
+ <ProgressCircular colorScheme="inverse" size="xs" />
136
+ <Text color="light.700">{t.generatingPlan}</Text>
137
+ </Row>
138
+ <Text color="light.700">
139
+ {t.analyzingRequirements}
140
+ </Text>
141
+ <Row gap="12px">
142
+ <Skeleton height="31px" width="148px" bgLevel={600} />
143
+ <Skeleton height="31px" width="148px" bgLevel={600} />
144
+ <Skeleton height="31px" width="148px" bgLevel={600} />
145
+ <Skeleton height="31px" width="148px" bgLevel={600} />
146
+ </Row>
147
+ </Card>
148
+ }
149
+
150
+ const AwaitingApproval = ({ customApproveText, chatId }: { chatId: string, customApproveText?: string }) => {
151
+ const t = useTranslate(dictionary)
152
+ const chat = useChat(chatId)
153
+
154
+ const onAnswer = (response: string) => {
155
+ chat.pushMessage(ChatEntry.createUserEntry('', false, undefined, undefined, response))
156
+ }
157
+
158
+ return <>
159
+ <Row gap="8px">
160
+ <Button colorScheme="light" onClick={() => onAnswer(t.cancel)}>
161
+ <Row gap="8px">
162
+ <Icon icon="Stop" />
163
+ {t.cancel}
164
+ </Row>
165
+ </Button>
166
+
167
+ <Button colorScheme="inverse" onClick={() => onAnswer(customApproveText ?? t.approve)}>
168
+ <Row gap="8px">
169
+ <Icon group="fill" icon="Play" />
170
+ {customApproveText ?? t.approve}
171
+ </Row>
172
+ </Button>
173
+ </Row>
174
+ </>
175
+ }
176
+
177
+ export const ToolStepsList = ({ toolStep, messageId, chatId }: { toolStep: ToolChatStep, messageId: number, chatId: string }) => {
178
+ const t = useTranslate(dictionary)
179
+ const chat = useCurrentChat()
180
+ const messages = useCurrentChatMessages()
181
+ const inputParsed = `\`\`\`json
182
+ ${JSON.stringify(toolStep?.input, null, 2)}
183
+ \`\`\``
184
+
185
+ const tool = useMemo(() => {
186
+ if (!toolStep) return undefined
187
+ return toolStep.attempts?.[0].tools?.[0]
188
+ }, [toolStep])
189
+
190
+ useEffect(() => {
191
+ if (!toolStep) return undefined
192
+ const executionId = toolStep.attempts?.[0].tools?.[0].executionId
193
+ if (!executionId) return
194
+
195
+ updateToolStep(messages, executionId, toolStep.status)
196
+
197
+ }, [messages, toolStep, toolStep.status])
198
+
199
+ return <>
200
+ {toolStep && tool ? <AnimatedHeight>
201
+ <div className="steps">
202
+ <Badge colorPalette="yellow" appearance="square">
203
+ <Icon icon="StopWatch" />
204
+ <Text>{tool.name} {t.keepWorking}</Text>
205
+ </Badge>
206
+ <Card mt="16px" gap="8px" bgLevel={500}>
207
+ <Row>
208
+ <ImageWithFallback src={tool.image} width="32px" fallback={<IconBox appearance="circle" icon="StackSpot" />} />
209
+ <Text>{tool.name}</Text>
210
+ </Row>
211
+
212
+ <Text>
213
+ {toolStep.user_question}
214
+ </Text>
215
+
216
+ <Accordion header={expand => <Row gap="8px" mb="4px">
217
+ <Card p="4px" bgLevel={400}>{expand}</Card>
218
+ <Text > {t.viewDetails} </Text>
219
+ </Row>}>
220
+ <Markdown onCopyCode={(code) => onCopyCode(code, `${messageId}`, chat)}>
221
+ {inputParsed}
222
+ </Markdown>
223
+ </Accordion>
224
+
225
+ <AwaitingApproval customApproveText={t.approveTool} chatId={chatId} />
226
+ </Card>
227
+ </div>
228
+ </AnimatedHeight> : null}
229
+ </>
230
+ }
231
+
232
+ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }: Props) => {
47
233
  const t = useTranslate(dictionary)
48
- const [isExpanded, setExpanded] = useState(false)
49
- const actualSteps = steps.filter(s => s.type === 'step')
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
+
250
+ const planning = steps.filter(s => s.type === 'planning')
251
+
252
+ useEffect(() => {
253
+ actualSteps.map((item) => {
254
+ const executionId = item.attempts[0]?.tools?.[0].executionId
255
+ if (executionId) {
256
+ planningToolDictionaryHelper.setMessageIdPlanningStepToolExecutionId(`${messageId}`, executionId)
257
+ }
258
+ })
259
+ }, [actualSteps, messageId])
260
+
261
+ const toolsStep = findLast(steps, s => s.type === 'tool')
262
+
263
+ useEffect(() => {
264
+ const executionId = toolsStep?.attempts?.[0]?.tools?.[0]?.executionId
265
+ if (executionId) {
266
+ planningToolDictionaryHelper.setMessageIdToolStepToolExecutionId(`${messageId}`, executionId)
267
+ }
268
+ }, [toolsStep, messageId])
269
+
270
+ const planningGoal = planning?.[0]?.goal
271
+ const isLastStepDone = actualSteps[actualSteps.length - 1]?.status !== 'running' &&
272
+ actualSteps[actualSteps.length - 1]?.status !== 'pending'
273
+ const totalTools = useMemo(() => actualSteps?.reduce((sum, step) => {
274
+ const firstAttempt = step.attempts && step.attempts[0]
275
+ const toolsCount = firstAttempt.tools?.length ?? 0
276
+ return sum + toolsCount
277
+ }, 0), [steps])
278
+
50
279
  let currentStepIndex = findLastIndex(actualSteps, s => s.status === 'running' || s.status === 'success')
51
280
  if (currentStepIndex === -1) currentStepIndex = 0
52
- const widget = useWidget()
53
281
 
54
- function openToolsPanel() {
55
- widget.set('currentMessageInPanel', { chatId, messageId })
56
- widget.set('panel', 'steps')
57
- }
58
-
59
- return (
60
- <AnimatedHeight>
282
+ return (<>
283
+ {actualSteps.length > 0 ? <AnimatedHeight>
61
284
  <div className="steps">
62
- <ul>
63
- {isExpanded
64
- ? actualSteps.map((s, i) => <Step step={s} key={i} index={i + 1} total={actualSteps.length} />)
65
- : <Step
66
- step={actualSteps[currentStepIndex]}
67
- index={currentStepIndex + 1}
68
- total={actualSteps.length}
69
- onClick={() => setExpanded(true)}
70
- />
71
- }
285
+ <Row gap="14px" mb="8px" ml="5px">
286
+ <Icon icon="Target" size="sm" color="light.600" />
287
+ <Text>{t.executionPlan}</Text>
288
+ </Row>
289
+
290
+ <ul className="steps-list">
291
+ {actualSteps.map((s, i) => <Step step={s} key={i} index={i + 1} />)}
292
+
293
+ <Step
294
+ step={{ status: 'target', input: planningGoal, attempts: [] }}
295
+ index={currentStepIndex + 1}
296
+ total={actualSteps.length}
297
+ totalTools={totalTools}
298
+ isAllDone={isLastStepDone}
299
+ />
72
300
  </ul>
73
- {isExpanded && <div className="step-actions">
74
- <Button colorScheme="light" size="sm" onClick={() => setExpanded(false)}>{t.hideSteps}</Button>
75
- <Button colorScheme="light" size="sm" className="icon-button details" onClick={openToolsPanel}>
76
- <Icon group="fill" icon="Play" size="xs" />
77
- {t.detailed}
78
- </Button>
79
- </div>}
301
+
302
+ <Column gap="8px" mt="8px">
303
+ <Divider colorScheme="light" />
304
+ <Text color="light.700">{planning?.[0]?.user_question}</Text>
305
+ {!userHasAlreadyAnswered && planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval chatId={chatId} />}
306
+ </Column>
307
+
80
308
  </div>
81
- </AnimatedHeight>
309
+ </AnimatedHeight> : null}
310
+
311
+ {toolsStep && toolsStep.status === 'awaiting_approval' && !userHasAlreadyAnswered &&
312
+ <ToolStepsList toolStep={toolsStep} messageId={messageId} chatId={chatId} />}
313
+ </>
82
314
  )
83
315
  }
84
316
 
317
+ export const ViewToolsDetails = ({ chatId }: { chatId: string }) => {
318
+ const t = useTranslate(dictionary)
319
+ const messages = useChatMessages(chatId)
320
+ const messageId = useMemo(() => {
321
+ const messageWithPlanning = findLast(messages, (message) => {
322
+ const steps = message.getValue().steps
323
+ const planningStep = steps?.find((step) => step.type === 'planning' && step.status === 'success')
324
+ return planningStep ? true : false
325
+ })
326
+ return messageWithPlanning?.id
327
+
328
+ }, [messages])
329
+ const widget = useWidget()
330
+
331
+ function openToolsPanel() {
332
+ if (messageId) {
333
+ widget.set('currentMessageInPanel', { chatId, messageId })
334
+ widget.set('panel', 'steps')
335
+ }
336
+ }
337
+
338
+ return <>
339
+ <div className="step-actions">
340
+ <Button colorScheme="light" size="sm" className="icon-button details" onClick={openToolsPanel}>
341
+ <Icon group="fill" icon="Play" size="xs" />
342
+ {t.detailed}
343
+ </Button>
344
+ </div>
345
+ </>
346
+ }
347
+
85
348
  const dictionary = {
86
349
  en: {
87
350
  step: 'Step',
88
351
  hideSteps: 'Hide steps',
89
352
  detailed: 'View detailed mode',
353
+ generatingPlan: 'Generating execution plan...',
354
+ analyzingRequirements: 'Analyzing task requirements',
355
+ executionPlan: 'Execution Plan',
356
+ planGoal: 'Plan Goal',
357
+ toolsToBeExecuted: 'Tools to be executed',
358
+ totalSteps: 'Total Steps',
359
+ totalTools: 'Total Tools',
360
+ stepGoal: 'Step Goal',
361
+ cancel: 'Cancel execution',
362
+ approve: 'Approve & Execute Plan',
363
+ keepWorking: 'will keep working after your answer',
364
+ viewDetails: 'View details',
365
+ approveTool: 'Approve execution',
366
+ pendingReview: 'Pending review',
367
+ noToolToBeUsed: 'No tool will be needed',
90
368
  },
91
369
  pt: {
92
370
  step: 'Passo',
93
371
  hideSteps: 'Esconder passos',
94
372
  detailed: 'Ver modo detalhado',
373
+ generatingPlan: 'Gerando plano de execução...',
374
+ analyzingRequirements: 'Analisando os requisitos da task',
375
+ executionPlan: 'Plano de Execução',
376
+ planGoal: 'Finalidade do Plano',
377
+ toolsToBeExecuted: 'Tools a serem executadas',
378
+ totalSteps: 'Total de Passos',
379
+ totalTools: 'Total de Tools',
380
+ stepGoal: 'Objetivo do passo',
381
+ cancel: 'Cancelar execução',
382
+ approve: 'Aprovar & Executar plano',
383
+ keepWorking: 'continuará trabalhando após a sua resposta',
384
+ viewDetails: 'Ver detalhes',
385
+ approveTool: 'Aprovar execução',
386
+ pendingReview: 'Revisão pendente',
387
+ noToolToBeUsed: 'Nenhuma tool será necessária',
95
388
  },
96
389
  } satisfies Dictionary
@@ -243,20 +243,27 @@ export const ChatList: IStyledComponentBase<
243
243
  }
244
244
 
245
245
  .steps {
246
- ul {
246
+ .steps-list {
247
247
  list-style: none;
248
248
  margin: 0;
249
249
  padding: 0;
250
250
  display: flex;
251
251
  flex-direction: column;
252
252
  gap: 6px;
253
+ }
253
254
 
254
- li {
255
- display: flex;
256
- flex-direction: row;
257
- gap: 4px;
258
- align-items: center;
255
+ .tools-list {
256
+ list-style: disc;
257
+ margin: 0;
258
+ padding-left: 24px;
259
+ display: block;
260
+ ::marker {
261
+ color: ${theme.color.light.contrastText};
262
+ }
263
+ }
259
264
 
265
+ ul {
266
+ li {
260
267
  &[role="button"] {
261
268
  cursor: pointer;
262
269
  }
@@ -275,7 +282,6 @@ export const ChatList: IStyledComponentBase<
275
282
  }
276
283
 
277
284
  .step-title {
278
- line-height: 0.75rem;
279
285
  overflow: hidden;
280
286
  text-overflow: ellipsis;
281
287
  display: -webkit-box;
@@ -285,8 +291,10 @@ export const ChatList: IStyledComponentBase<
285
291
  }
286
292
  }
287
293
 
288
- .step-actions {
289
- margin-top: 8px;
294
+ }
295
+
296
+ .step-actions {
297
+ margin-top: 16px;
290
298
  display: flex;
291
299
  gap: 6px;
292
300
 
@@ -296,7 +304,6 @@ export const ChatList: IStyledComponentBase<
296
304
  align-items: center;
297
305
  }
298
306
  }
299
- }
300
307
 
301
308
  .markdown img {
302
309
  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,127 @@
1
+ import { Button, Icon } from '@stack-spot/citric-react'
2
+ import { SelectionList } from '@stack-spot/portal-components/SelectionList'
3
+ import { agentToolsClient, genAiInferenceClient } from '@stack-spot/portal-network'
4
+ import { AgentLlmModelDto, AgentModel } from '@stack-spot/portal-network/api/agent-tools'
5
+ import { LlmModelsResponse, PaginatedResponseLlmModelsResponse } from '@stack-spot/portal-network/api/genAiInference'
6
+ import { Dispatch, ReactElement, useState } from 'react'
7
+ import { CSSProperties } from 'styled-components'
8
+ import { useCurrentChat, useCurrentChatState } from '../../context/hooks'
9
+ import { useMessageInputDictionary } from './dictionary'
10
+ import { RowWrapperStyled } from './styled'
11
+
12
+ const styles = {
13
+ selection: {
14
+ position: 'absolute',
15
+ bottom: 'calc(100% + 10px)',
16
+ right: '0',
17
+ margin: '0',
18
+ 'ul': {
19
+ margin: '0px',
20
+ },
21
+ },
22
+ }
23
+
24
+ export const ModelSwitcher = () => {
25
+ const t = useMessageInputDictionary()
26
+ const agent = useCurrentChatState('agent')
27
+ const chat = useCurrentChat()
28
+ const [visibleMenu, setVisibleMenu] = useState(false)
29
+ const [agentData] = agentToolsClient.agent.useStatefulQuery({ agentId: agent?.id || '' })
30
+ const [selectedLLMName, setSelectedLLMName] = useState<string | undefined>()
31
+ const [models] = genAiInferenceClient.listModels.useStatefulQuery({ pageSize: 999 })
32
+
33
+ const { modelName, listItems } = getModelData(chat, setSelectedLLMName, setVisibleMenu, selectedLLMName, agentData, models)
34
+
35
+ return (
36
+ <RowWrapperStyled>
37
+ <Button
38
+ className="button-select-model"
39
+ colorScheme="light"
40
+ size="sm"
41
+ aria-label={t.agent}
42
+ title={t.agent}
43
+ onClick={() => setVisibleMenu(state => !state)}
44
+ >
45
+ {modelName}
46
+ <Icon icon="ChevronDown" group="fill" size="sm" />
47
+ </Button>
48
+ <SelectionList
49
+ id="menuModelSwitcher"
50
+ visible={visibleMenu}
51
+ onHide={() => setVisibleMenu(false)}
52
+ items={listItems || []}
53
+ style={styles.selection as CSSProperties}
54
+ />
55
+ </RowWrapperStyled>
56
+ )
57
+ }
58
+
59
+
60
+ function getModelData(
61
+ chat: any,
62
+ setSelectedModelName: Dispatch<React.SetStateAction<string | undefined>>,
63
+ setVisibleMenu: Dispatch<React.SetStateAction<boolean>>,
64
+ selectedModelName?: string,
65
+ agent?: AgentModel,
66
+ models?: PaginatedResponseLlmModelsResponse) {
67
+
68
+ const modelListData = parseModelList(
69
+ setSelectedModelName, setVisibleMenu, chat,
70
+ agent?.visibility_level !== 'built_in' && !!agent?.model_id ? agent?.available_llm_models : models?.items,
71
+ )
72
+
73
+ if (agent?.visibility_level === 'built_in' || !agent?.model_id) {
74
+ const modelDefaultFromList = modelListData.find((model) => model.active)
75
+ return { modelName: modelDefaultFromList?.label, listItems: modelListData }
76
+ }
77
+
78
+ const listItems =
79
+ modelListData && modelListData?.length > 0 ? modelListData :
80
+ [{
81
+ active: true,
82
+ label: agent?.available_llm_models?.find((model) => model.is_default)?.model_name || agent?.model_name || '',
83
+ icon: <Icon icon="StackSpot" />,
84
+ }]
85
+
86
+ const modelSelectedName = selectedModelName || agent?.model_name ||
87
+ agent?.available_llm_models?.find((model) => model.is_default)?.model_name
88
+
89
+ return { modelName: modelSelectedName, listItems }
90
+ }
91
+
92
+ function parseModelList(
93
+ setSelectedModelName: Dispatch<React.SetStateAction<string | undefined>>,
94
+ setVisibleMenu: Dispatch<React.SetStateAction<boolean>>,
95
+ chat: any,
96
+ listModel?: Array<AgentLlmModelDto | LlmModelsResponse>,
97
+ ) {
98
+ const data = Array<{active?: boolean, label: string, icon: ReactElement, onClick: VoidFunction }>()
99
+
100
+ listModel?.forEach((model) => {
101
+ if ('model_id' in model) {
102
+ data.push({
103
+ active: chat.get('selected_model_id') ? chat.get('selected_model_id') === model.model_id : model.is_default,
104
+ label: model?.model_name || 'LLM',
105
+ icon: <Icon icon="Bookmark"/>,
106
+ onClick: () => {
107
+ setSelectedModelName(model.model_name)
108
+ chat.set('selected_model_id', model.model_id)
109
+ setVisibleMenu(false)
110
+ },
111
+ })
112
+ }
113
+ else {
114
+ data.push({
115
+ active: chat.get('selected_model_id') ? chat.get('selected_model_id') === model.id : model.resources[0]?.is_default,
116
+ label: model?.display_name || 'LLM',
117
+ icon: <Icon icon="Bookmark"/>,
118
+ onClick: () => {
119
+ setSelectedModelName(model.display_name)
120
+ chat.set('selected_model_id', model.id)
121
+ setVisibleMenu(false)
122
+ },
123
+ })
124
+ }
125
+ })
126
+ return data
127
+ }
@@ -1,3 +1,4 @@
1
+ import { Flex } from '@citric/core'
1
2
  import { theme } from '@stack-spot/portal-theme'
2
3
  import { styled } from 'styled-components'
3
4
 
@@ -302,3 +303,14 @@ export const MessageInputBox = styled.div`
302
303
  }
303
304
  }
304
305
  `
306
+ export const RowWrapperStyled = styled(Flex)`
307
+ width: 100%;
308
+ justify-content: end;
309
+ margin-right: 4px;
310
+ ul {
311
+ margin: 0;
312
+ }
313
+ .button-select-model {
314
+ border-radius: 15px !important;
315
+ }
316
+ `
@@ -37,7 +37,7 @@ export const NodeStep = ({ data: { step, index, nextStatus, onClick } }: Props)
37
37
  {!!step.attempts[0].tools?.length && <StackedBadge
38
38
  label={t.tools}
39
39
  images={step.attempts[0].tools?.slice(0, 3).map(
40
- tool => ({ key: tool.id, name: tool.name, url: tool.image, icon: <Icon icon="Cog" /> }),
40
+ tool => ({ key: tool.id, name: tool.name ?? '', url: tool.image, icon: <Icon icon="Cog" /> }),
41
41
  )}
42
42
  />}
43
43
  </div>}
@@ -8,6 +8,7 @@ const nodesSizes = {
8
8
  step: stepNodeSize,
9
9
  planning: planningNodeSize,
10
10
  answer: answerNodeSize,
11
+ tool: stepNodeSize,
11
12
  }
12
13
 
13
14
  export function useLayoutedElements(nodes: NodeWithoutLayout[], edges: Edge[]) {