@stack-spot/ai-chat-widget 3.6.0-beta.5 → 3.6.2-beta.5
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/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 +8 -5
- 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/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/NodeTool.d.ts +15 -0
- package/dist/views/Steps/FlowChart/NodeTool.d.ts.map +1 -0
- package/dist/views/Steps/FlowChart/NodeTool.js +41 -0
- package/dist/views/Steps/FlowChart/NodeTool.js.map +1 -0
- 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 +29 -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.d.ts.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/views/Chat/ButtonExecutionDetail.tsx +46 -0
- package/src/views/Chat/ChatMessage.tsx +9 -4
- package/src/views/Steps/FlowChart/HandleGroup.tsx +5 -3
- package/src/views/Steps/FlowChart/NodeStep.tsx +7 -5
- package/src/views/Steps/FlowChart/NodeTool.tsx +97 -0
- package/src/views/Steps/FlowChart/hooks.ts +40 -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 +3 -3
|
@@ -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'
|
|
@@ -228,8 +229,9 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
|
|
|
228
229
|
|
|
229
230
|
// Dynamic tool steps are identified by the "dynamic" id.
|
|
230
231
|
// We're temporarily hiding the toolbox for these dynamic tools while we finalize their UI.
|
|
231
|
-
const
|
|
232
|
-
const
|
|
232
|
+
const isDynamicSteps = !!entry?.steps?.some((s) => s.id?.toLowerCase() === 'dynamic')
|
|
233
|
+
const hasTools = ((agentsTools?.length ?? 0) > 0) || ((entry.tools?.length ?? 0) > 0)
|
|
234
|
+
const showToolBox = hasTools && !isDynamicSteps
|
|
233
235
|
|
|
234
236
|
useChatScrollToBottomEffect(ref, [entry])
|
|
235
237
|
useMidnightUpdateView()
|
|
@@ -355,7 +357,8 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
|
|
|
355
357
|
widget.set('panel', 'resources')
|
|
356
358
|
}
|
|
357
359
|
|
|
358
|
-
const
|
|
360
|
+
const buttonExecutionDetail = useMemo(() => <ButtonExecutionDetail chatId={chat.id} messageId={message.id} />, [chat.id, message.id])
|
|
361
|
+
const shouldShowToolsOnlyMessage = (entry.done !== false || entry.hasPlanning || isDynamicSteps) && !!entry.steps?.length
|
|
359
362
|
const shouldRender = entry.content || entry.error || shouldShowToolsOnlyMessage || !!entry.upload?.length
|
|
360
363
|
|
|
361
364
|
return shouldRender && (
|
|
@@ -372,7 +375,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
|
|
|
372
375
|
{entry.badges.map((b, index) => <Badge key={index} colorPalette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
|
|
373
376
|
</div>}
|
|
374
377
|
|
|
375
|
-
{!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
|
|
378
|
+
{!!entry.steps?.length && !isDynamicSteps && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
|
|
376
379
|
|
|
377
380
|
{renderContent()}
|
|
378
381
|
|
|
@@ -428,6 +431,8 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
|
|
|
428
431
|
</div>
|
|
429
432
|
}
|
|
430
433
|
|
|
434
|
+
{!!entry.steps?.length && isDynamicSteps && buttonExecutionDetail}
|
|
435
|
+
|
|
431
436
|
{shouldShowFooter && <div className="message-footer">
|
|
432
437
|
{entry.agentType === 'bot' && !entry.error && <div className="message-actions">
|
|
433
438
|
{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
|
)
|
|
@@ -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"
|
|
@@ -29,7 +32,7 @@ export const NodeStep = ({ data: { step, index, nextStatus, onClick } }: Props)
|
|
|
29
32
|
{getStatusIcon(step.status)}
|
|
30
33
|
</header>
|
|
31
34
|
{step.type === 'planning' && <Text className="step-title">{step.goal}</Text>}
|
|
32
|
-
{step.type === 'step' && step.input && <Text className="step-title">{step.input}</Text>}
|
|
35
|
+
{step.type === 'step' && step.input && <Text className="step-title">b {step.input}</Text>}
|
|
33
36
|
{step.type === 'step' && <div className="step-details">
|
|
34
37
|
<Text className={listToClass(['step-description', !!step.attempts[0].tools?.length && 'with-tools'])}>
|
|
35
38
|
{step.output}
|
|
@@ -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,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 StepCard = ({ title, icon, content, onClick }: { title: string, icon: WithIcon, content?: string, onClick?: () => void }) => (
|
|
48
|
+
<Column onClick={onClick} style={{ cursor: onClick ? 'pointer' : 'default' }}>
|
|
49
|
+
<Row>
|
|
50
|
+
<IconBox {...icon} appearance="square" 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 NodeTool = ({ 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 <StepCard 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
|
+
<StepCard 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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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) => {
|
|
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
|
+
}, 50)
|
|
25
|
+
|
|
26
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
27
|
+
const { width, height } = entries[0].contentRect
|
|
28
|
+
debouncedCallback({ width, height })
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
resizeObserver.observe(ref.current)
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
resizeObserver.disconnect()
|
|
35
|
+
debouncedCallback.cancel()
|
|
36
|
+
}
|
|
37
|
+
}, [callback])
|
|
38
|
+
|
|
39
|
+
return ref
|
|
40
|
+
}
|
|
@@ -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'
|
|
9
10
|
import { NodeStep } from './NodeStep'
|
|
10
|
-
import {
|
|
11
|
+
import { NodeTool } from './NodeTool'
|
|
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
|
+
tool: { planning: NodeTool, step: NodeTool, answer: NodeTool },
|
|
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 ? 'tool' : '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
|
}
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { ChatStep } from '@stack-spot/portal-network'
|
|
2
|
+
import { Node } from '@xyflow/react'
|
|
3
|
+
import { ChatEntry } from '../../../state/ChatEntry'
|
|
4
|
+
|
|
5
|
+
export type NodeType = 'step' | 'planning' | 'answer' | 'tool'
|
|
2
6
|
|
|
3
7
|
export interface NodeData {
|
|
4
8
|
nextStatus: ChatStep['status'] | undefined,
|
|
5
|
-
onClick?: () => void,
|
|
9
|
+
onClick?: (toolIndex?: number) => void,
|
|
6
10
|
step: ChatStep,
|
|
7
11
|
index: number,
|
|
12
|
+
message: ChatEntry,
|
|
13
|
+
onResize?: (size: { width: number, height: number }) => void,
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
export interface NodeWithoutLayout {
|
|
11
17
|
id: string,
|
|
12
|
-
type:
|
|
18
|
+
type: NodeType,
|
|
13
19
|
data?: NodeData,
|
|
14
20
|
}
|
|
21
|
+
|
|
22
|
+
export type NodeDataFullProps = NodeData & Record<string, unknown>
|
|
23
|
+
|
|
24
|
+
export interface NodeFullProps extends Node<NodeDataFullProps> {
|
|
25
|
+
type: NodeType,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|