@stack-spot/ai-chat-widget 2.8.5 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/app-metadata.json +3 -3
- package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
- package/dist/chat-interceptors/quick-commands.js +14 -5
- package/dist/chat-interceptors/quick-commands.js.map +1 -1
- package/dist/views/Chat/ButtonExecutionDetail.d.ts +5 -0
- package/dist/views/Chat/ButtonExecutionDetail.d.ts.map +1 -0
- package/dist/views/Chat/ButtonExecutionDetail.js +34 -0
- package/dist/views/Chat/ButtonExecutionDetail.js.map +1 -0
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +9 -7
- package/dist/views/Chat/ChatMessage.js.map +1 -1
- package/dist/views/Steps/FlowChart/HandleGroup.d.ts +4 -1
- package/dist/views/Steps/FlowChart/HandleGroup.d.ts.map +1 -1
- package/dist/views/Steps/FlowChart/HandleGroup.js +1 -1
- package/dist/views/Steps/FlowChart/HandleGroup.js.map +1 -1
- package/dist/views/Steps/FlowChart/NodeDynamic.d.ts +15 -0
- package/dist/views/Steps/FlowChart/NodeDynamic.d.ts.map +1 -0
- package/dist/views/Steps/FlowChart/NodeDynamic.js +41 -0
- package/dist/views/Steps/FlowChart/NodeDynamic.js.map +1 -0
- package/dist/views/Steps/FlowChart/NodeStep.d.ts +4 -1
- package/dist/views/Steps/FlowChart/NodeStep.d.ts.map +1 -1
- package/dist/views/Steps/FlowChart/NodeStep.js +2 -2
- package/dist/views/Steps/FlowChart/NodeStep.js.map +1 -1
- package/dist/views/Steps/FlowChart/hooks.d.ts +7 -0
- package/dist/views/Steps/FlowChart/hooks.d.ts.map +1 -0
- package/dist/views/Steps/FlowChart/hooks.js +31 -0
- package/dist/views/Steps/FlowChart/hooks.js.map +1 -0
- package/dist/views/Steps/FlowChart/index.d.ts +4 -2
- package/dist/views/Steps/FlowChart/index.d.ts.map +1 -1
- package/dist/views/Steps/FlowChart/index.js +53 -23
- package/dist/views/Steps/FlowChart/index.js.map +1 -1
- package/dist/views/Steps/FlowChart/layout.d.ts +4 -13
- package/dist/views/Steps/FlowChart/layout.d.ts.map +1 -1
- package/dist/views/Steps/FlowChart/layout.js +25 -7
- package/dist/views/Steps/FlowChart/layout.js.map +1 -1
- package/dist/views/Steps/FlowChart/styled.d.ts +0 -1
- package/dist/views/Steps/FlowChart/styled.d.ts.map +1 -1
- package/dist/views/Steps/FlowChart/styled.js +39 -15
- package/dist/views/Steps/FlowChart/styled.js.map +1 -1
- package/dist/views/Steps/FlowChart/types.d.ts +14 -2
- package/dist/views/Steps/FlowChart/types.d.ts.map +1 -1
- package/dist/views/Steps/StepModal.d.ts +2 -1
- package/dist/views/Steps/StepModal.d.ts.map +1 -1
- package/dist/views/Steps/StepModal.js +24 -7
- package/dist/views/Steps/StepModal.js.map +1 -1
- package/dist/views/Steps/StepsPanel.d.ts.map +1 -1
- package/dist/views/Steps/StepsPanel.js +6 -2
- package/dist/views/Steps/StepsPanel.js.map +1 -1
- package/dist/views/Steps/dictionary.d.ts +5 -1
- package/dist/views/Steps/dictionary.d.ts.map +1 -1
- package/dist/views/Steps/dictionary.js +4 -0
- package/dist/views/Steps/dictionary.js.map +1 -1
- package/dist/views/Steps/utils.js +2 -2
- package/dist/views/Steps/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/app-metadata.json +3 -3
- package/src/chat-interceptors/quick-commands.ts +14 -5
- package/src/views/Chat/ButtonExecutionDetail.tsx +46 -0
- package/src/views/Chat/ChatMessage.tsx +10 -6
- package/src/views/Steps/FlowChart/HandleGroup.tsx +5 -3
- package/src/views/Steps/FlowChart/NodeDynamic.tsx +97 -0
- package/src/views/Steps/FlowChart/NodeStep.tsx +6 -4
- package/src/views/Steps/FlowChart/hooks.ts +41 -0
- package/src/views/Steps/FlowChart/index.tsx +67 -23
- package/src/views/Steps/FlowChart/layout.ts +39 -16
- package/src/views/Steps/FlowChart/styled.ts +39 -15
- package/src/views/Steps/FlowChart/types.ts +16 -2
- package/src/views/Steps/StepModal.tsx +36 -13
- package/src/views/Steps/StepsPanel.tsx +9 -2
- package/src/views/Steps/dictionary.ts +4 -0
- package/src/views/Steps/utils.tsx +2 -2
|
@@ -12,8 +12,8 @@ export function getStatusIcon(status) {
|
|
|
12
12
|
}
|
|
13
13
|
export function getTypeIcon(type) {
|
|
14
14
|
switch (type) {
|
|
15
|
-
case 'planning': return { group: '
|
|
16
|
-
default: return { group: '
|
|
15
|
+
case 'planning': return { group: 'fill', icon: 'ChevronRight' };
|
|
16
|
+
default: return { group: 'outline', icon: 'StackSpot' };
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
export function getTitle(translation, step, index) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/views/Steps/utils.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAY,MAAM,0BAA0B,CAAA;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAEhD,MAAM,UAAU,aAAa,CAAC,MAA0B;IACtD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,SAAS,CAAC,CAAC,OAAO,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,EAAC,IAAI,EAAC,aAAa,EAAC,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,GAAI,CAAA;QAC3G,KAAK,OAAO,CAAC,CAAC,OAAO,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,EAAC,IAAI,EAAC,aAAa,EAAC,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAI,CAAA;QACxG,KAAK,SAAS,CAAC,CAAC,OAAO,KAAC,gBAAgB,IAAC,WAAW,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,GAAG,CAAA;QAC3E,OAAO,CAAC,CAAC,OAAO,IAAI,CAAA;IACtB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAsB;IAChD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../../src/views/Steps/utils.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAY,MAAM,0BAA0B,CAAA;AACzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAA;AAE3D,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAEhD,MAAM,UAAU,aAAa,CAAC,MAA0B;IACtD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,SAAS,CAAC,CAAC,OAAO,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,EAAC,IAAI,EAAC,aAAa,EAAC,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,GAAI,CAAA;QAC3G,KAAK,OAAO,CAAC,CAAC,OAAO,KAAC,IAAI,IAAC,KAAK,EAAC,MAAM,EAAC,IAAI,EAAC,aAAa,EAAC,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,GAAI,CAAA;QACxG,KAAK,SAAS,CAAC,CAAC,OAAO,KAAC,gBAAgB,IAAC,WAAW,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,GAAG,CAAA;QAC3E,OAAO,CAAC,CAAC,OAAO,IAAI,CAAA;IACtB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,IAAsB;IAChD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,UAAU,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;QAC/D,OAAO,CAAC,CAAC,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,CAAA;IACzD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,WAA2D,EAAE,IAA0B,EAAE,KAAa;IAC7H,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAA;IACpB,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,UAAU,CAAC,CAAC,OAAO,WAAW,CAAC,QAAQ,CAAA;QAC5C,KAAK,MAAM,CAAC,CAAC,OAAO,GAAG,WAAW,CAAC,IAAI,IAAI,KAAK,EAAE,CAAA;QAClD,KAAK,QAAQ,CAAC,CAAC,OAAO,WAAW,CAAC,MAAM,CAAA;IAC1C,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAS,EAAE,eAAe,GAAG,CAAC;IACxD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,eAAe,CAAC,CAAA;IAC5C,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,GAAG,MAAM,CAAA;AACxC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stack-spot/ai-chat-widget",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"@citric/core": "^6.4.0",
|
|
17
17
|
"@stack-spot/portal-components": "^2.27.3",
|
|
18
18
|
"@citric/icons": "^5.13.0",
|
|
19
|
-
"@stack-spot/portal-network": "0.
|
|
19
|
+
"@stack-spot/portal-network": "0.212.0",
|
|
20
20
|
"@citric/ui": "^6.10.2",
|
|
21
21
|
"@stack-spot/portal-translate": "^2.1.0",
|
|
22
22
|
"lodash": "^4.17.0",
|
package/src/app-metadata.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stack-spot/ai-chat-widget",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"date": "
|
|
3
|
+
"version": "2.10.0",
|
|
4
|
+
"date": "Tue Jan 06 2026 12:17:53 GMT+0000 (Coordinated Universal Time)",
|
|
5
5
|
"dependencies": [
|
|
6
6
|
{
|
|
7
7
|
"name": "@stack-spot/app-metadata",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
"name": "@stack-spot/portal-network",
|
|
124
|
-
"version": "0.
|
|
124
|
+
"version": "0.212.0(@stack-spot/auth@6.1.0)(@stack-spot/opa@2.5.0(@stack-spot/auth@6.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@stack-spot/portal-translate@2.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)"
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
"name": "@stack-spot/portal-theme",
|
|
@@ -370,7 +370,14 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
|
|
|
370
370
|
}
|
|
371
371
|
|
|
372
372
|
if (ctx.qc.use_selected_code && (!code && ctx.context.upload_ids?.length === 0)) {
|
|
373
|
-
|
|
373
|
+
widget.set('panel', 'editor')
|
|
374
|
+
ctx.chat.pushMessage(new ChatEntry({
|
|
375
|
+
agentType: 'bot',
|
|
376
|
+
type: 'text',
|
|
377
|
+
agent: ctx.chat.get('agent'),
|
|
378
|
+
content: t.requiresSelection,
|
|
379
|
+
}))
|
|
380
|
+
return
|
|
374
381
|
}
|
|
375
382
|
manageConversationContext(ctx)
|
|
376
383
|
await computeCustomInputs(ctx)
|
|
@@ -458,8 +465,10 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
|
|
|
458
465
|
const start = new Date().getTime()
|
|
459
466
|
try {
|
|
460
467
|
const result = await runQuickCommand(ctx)
|
|
461
|
-
|
|
462
|
-
|
|
468
|
+
if (result) {
|
|
469
|
+
outputResult(ctx, result)
|
|
470
|
+
registerAnalyticsEvent(ctx, '200', start)
|
|
471
|
+
}
|
|
463
472
|
} catch (error: any) {
|
|
464
473
|
let message = error.message || `${error}`
|
|
465
474
|
if (error instanceof AbortedError || error instanceof CancelledError) message = t.aborted
|
|
@@ -483,14 +492,14 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
|
|
|
483
492
|
|
|
484
493
|
const dictionary = {
|
|
485
494
|
en: {
|
|
486
|
-
requiresSelection: '
|
|
495
|
+
requiresSelection: 'Select the code in the editor next to it for this quick command to interact with it.',
|
|
487
496
|
startQuestioning: 'To execute the Quick Command "$0", I\'ll need you to provide some information. Some may be mandatory, and others optional. Let\'s get started.',
|
|
488
497
|
progress: 'Running step "$0" from Quick Command "$1".',
|
|
489
498
|
aborted: 'The quick command execution aborted by the user.',
|
|
490
499
|
notFound: 'There\'s no quick command with the provided name. If you don\'t wish to run a command, prefix the first "/" with a "\\".',
|
|
491
500
|
},
|
|
492
501
|
pt: {
|
|
493
|
-
requiresSelection: '
|
|
502
|
+
requiresSelection: 'Selecione o código no editor ao lado para que este quick command possa interagir com ele.',
|
|
494
503
|
startQuestioning: 'Para executar o Quick Command "$0", vou precisar que você providencie algumas explicações. Algumas são obrigatórias e outras opcionais. Vamos começar.',
|
|
495
504
|
progress: 'Executando step "$0" do Quick Command "$1".',
|
|
496
505
|
aborted: 'A execução do quick command foi abortada pelo usuário.',
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { LoadingCircular } from '@citric/ui'
|
|
2
|
+
import { Button, Icon, Row } from '@stack-spot/citric-react'
|
|
3
|
+
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
4
|
+
import { useEffect, useMemo } from 'react'
|
|
5
|
+
import { useChatMessages, useWidget } from '../../context/hooks'
|
|
6
|
+
|
|
7
|
+
export const ButtonExecutionDetail = ({ chatId, messageId }: { chatId: string, messageId: number }) => {
|
|
8
|
+
const t = useTranslate(dictionary)
|
|
9
|
+
const messages = useChatMessages(chatId)
|
|
10
|
+
const widget = useWidget()
|
|
11
|
+
|
|
12
|
+
const isRunning = useMemo(() => {
|
|
13
|
+
const messageEntry = messages?.find((message) => message.id === messageId)
|
|
14
|
+
return !messageEntry?.getValue().steps?.find((step) => step.type === 'answer' && step.status !== 'running')
|
|
15
|
+
}, [messageId, messages])
|
|
16
|
+
|
|
17
|
+
function openToolsPanel() {
|
|
18
|
+
if (messageId) {
|
|
19
|
+
widget.set('currentMessageInPanel', { chatId, messageId })
|
|
20
|
+
widget.set('panel', 'steps')
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
isRunning && openToolsPanel()
|
|
26
|
+
}, [isRunning])
|
|
27
|
+
|
|
28
|
+
return <>
|
|
29
|
+
<Row className="step-actions">
|
|
30
|
+
{isRunning && <LoadingCircular colorScheme="inverse" size="xs" />}
|
|
31
|
+
<Button colorScheme="light" size="sm" appearance="none" className="icon-button details" onClick={openToolsPanel}>
|
|
32
|
+
<Icon group="outline" icon="Expand" size="xs" />
|
|
33
|
+
{t.detailed}
|
|
34
|
+
</Button>
|
|
35
|
+
</Row>
|
|
36
|
+
</>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const dictionary = {
|
|
40
|
+
en: {
|
|
41
|
+
detailed: 'View execution details',
|
|
42
|
+
},
|
|
43
|
+
pt: {
|
|
44
|
+
detailed: 'Ver detalhes da execução',
|
|
45
|
+
},
|
|
46
|
+
} satisfies Dictionary
|
|
@@ -15,6 +15,7 @@ import { ChatEntry, SerializableAction, TextChatEntry } from '../../state/ChatEn
|
|
|
15
15
|
import { useDateFormatter } from '../../utils/date'
|
|
16
16
|
import { toolById } from '../../utils/tools'
|
|
17
17
|
import { AgentInfo } from './AgentInfo'
|
|
18
|
+
import { ButtonExecutionDetail } from './ButtonExecutionDetail'
|
|
18
19
|
import { useChatScrollToBottomEffect } from './chat-scroll'
|
|
19
20
|
import { onCopyAll, onCopyCode, onLikeOrDislike } from './events'
|
|
20
21
|
import { StepsList, StepsPlaceholder, ViewToolsDetails } from './StepsList'
|
|
@@ -226,10 +227,10 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
|
|
|
226
227
|
const [showUserButtonCopy, setShowUserButtonCopy] = useState(false)
|
|
227
228
|
const isPlanning = useCurrentChatState('isPlaning') ?? false
|
|
228
229
|
|
|
229
|
-
// Dynamic
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
const showToolBox =
|
|
230
|
+
// Dynamic steps are identified by the "dynamic" id.
|
|
231
|
+
const isDynamicSteps = !!entry?.steps?.some((s) => s.id?.toLowerCase() === 'dynamic')
|
|
232
|
+
const hasTools = ((agentsTools?.length ?? 0) > 0) || ((entry.tools?.length ?? 0) > 0)
|
|
233
|
+
const showToolBox = hasTools && !isDynamicSteps
|
|
233
234
|
|
|
234
235
|
useChatScrollToBottomEffect(ref, [entry])
|
|
235
236
|
useMidnightUpdateView()
|
|
@@ -355,7 +356,8 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
|
|
|
355
356
|
widget.set('panel', 'resources')
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
const
|
|
359
|
+
const buttonExecutionDetail = useMemo(() => <ButtonExecutionDetail chatId={chat.id} messageId={message.id} />, [chat.id, message.id])
|
|
360
|
+
const shouldShowToolsOnlyMessage = (entry.done !== false || entry.hasPlanning || isDynamicSteps) && !!entry.steps?.length
|
|
359
361
|
const shouldRender = entry.content || entry.error || shouldShowToolsOnlyMessage || !!entry.upload?.length
|
|
360
362
|
|
|
361
363
|
return shouldRender && (
|
|
@@ -372,7 +374,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
|
|
|
372
374
|
{entry.badges.map((b, index) => <Badge key={index} colorPalette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
|
|
373
375
|
</div>}
|
|
374
376
|
|
|
375
|
-
{!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
|
|
377
|
+
{!!entry.steps?.length && !isDynamicSteps && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
|
|
376
378
|
|
|
377
379
|
{renderContent()}
|
|
378
380
|
|
|
@@ -428,6 +430,8 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
|
|
|
428
430
|
</div>
|
|
429
431
|
}
|
|
430
432
|
|
|
433
|
+
{!!entry.steps?.length && isDynamicSteps && buttonExecutionDetail}
|
|
434
|
+
|
|
431
435
|
{shouldShowFooter && <div className="message-footer">
|
|
432
436
|
{entry.agentType === 'bot' && !entry.error && <div className="message-actions">
|
|
433
437
|
{entry.type === 'md' && (
|
|
@@ -3,10 +3,12 @@ import { Handle, Position } from '@xyflow/react'
|
|
|
3
3
|
interface Props {
|
|
4
4
|
renderSource?: boolean,
|
|
5
5
|
renderTarget?: boolean,
|
|
6
|
+
targetPosition?: Position,
|
|
7
|
+
sourcePosition?: Position,
|
|
6
8
|
}
|
|
7
|
-
export const HandleGroup = ({ renderSource = true, renderTarget = true }: Props) => (
|
|
9
|
+
export const HandleGroup = ({ renderSource = true, renderTarget = true, targetPosition, sourcePosition }: Props) => (
|
|
8
10
|
<>
|
|
9
|
-
{renderTarget && <Handle type="target" position={Position.Left} isConnectable className="target-handle" />}
|
|
10
|
-
{renderSource && <Handle type="source" position={Position.Right} isConnectable className="source-handle" />}
|
|
11
|
+
{renderTarget && <Handle type="target" position={targetPosition || Position.Left} isConnectable className="target-handle" />}
|
|
12
|
+
{renderSource && <Handle type="source" position={sourcePosition || Position.Right} isConnectable className="source-handle" />}
|
|
11
13
|
</>
|
|
12
14
|
)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Icon, WithIcon } from '@stack-spot/citric-icons'
|
|
2
|
+
import { Avatar, Column, IconBox, Row, Skeleton, Text } from '@stack-spot/citric-react'
|
|
3
|
+
import { AnimatedHeight } from '@stack-spot/portal-components/AnimatedHeight'
|
|
4
|
+
import { ChatAgentTool } from '@stack-spot/portal-network'
|
|
5
|
+
import { listToClass } from '@stack-spot/portal-theme'
|
|
6
|
+
import { Position } from '@xyflow/react'
|
|
7
|
+
import { useChatEntry } from '../../../context/hooks'
|
|
8
|
+
import { useStepsDictionary } from '../dictionary'
|
|
9
|
+
import { getTitle, getTypeIcon } from '../utils'
|
|
10
|
+
import { HandleGroup } from './HandleGroup'
|
|
11
|
+
import { useResizeObserver } from './hooks'
|
|
12
|
+
import { NodeData } from './types'
|
|
13
|
+
|
|
14
|
+
const ToolItem = ({ tool, index, onClick }: { tool: ChatAgentTool, index: number, onClick?: (index: number) => void }) => {
|
|
15
|
+
const { name, image, input, status } = tool
|
|
16
|
+
|
|
17
|
+
if (status === 'running') {
|
|
18
|
+
return <Skeleton height="48px" bgLevel={600} />
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="tool" onClick={() => onClick?.(index)}>
|
|
23
|
+
<Row gap="4px" alignItems="center" mb={2}>
|
|
24
|
+
{image
|
|
25
|
+
? <Avatar size="xxs" image={image} name={name} />
|
|
26
|
+
: <Icon icon="Cog" group="outline" />
|
|
27
|
+
}
|
|
28
|
+
<Text weight="500" className="title">{name}</Text>
|
|
29
|
+
</Row>
|
|
30
|
+
<Text nowrapEllipsis color="light.700">{input}</Text>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ListTools = ({ tools, onClick }: { tools: ChatAgentTool[], onClick?: (toolIndex?: number) => void }) => (
|
|
36
|
+
<AnimatedHeight>
|
|
37
|
+
<Column gap="8px">
|
|
38
|
+
{tools.map((tool, index) => (
|
|
39
|
+
<Column key={`${tool.id}-${index}`} className="wrapper-tool">
|
|
40
|
+
<ToolItem tool={tool} index={index} onClick={onClick} />
|
|
41
|
+
</Column>
|
|
42
|
+
))}
|
|
43
|
+
</Column>
|
|
44
|
+
</AnimatedHeight>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const NodeBox = ({ title, icon, content, onClick }: { title: string, icon: WithIcon, content?: string, onClick?: () => void }) => (
|
|
48
|
+
<Column onClick={onClick} style={{ cursor: onClick ? 'pointer' : 'default' }}>
|
|
49
|
+
<Row gap="4px" alignItems="center">
|
|
50
|
+
<IconBox {...icon} appearance="none" colorScheme="light" size="sm" />
|
|
51
|
+
<Text className="step-index" weight="500">{title}</Text>
|
|
52
|
+
</Row>
|
|
53
|
+
<Text nowrapEllipsis color="light.700">
|
|
54
|
+
{content}
|
|
55
|
+
</Text>
|
|
56
|
+
</Column>
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
interface Props {
|
|
60
|
+
data: NodeData,
|
|
61
|
+
targetPosition?: Position,
|
|
62
|
+
sourcePosition?: Position,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const NodeDynamic = ({ data, ...props }: Props) => {
|
|
66
|
+
const { step, index, onClick, onResize, message } = data
|
|
67
|
+
const content = useChatEntry(message)?.content
|
|
68
|
+
const t = useStepsDictionary()
|
|
69
|
+
const ref = useResizeObserver(onResize)
|
|
70
|
+
|
|
71
|
+
const tools = step.type === 'step' ? step?.attempts?.[0]?.tools : undefined
|
|
72
|
+
|
|
73
|
+
const renderContent = () => {
|
|
74
|
+
if (step.type === 'planning') {
|
|
75
|
+
return <NodeBox title={t.userPrompt} content={step.user_question} icon={{ ...getTypeIcon(step.type) }} />
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (step.type === 'step') {
|
|
79
|
+
return tools?.length ? <ListTools onClick={onClick} tools={tools} /> : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (step.type === 'answer') {
|
|
83
|
+
return <Column p={3} bg="light.400" onClick={() => onClick?.(index)}>
|
|
84
|
+
<NodeBox onClick={onClick} title={t.finalAnswer} content={content} icon={{ ...getTypeIcon(step.type) }} />
|
|
85
|
+
</Column>
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<div ref={ref} aria-label={getTitle(t, step, index)}>
|
|
91
|
+
<div className={listToClass(['chart-node', null, step.status])} >
|
|
92
|
+
{renderContent()}
|
|
93
|
+
<HandleGroup {...props} renderSource={step.type !== 'answer'} renderTarget={step.type !== 'planning'} />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Icon } from '@stack-spot/citric-icons'
|
|
2
2
|
import { Text } from '@stack-spot/citric-react'
|
|
3
3
|
import { listToClass } from '@stack-spot/portal-theme'
|
|
4
|
+
import { Position } from '@xyflow/react'
|
|
4
5
|
import { StackedBadge } from '../../../components/StackedBadge'
|
|
5
6
|
import { useStepsDictionary } from '../dictionary'
|
|
6
7
|
import { getStatusIcon, getTitle, getTypeIcon } from '../utils'
|
|
@@ -9,15 +10,17 @@ import { NodeData } from './types'
|
|
|
9
10
|
|
|
10
11
|
interface Props {
|
|
11
12
|
data: NodeData,
|
|
13
|
+
targetPosition?: Position,
|
|
14
|
+
sourcePosition?: Position,
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
export const NodeStep = ({ data: { step, index, nextStatus, onClick } }: Props) => {
|
|
17
|
+
export const NodeStep = ({ data: { step, index, nextStatus, onClick }, ...props }: Props) => {
|
|
15
18
|
const t = useStepsDictionary()
|
|
16
19
|
|
|
17
20
|
return (
|
|
18
21
|
<div
|
|
19
22
|
className={listToClass(['chart-node', step.type, nextStatus])}
|
|
20
|
-
onClick={onClick}
|
|
23
|
+
onClick={() => onClick?.()}
|
|
21
24
|
onKeyDown={e => e.key === 'Enter' && onClick?.()}
|
|
22
25
|
tabIndex={0}
|
|
23
26
|
role="button"
|
|
@@ -41,7 +44,6 @@ export const NodeStep = ({ data: { step, index, nextStatus, onClick } }: Props)
|
|
|
41
44
|
)}
|
|
42
45
|
/>}
|
|
43
46
|
</div>}
|
|
44
|
-
<HandleGroup renderSource={step.type !== 'answer'} renderTarget={step.type !== 'planning'} />
|
|
45
|
-
</div>
|
|
47
|
+
<HandleGroup {...props} renderSource={step.type !== 'answer'} renderTarget={step.type !== 'planning'} /> </div>
|
|
46
48
|
)
|
|
47
49
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { debounce } from 'lodash'
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
3
|
+
|
|
4
|
+
interface Size {
|
|
5
|
+
width: number,
|
|
6
|
+
height: number,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const useResizeObserver = (callback?: (size: Size) => void, delay = 50) => {
|
|
10
|
+
const ref = useRef<HTMLDivElement | null>(null)
|
|
11
|
+
const previousSizeRef = useRef<Size | null>(null)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!ref.current || !callback) return
|
|
15
|
+
|
|
16
|
+
const debouncedCallback = debounce((newSize: Size) => {
|
|
17
|
+
const prevSize = previousSizeRef.current
|
|
18
|
+
const hasChanged = !prevSize || prevSize.width !== newSize.width || prevSize.height !== newSize.height
|
|
19
|
+
|
|
20
|
+
if (hasChanged) {
|
|
21
|
+
previousSizeRef.current = newSize
|
|
22
|
+
callback(newSize)
|
|
23
|
+
}
|
|
24
|
+
}, delay)
|
|
25
|
+
|
|
26
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
27
|
+
if (!entries.length) return
|
|
28
|
+
const { width, height } = entries[0].contentRect
|
|
29
|
+
debouncedCallback({ width, height })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
resizeObserver.observe(ref.current)
|
|
33
|
+
|
|
34
|
+
return () => {
|
|
35
|
+
resizeObserver.disconnect()
|
|
36
|
+
debouncedCallback.cancel()
|
|
37
|
+
}
|
|
38
|
+
}, [callback])
|
|
39
|
+
|
|
40
|
+
return ref
|
|
41
|
+
}
|
|
@@ -1,34 +1,63 @@
|
|
|
1
1
|
import { ChatStep } from '@stack-spot/portal-network'
|
|
2
2
|
import { listToClass, theme } from '@stack-spot/portal-theme'
|
|
3
|
-
import { Background, Controls, Edge, MarkerType, ReactFlow } from '@xyflow/react'
|
|
3
|
+
import { Background, Controls, Edge, MarkerType, ReactFlow, ReactFlowProvider, useReactFlow } from '@xyflow/react'
|
|
4
4
|
import '@xyflow/react/dist/style.css'
|
|
5
|
-
import {
|
|
5
|
+
import { last } from 'lodash'
|
|
6
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
6
7
|
import { useChatEntry } from '../../../context/hooks'
|
|
7
8
|
import { ChatEntry } from '../../../state/ChatEntry'
|
|
8
|
-
import { useLayoutedElements } from './layout'
|
|
9
|
+
import { LayoutDirection, useLayoutedElements } from './layout'
|
|
10
|
+
import { NodeDynamic } from './NodeDynamic'
|
|
9
11
|
import { NodeStep } from './NodeStep'
|
|
10
|
-
import { FlowChartBox
|
|
12
|
+
import { FlowChartBox } from './styled'
|
|
13
|
+
import { NodeFullProps } from './types'
|
|
11
14
|
|
|
12
15
|
interface Props {
|
|
13
16
|
message: ChatEntry,
|
|
14
|
-
onClick: (step: ChatStep,
|
|
17
|
+
onClick: (step: ChatStep, toolIndex?: number) => void,
|
|
18
|
+
direction?: LayoutDirection,
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
const
|
|
18
|
-
planning: NodeStep,
|
|
19
|
-
step:
|
|
20
|
-
answer: NodeStep,
|
|
21
|
+
const NODE_TYPES = {
|
|
22
|
+
step: { planning: NodeStep, step: NodeStep, answer: NodeStep },
|
|
23
|
+
stepDynamic: { planning: NodeDynamic, step: NodeDynamic, answer: NodeDynamic },
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
const Flow = ({ message, onClick, direction = 'LR' }: Props) => {
|
|
24
27
|
const steps = useChatEntry(message).steps
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const [nodeSizes, setNodeSizes] = useState<Record<string, { width: number, height: number }>>({})
|
|
29
|
+
const reactFlowInstance = useReactFlow()
|
|
30
|
+
|
|
31
|
+
const isDynamic = useMemo(() => steps?.some((step) => step.id === 'dynamic'), [steps])
|
|
32
|
+
const nodeTypes = useMemo(() => NODE_TYPES[isDynamic ? 'stepDynamic' : 'step'], [isDynamic])
|
|
33
|
+
|
|
34
|
+
const handleNodeSizeChange = useCallback((id: string, size: { width: number, height: number }) => {
|
|
35
|
+
setNodeSizes((prev) => {
|
|
36
|
+
const current = prev[id]
|
|
37
|
+
if (current?.width === size.width && current?.height === size.height) return prev
|
|
38
|
+
return { ...prev, [id]: size }
|
|
39
|
+
})
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
const baseElements = useMemo(() => {
|
|
43
|
+
const nodes = steps?.map((s, index) => {
|
|
44
|
+
const nodeSize = nodeSizes[s.id]
|
|
45
|
+
return {
|
|
46
|
+
id: s.id,
|
|
47
|
+
type: s.type,
|
|
48
|
+
focusable: false,
|
|
49
|
+
...(nodeSize && { width: nodeSize.width, height: nodeSize.height }),
|
|
50
|
+
origin: [1, 0],
|
|
51
|
+
data: {
|
|
52
|
+
message,
|
|
53
|
+
step: s,
|
|
54
|
+
index,
|
|
55
|
+
nextStatus: steps[index + 1]?.status,
|
|
56
|
+
onClick: (toolIndex) => onClick(s, toolIndex),
|
|
57
|
+
onResize: (size) => handleNodeSizeChange(s.id, size),
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
}) as NodeFullProps[] ?? []
|
|
32
61
|
const edges: Edge[] = []
|
|
33
62
|
for (let i = 0; i < nodes.length - 1; i++) {
|
|
34
63
|
edges.push({
|
|
@@ -40,24 +69,33 @@ export const FlowChart = ({ message, onClick }: Props) => {
|
|
|
40
69
|
markerEnd: {
|
|
41
70
|
type: MarkerType.Arrow,
|
|
42
71
|
strokeWidth: 2,
|
|
43
|
-
color:
|
|
72
|
+
color: theme.color.light[700],
|
|
44
73
|
},
|
|
45
74
|
})
|
|
46
75
|
}
|
|
47
76
|
return { nodes, edges }
|
|
48
|
-
}, [steps])
|
|
77
|
+
}, [steps, nodeSizes, message, onClick, handleNodeSizeChange])
|
|
78
|
+
|
|
79
|
+
const { nodes, edges } = useLayoutedElements(baseElements.nodes, baseElements.edges, direction)
|
|
49
80
|
|
|
50
|
-
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const lastNode = last(nodes)
|
|
83
|
+
const isRunning = lastNode?.data?.step?.status === 'running'
|
|
84
|
+
const shouldFitView = nodes.length > 2 && isRunning || (lastNode?.height ?? 0 > 180) && isRunning
|
|
85
|
+
if (shouldFitView) {
|
|
86
|
+
reactFlowInstance.fitView({ duration: 1000, padding: { bottom: '120px', top: '20px' } })
|
|
87
|
+
}
|
|
88
|
+
}, [nodes, edges, reactFlowInstance])
|
|
51
89
|
|
|
52
90
|
return (
|
|
53
91
|
<FlowChartBox>
|
|
54
92
|
<ReactFlow
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
edges={layouted.edges}
|
|
93
|
+
nodes={nodes}
|
|
94
|
+
edges={edges}
|
|
58
95
|
nodeTypes={nodeTypes}
|
|
59
96
|
snapToGrid={true}
|
|
60
97
|
fitView
|
|
98
|
+
maxZoom={1}
|
|
61
99
|
>
|
|
62
100
|
<Controls orientation="horizontal" className="controls" showInteractive={false} />
|
|
63
101
|
<Background />
|
|
@@ -65,3 +103,9 @@ export const FlowChart = ({ message, onClick }: Props) => {
|
|
|
65
103
|
</FlowChartBox>
|
|
66
104
|
)
|
|
67
105
|
}
|
|
106
|
+
|
|
107
|
+
export const FlowChart = (props: Props) => (
|
|
108
|
+
<ReactFlowProvider>
|
|
109
|
+
<Flow {...props} />
|
|
110
|
+
</ReactFlowProvider>
|
|
111
|
+
)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import dagre from '@dagrejs/dagre'
|
|
2
|
-
import { Edge } from '@xyflow/react'
|
|
2
|
+
import { Edge, Position } from '@xyflow/react'
|
|
3
3
|
import { useMemo } from 'react'
|
|
4
4
|
import { answerNodeSize, planningNodeSize, stepNodeSize } from './styled'
|
|
5
|
-
import {
|
|
5
|
+
import { NodeFullProps } from './types'
|
|
6
6
|
|
|
7
7
|
const nodesSizes = {
|
|
8
8
|
step: stepNodeSize,
|
|
@@ -11,40 +11,63 @@ const nodesSizes = {
|
|
|
11
11
|
tool: stepNodeSize,
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export
|
|
14
|
+
export type LayoutDirection = 'TB' | 'BT' | 'LR' | 'RL'
|
|
15
|
+
|
|
16
|
+
const getHandlePositions = (direction: LayoutDirection) => {
|
|
17
|
+
switch (direction) {
|
|
18
|
+
case 'TB':
|
|
19
|
+
return { target: Position.Top, source: Position.Bottom }
|
|
20
|
+
case 'BT':
|
|
21
|
+
return { target: Position.Bottom, source: Position.Top }
|
|
22
|
+
case 'LR':
|
|
23
|
+
return { target: Position.Left, source: Position.Right }
|
|
24
|
+
case 'RL':
|
|
25
|
+
return { target: Position.Right, source: Position.Left }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useLayoutedElements(nodes: NodeFullProps[], edges: Edge[], direction: LayoutDirection = 'LR') {
|
|
15
30
|
const dagreGraph = useMemo(() => new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})), [])
|
|
31
|
+
|
|
16
32
|
return useMemo(() => {
|
|
17
|
-
dagreGraph.setGraph({ rankdir:
|
|
18
|
-
|
|
33
|
+
dagreGraph.setGraph({ rankdir: direction })
|
|
34
|
+
|
|
19
35
|
nodes.forEach((node) => {
|
|
20
36
|
const { width, height } = nodesSizes[node.type]
|
|
21
|
-
|
|
37
|
+
const nodeWidth = node.width ?? width
|
|
38
|
+
const nodeHeight = node.height ?? height
|
|
39
|
+
dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight })
|
|
22
40
|
})
|
|
23
|
-
|
|
41
|
+
|
|
24
42
|
edges.forEach((edge) => {
|
|
25
43
|
dagreGraph.setEdge(edge.source, edge.target)
|
|
26
44
|
})
|
|
27
|
-
|
|
45
|
+
|
|
28
46
|
dagre.layout(dagreGraph)
|
|
29
|
-
|
|
30
|
-
const
|
|
47
|
+
|
|
48
|
+
const { target, source } = getHandlePositions(direction)
|
|
49
|
+
|
|
50
|
+
const newNodes: NodeFullProps[] = nodes.map((node) => {
|
|
31
51
|
const { width, height } = nodesSizes[node.type]
|
|
52
|
+
const nodeWidth = node?.width ?? width
|
|
53
|
+
const nodeHeight = node.height ?? height
|
|
32
54
|
const nodeWithPosition = dagreGraph.node(node.id)
|
|
33
55
|
const newNode = {
|
|
34
56
|
...node,
|
|
35
|
-
targetPosition:
|
|
36
|
-
sourcePosition:
|
|
57
|
+
targetPosition: target,
|
|
58
|
+
sourcePosition: source,
|
|
37
59
|
// We are shifting the dagre node position (anchor=center center) to the top left
|
|
38
60
|
// so it matches the React Flow node anchor point (top left).
|
|
39
61
|
position: {
|
|
40
|
-
x: nodeWithPosition.x -
|
|
41
|
-
y: nodeWithPosition.y -
|
|
62
|
+
x: nodeWithPosition.x - nodeWidth / 2,
|
|
63
|
+
y: nodeWithPosition.y - nodeHeight / 2,
|
|
42
64
|
},
|
|
43
65
|
}
|
|
44
|
-
|
|
66
|
+
|
|
45
67
|
return newNode
|
|
46
68
|
})
|
|
47
|
-
|
|
69
|
+
|
|
48
70
|
return { nodes: newNodes, edges }
|
|
49
71
|
}, [nodes, edges])
|
|
50
72
|
}
|
|
73
|
+
|