@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/app-metadata.json +3 -3
  3. package/dist/views/Chat/ButtonExecutionDetail.d.ts +5 -0
  4. package/dist/views/Chat/ButtonExecutionDetail.d.ts.map +1 -0
  5. package/dist/views/Chat/ButtonExecutionDetail.js +34 -0
  6. package/dist/views/Chat/ButtonExecutionDetail.js.map +1 -0
  7. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  8. package/dist/views/Chat/ChatMessage.js +8 -5
  9. package/dist/views/Chat/ChatMessage.js.map +1 -1
  10. package/dist/views/Steps/FlowChart/HandleGroup.d.ts +4 -1
  11. package/dist/views/Steps/FlowChart/HandleGroup.d.ts.map +1 -1
  12. package/dist/views/Steps/FlowChart/HandleGroup.js +1 -1
  13. package/dist/views/Steps/FlowChart/HandleGroup.js.map +1 -1
  14. package/dist/views/Steps/FlowChart/NodeStep.d.ts +4 -1
  15. package/dist/views/Steps/FlowChart/NodeStep.d.ts.map +1 -1
  16. package/dist/views/Steps/FlowChart/NodeStep.js +2 -2
  17. package/dist/views/Steps/FlowChart/NodeStep.js.map +1 -1
  18. package/dist/views/Steps/FlowChart/NodeTool.d.ts +15 -0
  19. package/dist/views/Steps/FlowChart/NodeTool.d.ts.map +1 -0
  20. package/dist/views/Steps/FlowChart/NodeTool.js +41 -0
  21. package/dist/views/Steps/FlowChart/NodeTool.js.map +1 -0
  22. package/dist/views/Steps/FlowChart/hooks.d.ts +7 -0
  23. package/dist/views/Steps/FlowChart/hooks.d.ts.map +1 -0
  24. package/dist/views/Steps/FlowChart/hooks.js +29 -0
  25. package/dist/views/Steps/FlowChart/hooks.js.map +1 -0
  26. package/dist/views/Steps/FlowChart/index.d.ts +4 -2
  27. package/dist/views/Steps/FlowChart/index.d.ts.map +1 -1
  28. package/dist/views/Steps/FlowChart/index.js +53 -23
  29. package/dist/views/Steps/FlowChart/index.js.map +1 -1
  30. package/dist/views/Steps/FlowChart/layout.d.ts +4 -13
  31. package/dist/views/Steps/FlowChart/layout.d.ts.map +1 -1
  32. package/dist/views/Steps/FlowChart/layout.js +25 -7
  33. package/dist/views/Steps/FlowChart/layout.js.map +1 -1
  34. package/dist/views/Steps/FlowChart/styled.d.ts +0 -1
  35. package/dist/views/Steps/FlowChart/styled.d.ts.map +1 -1
  36. package/dist/views/Steps/FlowChart/styled.js +39 -15
  37. package/dist/views/Steps/FlowChart/styled.js.map +1 -1
  38. package/dist/views/Steps/FlowChart/types.d.ts +14 -2
  39. package/dist/views/Steps/FlowChart/types.d.ts.map +1 -1
  40. package/dist/views/Steps/StepModal.d.ts +2 -1
  41. package/dist/views/Steps/StepModal.d.ts.map +1 -1
  42. package/dist/views/Steps/StepModal.js +24 -7
  43. package/dist/views/Steps/StepModal.js.map +1 -1
  44. package/dist/views/Steps/StepsPanel.d.ts.map +1 -1
  45. package/dist/views/Steps/StepsPanel.js +6 -2
  46. package/dist/views/Steps/StepsPanel.js.map +1 -1
  47. package/dist/views/Steps/dictionary.d.ts +5 -1
  48. package/dist/views/Steps/dictionary.d.ts.map +1 -1
  49. package/dist/views/Steps/dictionary.js +4 -0
  50. package/dist/views/Steps/dictionary.js.map +1 -1
  51. package/dist/views/Steps/utils.d.ts.map +1 -1
  52. package/dist/views/Steps/utils.js +2 -2
  53. package/dist/views/Steps/utils.js.map +1 -1
  54. package/package.json +2 -2
  55. package/src/app-metadata.json +3 -3
  56. package/src/views/Chat/ButtonExecutionDetail.tsx +46 -0
  57. package/src/views/Chat/ChatMessage.tsx +9 -4
  58. package/src/views/Steps/FlowChart/HandleGroup.tsx +5 -3
  59. package/src/views/Steps/FlowChart/NodeStep.tsx +7 -5
  60. package/src/views/Steps/FlowChart/NodeTool.tsx +97 -0
  61. package/src/views/Steps/FlowChart/hooks.ts +40 -0
  62. package/src/views/Steps/FlowChart/index.tsx +67 -23
  63. package/src/views/Steps/FlowChart/layout.ts +39 -16
  64. package/src/views/Steps/FlowChart/styled.ts +39 -15
  65. package/src/views/Steps/FlowChart/types.ts +16 -2
  66. package/src/views/Steps/StepModal.tsx +36 -13
  67. package/src/views/Steps/StepsPanel.tsx +9 -2
  68. package/src/views/Steps/dictionary.ts +4 -0
  69. 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 shouldHideToolbox = entry?.steps?.some((step) => step?.id === 'dynamic')
232
- const showToolBox = (!!agentsTools?.length || !!entry.tools?.length) && !shouldHideToolbox
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 shouldShowToolsOnlyMessage = (entry.done !== false || entry.hasPlanning) && !!entry.steps?.length
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 { useMemo } from 'react'
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 { FlowChartBox, runningColor } from './styled'
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, index: number) => void,
17
+ onClick: (step: ChatStep, toolIndex?: number) => void,
18
+ direction?: LayoutDirection,
15
19
  }
16
20
 
17
- const nodeTypes = {
18
- planning: NodeStep,
19
- step: NodeStep,
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
- export const FlowChart = ({ message, onClick }: Props) => {
26
+ const Flow = ({ message, onClick, direction = 'LR' }: Props) => {
24
27
  const steps = useChatEntry(message).steps
25
- const { nodes, edges } = useMemo(() => {
26
- const nodes = steps?.map((s, i) => ({
27
- id: s.id,
28
- type: s.type,
29
- focusable: false,
30
- data: { step: s, index: i, nextStatus: steps[i + 1]?.status, onClick: () => onClick(s, i) },
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: nodes[i]?.data?.nextStatus === 'running' ? runningColor : theme.color.light[700],
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
- const layouted = useLayoutedElements(nodes, edges)
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
- // @ts-ignore wrong type in the lib
56
- nodes={layouted.nodes}
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 { NodeWithoutLayout } from './types'
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 function useLayoutedElements(nodes: NodeWithoutLayout[], edges: Edge[]) {
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: 'LR' })
18
-
33
+ dagreGraph.setGraph({ rankdir: direction })
34
+
19
35
  nodes.forEach((node) => {
20
36
  const { width, height } = nodesSizes[node.type]
21
- dagreGraph.setNode(node.id, { width, height })
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 newNodes = nodes.map((node) => {
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: 'left',
36
- sourcePosition: 'right',
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 - width / 2,
41
- y: nodeWithPosition.y - height / 2,
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: 160, height: 167 }
5
- export const planningNodeSize = { width: 160, height: 61 }
6
- export const answerNodeSize = { width: 160, height: 40 }
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: 10px;
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: 'step' | 'planning' | 'answer' | 'tool',
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
+