@stack-spot/ai-chat-widget 2.8.4 → 2.9.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.js +2 -2
- 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 +2 -2
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stack-spot/ai-chat-widget",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.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": "Mon
|
|
3
|
+
"version": "2.9.0",
|
|
4
|
+
"date": "Mon Jan 05 2026 17:22:54 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",
|
|
@@ -271,8 +271,8 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
|
|
|
271
271
|
stepSlug: step.slug,
|
|
272
272
|
slug: slug,
|
|
273
273
|
quickCommandStartScriptRequest: {
|
|
274
|
-
input_data:
|
|
275
|
-
|
|
274
|
+
input_data: code,
|
|
275
|
+
custom_inputs: customInputs,
|
|
276
276
|
context: stepContext,
|
|
277
277
|
slugs_executions: resultMap,
|
|
278
278
|
},
|
|
@@ -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
|
+
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { theme } from '@stack-spot/portal-theme'
|
|
2
2
|
import { styled } from 'styled-components'
|
|
3
3
|
|
|
4
|
-
export const stepNodeSize = { width:
|
|
5
|
-
export const planningNodeSize = { width:
|
|
6
|
-
export const answerNodeSize = { width:
|
|
7
|
-
export const runningColor = '#0097FA'
|
|
4
|
+
export const stepNodeSize = { width: 400, height: 167 }
|
|
5
|
+
export const planningNodeSize = { width: 400, height: 61 }
|
|
6
|
+
export const answerNodeSize = { width: 400, height: 40 }
|
|
8
7
|
|
|
9
8
|
export const FlowChartBox = styled.div`
|
|
10
9
|
width: 100%;
|
|
@@ -19,31 +18,35 @@ export const FlowChartBox = styled.div`
|
|
|
19
18
|
border: 1px solid ${theme.color.light[600]};
|
|
20
19
|
background-color: ${theme.color.light[500]};
|
|
21
20
|
box-sizing: border-box;
|
|
22
|
-
justify-content: center;
|
|
23
|
-
|
|
24
|
-
&.running .source-handle {
|
|
25
|
-
background-color: ${runningColor};
|
|
26
|
-
}
|
|
21
|
+
justify-content: center;
|
|
22
|
+
width: 400px;
|
|
27
23
|
|
|
28
24
|
&.pending .source-handle {
|
|
29
25
|
opacity: 0.3;
|
|
30
26
|
}
|
|
31
27
|
|
|
28
|
+
.source-handle {
|
|
29
|
+
transition: opacity 1s ease; opacity: 1;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
&.running .source-handle {
|
|
33
|
+
opacity: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
&.step {
|
|
33
37
|
width: ${stepNodeSize.width}px;
|
|
34
|
-
height: ${stepNodeSize.height}px;
|
|
38
|
+
min-height: ${stepNodeSize.height}px;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
&.planning {
|
|
38
42
|
width: ${planningNodeSize.width}px;
|
|
39
|
-
height: ${planningNodeSize.height}px;
|
|
43
|
+
min-height: ${planningNodeSize.height}px;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
&.answer {
|
|
43
47
|
width: ${answerNodeSize.width}px;
|
|
44
|
-
height: ${answerNodeSize.height}px;
|
|
48
|
+
min-height: ${answerNodeSize.height}px;
|
|
45
49
|
}
|
|
46
|
-
|
|
47
50
|
header {
|
|
48
51
|
display: flex;
|
|
49
52
|
gap: 4px;
|
|
@@ -71,7 +74,7 @@ export const FlowChartBox = styled.div`
|
|
|
71
74
|
flex-direction: column;
|
|
72
75
|
align-items: start;
|
|
73
76
|
justify-content: space-between;
|
|
74
|
-
gap:
|
|
77
|
+
gap: 8px;
|
|
75
78
|
overflow: hidden;
|
|
76
79
|
|
|
77
80
|
.step-description {
|
|
@@ -89,6 +92,28 @@ export const FlowChartBox = styled.div`
|
|
|
89
92
|
}
|
|
90
93
|
}
|
|
91
94
|
}
|
|
95
|
+
|
|
96
|
+
.wrapper-tool {
|
|
97
|
+
&:not(:last-child) {
|
|
98
|
+
border-bottom: 1px solid ${theme.color.light[600]};
|
|
99
|
+
padding-bottom: 8px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.tool {
|
|
103
|
+
padding: 8px;
|
|
104
|
+
border-radius: 4px;
|
|
105
|
+
min-height: 48px;
|
|
106
|
+
border: 1px solid ${theme.color.light[500]};
|
|
107
|
+
background-color: ${theme.color.light[400]};
|
|
108
|
+
border-bottom: 1px solid ${theme.color.light[300]};
|
|
109
|
+
cursor: pointer;
|
|
110
|
+
|
|
111
|
+
&:hover .title {
|
|
112
|
+
text-decoration: underline;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
92
117
|
}
|
|
93
118
|
|
|
94
119
|
.source-handle {
|
|
@@ -121,7 +146,6 @@ export const FlowChartBox = styled.div`
|
|
|
121
146
|
opacity: 0.3;
|
|
122
147
|
}
|
|
123
148
|
&.running path {
|
|
124
|
-
stroke: ${runningColor};
|
|
125
149
|
stroke-dasharray: 5, 5;
|
|
126
150
|
}
|
|
127
151
|
}
|