@stack-spot/ai-chat-widget 1.8.5 → 1.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.
Files changed (143) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/StackspotAIWidget.d.ts +5 -1
  3. package/dist/StackspotAIWidget.d.ts.map +1 -1
  4. package/dist/StackspotAIWidget.js +6 -5
  5. package/dist/StackspotAIWidget.js.map +1 -1
  6. package/dist/app-metadata.json +31 -19
  7. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  8. package/dist/chat-interceptors/send-message.js +3 -1
  9. package/dist/chat-interceptors/send-message.js.map +1 -1
  10. package/dist/components/Accordion.d.ts.map +1 -1
  11. package/dist/components/AnimatedOpacity.d.ts +8 -0
  12. package/dist/components/AnimatedOpacity.d.ts.map +1 -0
  13. package/dist/components/AnimatedOpacity.js +46 -0
  14. package/dist/components/AnimatedOpacity.js.map +1 -0
  15. package/dist/components/Code.d.ts +2 -1
  16. package/dist/components/Code.d.ts.map +1 -1
  17. package/dist/components/Code.js +4 -4
  18. package/dist/components/Code.js.map +1 -1
  19. package/dist/components/FadingOverflow.d.ts.map +1 -1
  20. package/dist/components/FallbackBoundary/index.d.ts.map +1 -1
  21. package/dist/components/IconInput.d.ts.map +1 -1
  22. package/dist/components/Markdown.d.ts.map +1 -1
  23. package/dist/components/Modal.d.ts +9 -0
  24. package/dist/components/Modal.d.ts.map +1 -0
  25. package/dist/components/Modal.js +58 -0
  26. package/dist/components/Modal.js.map +1 -0
  27. package/dist/components/ProgressBar.d.ts.map +1 -1
  28. package/dist/components/QuickStartButton.d.ts.map +1 -1
  29. package/dist/components/RightPanelForm.d.ts.map +1 -1
  30. package/dist/components/RightPanelTabs.d.ts.map +1 -1
  31. package/dist/components/Selector/index.d.ts.map +1 -1
  32. package/dist/components/Tooltip/context.d.ts.map +1 -1
  33. package/dist/layout.css +34 -0
  34. package/dist/right-panel/DefaultPanel.d.ts.map +1 -1
  35. package/dist/right-panel/RightPanelProvider.d.ts.map +1 -1
  36. package/dist/state/ChatEntry.d.ts +74 -3
  37. package/dist/state/ChatEntry.d.ts.map +1 -1
  38. package/dist/state/ChatEntry.js +4 -1
  39. package/dist/state/ChatEntry.js.map +1 -1
  40. package/dist/state/WidgetState.d.ts +8 -1
  41. package/dist/state/WidgetState.d.ts.map +1 -1
  42. package/dist/state/WidgetState.js +2 -2
  43. package/dist/state/WidgetState.js.map +1 -1
  44. package/dist/utils/error.d.ts +2 -0
  45. package/dist/utils/error.d.ts.map +1 -0
  46. package/dist/utils/error.js +54 -0
  47. package/dist/utils/error.js.map +1 -0
  48. package/dist/views/Agents/AgentDescription.d.ts.map +1 -1
  49. package/dist/views/Agents/AgentsTab.d.ts.map +1 -1
  50. package/dist/views/Chat/AgentInfo.d.ts.map +1 -1
  51. package/dist/views/Chat/ChatMessage.d.ts +2 -1
  52. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  53. package/dist/views/Chat/ChatMessage.js +65 -7
  54. package/dist/views/Chat/ChatMessage.js.map +1 -1
  55. package/dist/views/Chat/ChatMessages.d.ts.map +1 -1
  56. package/dist/views/Chat/StepsList.d.ts +9 -0
  57. package/dist/views/Chat/StepsList.d.ts.map +1 -0
  58. package/dist/views/Chat/StepsList.js +51 -0
  59. package/dist/views/Chat/StepsList.js.map +1 -0
  60. package/dist/views/Chat/index.d.ts.map +1 -1
  61. package/dist/views/Chat/styled.d.ts +3 -1
  62. package/dist/views/Chat/styled.d.ts.map +1 -1
  63. package/dist/views/Chat/styled.js +56 -0
  64. package/dist/views/Chat/styled.js.map +1 -1
  65. package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -1
  66. package/dist/views/Home/BuiltInAgent.d.ts.map +1 -1
  67. package/dist/views/Home/index.d.ts.map +1 -1
  68. package/dist/views/MessageInput/AgentSelector.d.ts.map +1 -1
  69. package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
  70. package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -1
  71. package/dist/views/MinimizedHeader.d.ts.map +1 -1
  72. package/dist/views/Stacks.js +1 -0
  73. package/dist/views/Stacks.js.map +1 -1
  74. package/dist/views/Tools/FlowChart/HandleGroup.d.ts +7 -0
  75. package/dist/views/Tools/FlowChart/HandleGroup.d.ts.map +1 -0
  76. package/dist/views/Tools/FlowChart/HandleGroup.js +4 -0
  77. package/dist/views/Tools/FlowChart/HandleGroup.js.map +1 -0
  78. package/dist/views/Tools/FlowChart/NodeStep.d.ts +7 -0
  79. package/dist/views/Tools/FlowChart/NodeStep.d.ts.map +1 -0
  80. package/dist/views/Tools/FlowChart/NodeStep.js +15 -0
  81. package/dist/views/Tools/FlowChart/NodeStep.js.map +1 -0
  82. package/dist/views/Tools/FlowChart/index.d.ts +9 -0
  83. package/dist/views/Tools/FlowChart/index.d.ts.map +1 -0
  84. package/dist/views/Tools/FlowChart/index.js +52 -0
  85. package/dist/views/Tools/FlowChart/index.js.map +1 -0
  86. package/dist/views/Tools/FlowChart/layout.d.ts +17 -0
  87. package/dist/views/Tools/FlowChart/layout.d.ts.map +1 -0
  88. package/dist/views/Tools/FlowChart/layout.js +40 -0
  89. package/dist/views/Tools/FlowChart/layout.js.map +1 -0
  90. package/dist/views/Tools/FlowChart/styled.d.ts +15 -0
  91. package/dist/views/Tools/FlowChart/styled.d.ts.map +1 -0
  92. package/dist/views/Tools/FlowChart/styled.js +181 -0
  93. package/dist/views/Tools/FlowChart/styled.js.map +1 -0
  94. package/dist/views/Tools/FlowChart/types.d.ts +13 -0
  95. package/dist/views/Tools/FlowChart/types.d.ts.map +1 -0
  96. package/dist/views/Tools/FlowChart/types.js +2 -0
  97. package/dist/views/Tools/FlowChart/types.js.map +1 -0
  98. package/dist/views/Tools/StepModal.d.ts +9 -0
  99. package/dist/views/Tools/StepModal.d.ts.map +1 -0
  100. package/dist/views/Tools/StepModal.js +156 -0
  101. package/dist/views/Tools/StepModal.js.map +1 -0
  102. package/dist/views/Tools/ToolsPanel.d.ts +6 -0
  103. package/dist/views/Tools/ToolsPanel.d.ts.map +1 -0
  104. package/dist/views/Tools/ToolsPanel.js +14 -0
  105. package/dist/views/Tools/ToolsPanel.js.map +1 -0
  106. package/dist/views/Tools/dictionary.d.ts +41 -0
  107. package/dist/views/Tools/dictionary.d.ts.map +1 -0
  108. package/dist/views/Tools/dictionary.js +43 -0
  109. package/dist/views/Tools/dictionary.js.map +1 -0
  110. package/dist/views/Tools/index.d.ts +5 -0
  111. package/dist/views/Tools/index.d.ts.map +1 -0
  112. package/dist/views/Tools/index.js +31 -0
  113. package/dist/views/Tools/index.js.map +1 -0
  114. package/dist/views/Tools/utils.d.ts +6 -0
  115. package/dist/views/Tools/utils.d.ts.map +1 -0
  116. package/dist/views/Tools/utils.js +32 -0
  117. package/dist/views/Tools/utils.js.map +1 -0
  118. package/package.json +9 -6
  119. package/src/StackspotAIWidget.tsx +13 -4
  120. package/src/app-metadata.json +31 -19
  121. package/src/chat-interceptors/send-message.ts +8 -3
  122. package/src/components/AnimatedOpacity.tsx +55 -0
  123. package/src/components/Code.tsx +5 -3
  124. package/src/components/Modal.tsx +87 -0
  125. package/src/layout.css +34 -0
  126. package/src/state/ChatEntry.ts +79 -4
  127. package/src/state/WidgetState.ts +6 -2
  128. package/src/utils/error.ts +56 -0
  129. package/src/views/Chat/ChatMessage.tsx +121 -18
  130. package/src/views/Chat/StepsList.tsx +97 -0
  131. package/src/views/Chat/styled.ts +62 -1
  132. package/src/views/Stacks.tsx +1 -0
  133. package/src/views/Tools/FlowChart/HandleGroup.tsx +12 -0
  134. package/src/views/Tools/FlowChart/NodeStep.tsx +57 -0
  135. package/src/views/Tools/FlowChart/index.tsx +71 -0
  136. package/src/views/Tools/FlowChart/layout.ts +49 -0
  137. package/src/views/Tools/FlowChart/styled.ts +182 -0
  138. package/src/views/Tools/FlowChart/types.ts +14 -0
  139. package/src/views/Tools/StepModal.tsx +247 -0
  140. package/src/views/Tools/ToolsPanel.tsx +24 -0
  141. package/src/views/Tools/dictionary.ts +46 -0
  142. package/src/views/Tools/index.tsx +37 -0
  143. package/src/views/Tools/utils.tsx +34 -0
@@ -0,0 +1,97 @@
1
+ import { Button, IconBox, Text } from '@citric/core'
2
+ import { CheckCircleFill, Circle, PlayFill, TimesCircleFill } from '@citric/icons'
3
+ import { LoadingCircular } from '@citric/ui'
4
+ import { AnimatedHeight } from '@stack-spot/portal-components/AnimatedHeight'
5
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
6
+ import { findLastIndex } from 'lodash'
7
+ import { useState } from 'react'
8
+ import { useWidget } from '../../context/hooks'
9
+ import { ChatEntryStep } from '../../state/ChatEntry'
10
+ import { PropsOf } from '../../types'
11
+
12
+ interface Props {
13
+ steps: ChatEntryStep[],
14
+ messageId: number,
15
+ chatId: string,
16
+ }
17
+
18
+ interface StepProps {
19
+ step: ChatEntryStep,
20
+ index: number,
21
+ total: number,
22
+ onClick?: () => void,
23
+ }
24
+
25
+ function getStatusIcon(status: ChatEntryStep['status']) {
26
+ const iconBoxProps: PropsOf<typeof IconBox> = { colorIcon: 'light.700', size: 'xs' }
27
+ switch (status) {
28
+ case 'error': return <IconBox {...iconBoxProps}><TimesCircleFill /></IconBox>
29
+ case 'success': return <IconBox {...iconBoxProps}><CheckCircleFill /></IconBox>
30
+ case 'pending': return <IconBox {...iconBoxProps}><Circle /></IconBox>
31
+ case 'running': return <LoadingCircular className="loading" colorScheme="inverse" size="xs" />
32
+ }
33
+ }
34
+
35
+ const Step = ({ step, index, total, onClick }: StepProps) => {
36
+ const t = useTranslate(dictionary)
37
+ return (
38
+ <li tabIndex={onClick ? 0 : undefined} onClick={onClick} role={onClick ? 'button' : 'listitem'}>
39
+ <div className="step-status-icon">{getStatusIcon(step.status)}</div>
40
+ <Text className="step-title" appearance="microtext1" colorScheme="light.700">
41
+ {t.step} {index}/{total}: {step.input}
42
+ </Text>
43
+ </li>
44
+ )
45
+ }
46
+
47
+ export const StepsList = ({ steps, chatId, messageId }: Props) => {
48
+ const t = useTranslate(dictionary)
49
+ const [isExpanded, setExpanded] = useState(false)
50
+ const actualSteps = steps.filter(s => s.type === 'step')
51
+ let currentStepIndex = findLastIndex(actualSteps, s => s.status === 'running' || s.status === 'success')
52
+ if (currentStepIndex === -1) currentStepIndex = 0
53
+ const widget = useWidget()
54
+
55
+ function openToolsPanel() {
56
+ widget.set('currentMessageInToolsPanel', { chatId, messageId })
57
+ widget.set('panel', 'tools')
58
+ }
59
+
60
+ return (
61
+ <AnimatedHeight>
62
+ <div className="steps">
63
+ <ul>
64
+ {isExpanded
65
+ ? actualSteps.map((s, i) => <Step step={s} key={i} index={i + 1} total={actualSteps.length} />)
66
+ : <Step
67
+ step={actualSteps[currentStepIndex]}
68
+ index={currentStepIndex + 1}
69
+ total={actualSteps.length}
70
+ onClick={() => setExpanded(true)}
71
+ />
72
+ }
73
+ </ul>
74
+ {isExpanded && <div className="step-actions">
75
+ <Button colorScheme="light" size="sm" onClick={() => setExpanded(false)}>{t.hideSteps}</Button>
76
+ <Button colorScheme="light" size="sm" className="icon-button" onClick={openToolsPanel}>
77
+ <IconBox size="xs"><PlayFill /></IconBox>
78
+ {t.detailed}
79
+ </Button>
80
+ </div>}
81
+ </div>
82
+ </AnimatedHeight>
83
+ )
84
+ }
85
+
86
+ const dictionary = {
87
+ en: {
88
+ step: 'Step',
89
+ hideSteps: 'Hide steps',
90
+ detailed: 'View detailed mode',
91
+ },
92
+ pt: {
93
+ step: 'Passo',
94
+ hideSteps: 'Esconder passos',
95
+ detailed: 'Ver modo detalhado',
96
+ },
97
+ } satisfies Dictionary
@@ -1,7 +1,12 @@
1
1
  import { theme } from '@stack-spot/portal-theme'
2
+ import { DetailedHTMLProps, HTMLAttributes } from 'react'
2
3
  import { styled } from 'styled-components'
4
+ import { FastOmit, IStyledComponentBase } from 'styled-components/dist/types'
3
5
 
4
- export const ChatList = styled.ul`
6
+ export const ChatList: IStyledComponentBase<
7
+ 'web',
8
+ FastOmit<DetailedHTMLProps<HTMLAttributes<HTMLUListElement>, HTMLUListElement>, never>
9
+ > = styled.ul`
5
10
  display: flex;
6
11
  flex-direction: column;
7
12
  justify-content: end;
@@ -183,4 +188,60 @@ export const ChatList = styled.ul`
183
188
  }
184
189
  }
185
190
  }
191
+
192
+ .steps {
193
+ ul {
194
+ list-style: none;
195
+ margin: 0;
196
+ padding: 0;
197
+ display: flex;
198
+ flex-direction: column;
199
+ gap: 6px;
200
+
201
+ li {
202
+ display: flex;
203
+ flex-direction: row;
204
+ gap: 4px;
205
+ align-items: center;
206
+
207
+ &[role="button"] {
208
+ cursor: pointer;
209
+ }
210
+
211
+ .loading {
212
+ width: 12px;
213
+ height: 12px;
214
+ }
215
+
216
+ .step-status-icon {
217
+ width: 20px;
218
+ height: 20px;
219
+ display: flex;
220
+ justify-content: center;
221
+ align-items: center;
222
+ }
223
+
224
+ .step-title {
225
+ line-height: 0.75rem;
226
+ overflow: hidden;
227
+ text-overflow: ellipsis;
228
+ display: -webkit-box;
229
+ -webkit-line-clamp: 1;
230
+ -webkit-box-orient: vertical;
231
+ }
232
+ }
233
+ }
234
+
235
+ .step-actions {
236
+ margin-top: 8px;
237
+ display: flex;
238
+ gap: 6px;
239
+
240
+ .icon-button {
241
+ display: flex;
242
+ gap: 6px;
243
+ align-items: center;
244
+ }
245
+ }
246
+ }
186
247
  `
@@ -46,6 +46,7 @@ const StacksTab = ({ visibility }: { visibility: VisibilityLevelEnum }) => {
46
46
  const { close } = useRightPanel()
47
47
  const chat = useCurrentChat()
48
48
  const [filter, setFilter] = useState('')
49
+ // @ts-ignore type in backend (openapi) is incorrect. fixme.
49
50
  const stacks = aiClient.aiStacks.useQuery({ visibility, order: 'a-to-z' })
50
51
  const [value, setValue] = useState<GetAiStackResponse | undefined>(stacks.find(s => s.id === chat.get('stack')?.id))
51
52
  const filtered = useMemo(
@@ -0,0 +1,12 @@
1
+ import { Handle, Position } from '@xyflow/react'
2
+
3
+ interface Props {
4
+ renderSource?: boolean,
5
+ renderTarget?: boolean,
6
+ }
7
+ export const HandleGroup = ({ renderSource = true, renderTarget = true }: Props) => (
8
+ <>
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
+ </>
12
+ )
@@ -0,0 +1,57 @@
1
+ import { IconBox, Text } from '@citric/core'
2
+ import { Cog } from '@citric/icons'
3
+ import { listToClass } from '@stack-spot/portal-theme'
4
+ import { last } from 'lodash'
5
+ import { useToolsDictionary } from '../dictionary'
6
+ import { getStatusIcon, getTitle, getTypeIcon } from '../utils'
7
+ import { HandleGroup } from './HandleGroup'
8
+ import { NodeData } from './types'
9
+
10
+ interface Props {
11
+ data: NodeData,
12
+ }
13
+
14
+ export const NodeStep = ({ data: { step, index, nextStatus, onClick } }: Props) => {
15
+ const t = useToolsDictionary()
16
+
17
+ return (
18
+ <div
19
+ className={listToClass(['chart-node', step.type, nextStatus])}
20
+ onClick={onClick}
21
+ onKeyDown={e => e.key === 'Enter' && onClick?.()}
22
+ tabIndex={0}
23
+ role="button"
24
+ aria-label={getTitle(t, step, index)}
25
+ >
26
+ <header>
27
+ <IconBox>{getTypeIcon(step.type)}</IconBox>
28
+ <Text className="step-index">{getTitle(t, step, index)}</Text>
29
+ {getStatusIcon(step.status)}
30
+ </header>
31
+ {step.input && <Text className="step-title">{step.input}</Text>}
32
+ {step.type === 'step' && <div className="step-details">
33
+ <Text className={listToClass(['step-description', !!step.tools?.length && 'with-tools'])}>
34
+ {step.output || last(step.tools)?.output}
35
+ </Text>
36
+ {!!step.tools?.length && (
37
+ <div className="step-tools">
38
+ <Text appearance="microtext1">{t.tools}</Text>
39
+ <ul>
40
+ {step.tools.slice(0, 3).map(
41
+ ({ id, name, image }) => (
42
+ <li key={id}>
43
+ {image
44
+ ? <img alt={name} aria-label={name} title={name} src={image} />
45
+ : <IconBox size="xs" aria-label={name} title={name}><Cog /></IconBox>
46
+ }
47
+ </li>
48
+ ),
49
+ )}
50
+ </ul>
51
+ </div>
52
+ )}
53
+ </div>}
54
+ <HandleGroup renderSource={step.type !== 'answer'} renderTarget={step.type !== 'planning'} />
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,71 @@
1
+ import { listToClass, theme } from '@stack-spot/portal-theme'
2
+ import { Background, Controls, Edge, MarkerType, ReactFlow } from '@xyflow/react'
3
+ import '@xyflow/react/dist/style.css'
4
+ import { useMemo } from 'react'
5
+ import { useChatEntry } from '../../../context/hooks'
6
+ import { ChatEntry, ChatEntryStep } from '../../../state/ChatEntry'
7
+ import { useLayoutedElements } from './layout'
8
+ import { NodeStep } from './NodeStep'
9
+ import { FlowChartBox, runningColor } from './styled'
10
+
11
+ interface Props {
12
+ message: ChatEntry,
13
+ onClick: (step: ChatEntryStep, index: number) => void,
14
+ }
15
+
16
+ const nodeTypes = {
17
+ planning: NodeStep,
18
+ step: NodeStep,
19
+ answer: NodeStep,
20
+ }
21
+
22
+ export const FlowChart = ({ message, onClick }: Props) => {
23
+ const steps = useChatEntry(message).steps
24
+ const { nodes, edges } = useMemo(() => {
25
+ const nodes = steps?.map((s, i) => ({
26
+ id: s.id,
27
+ type: s.type,
28
+ focusable: false,
29
+ data: { step: s, index: i, nextStatus: steps[i + 1]?.status, onClick: () => onClick(s, i) },
30
+ })) ?? []
31
+ const edges: Edge[] = []
32
+ for (let i = 0; i < nodes.length - 1; i++) {
33
+ edges.push({
34
+ id: `${nodes[i].id}-${nodes[i + 1].id}`,
35
+ source: nodes[i].id,
36
+ target: nodes[i + 1].id,
37
+ className: listToClass(['edge', nodes[i]?.data?.nextStatus ?? 'pending']),
38
+ focusable: false,
39
+ markerEnd: {
40
+ type: MarkerType.Arrow,
41
+ strokeWidth: 2,
42
+ color: nodes[i]?.data?.nextStatus === 'running' ? runningColor : theme.color.light[700],
43
+ },
44
+ })
45
+ }
46
+ return { nodes, edges }
47
+ }, [steps])
48
+
49
+ const layouted = useLayoutedElements(nodes, edges)
50
+
51
+ return (
52
+ <FlowChartBox>
53
+ <ReactFlow
54
+ // @ts-ignore wrong type in the lib
55
+ nodes={layouted.nodes}
56
+ edges={layouted.edges}
57
+ nodeTypes={nodeTypes}
58
+ snapToGrid={true}
59
+ fitViewOptions={{
60
+ minZoom: 1,
61
+ maxZoom: 1,
62
+ nodes: [layouted.nodes.find(n => n.data?.nextStatus === 'pending') ?? layouted.nodes[0]],
63
+ }}
64
+ fitView
65
+ >
66
+ <Controls orientation="horizontal" className="controls" showInteractive={false} />
67
+ <Background />
68
+ </ReactFlow>
69
+ </FlowChartBox>
70
+ )
71
+ }
@@ -0,0 +1,49 @@
1
+ import dagre from '@dagrejs/dagre'
2
+ import { Edge } from '@xyflow/react'
3
+ import { useMemo } from 'react'
4
+ import { answerNodeSize, planningNodeSize, stepNodeSize } from './styled'
5
+ import { NodeWithoutLayout } from './types'
6
+
7
+ const nodesSizes = {
8
+ step: stepNodeSize,
9
+ planning: planningNodeSize,
10
+ answer: answerNodeSize,
11
+ }
12
+
13
+ export function useLayoutedElements(nodes: NodeWithoutLayout[], edges: Edge[]) {
14
+ const dagreGraph = useMemo(() => new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})), [])
15
+ return useMemo(() => {
16
+ dagreGraph.setGraph({ rankdir: 'LR' })
17
+
18
+ nodes.forEach((node) => {
19
+ const { width, height } = nodesSizes[node.type]
20
+ dagreGraph.setNode(node.id, { width, height })
21
+ })
22
+
23
+ edges.forEach((edge) => {
24
+ dagreGraph.setEdge(edge.source, edge.target)
25
+ })
26
+
27
+ dagre.layout(dagreGraph)
28
+
29
+ const newNodes = nodes.map((node) => {
30
+ const { width, height } = nodesSizes[node.type]
31
+ const nodeWithPosition = dagreGraph.node(node.id)
32
+ const newNode = {
33
+ ...node,
34
+ targetPosition: 'left',
35
+ sourcePosition: 'right',
36
+ // We are shifting the dagre node position (anchor=center center) to the top left
37
+ // so it matches the React Flow node anchor point (top left).
38
+ position: {
39
+ x: nodeWithPosition.x - width / 2,
40
+ y: nodeWithPosition.y - height / 2,
41
+ },
42
+ }
43
+
44
+ return newNode
45
+ })
46
+
47
+ return { nodes: newNodes, edges }
48
+ }, [nodes, edges])
49
+ }
@@ -0,0 +1,182 @@
1
+ import { theme } from '@stack-spot/portal-theme'
2
+ import { styled } from 'styled-components'
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'
8
+
9
+ export const FlowChartBox = styled.div`
10
+ width: 100%;
11
+ height: 100%;
12
+
13
+ .chart-node {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 10px;
17
+ padding: 6px;
18
+ border-radius: 4px;
19
+ border: 1px solid ${theme.color.light[600]};
20
+ background-color: ${theme.color.light[500]};
21
+ box-sizing: border-box;
22
+ justify-content: center;
23
+
24
+ &.running .source-handle {
25
+ background-color: ${runningColor};
26
+ }
27
+
28
+ &.pending .source-handle {
29
+ opacity: 0.3;
30
+ }
31
+
32
+ &.step {
33
+ width: ${stepNodeSize.width}px;
34
+ height: ${stepNodeSize.height}px;
35
+ }
36
+
37
+ &.planning {
38
+ width: ${planningNodeSize.width}px;
39
+ height: ${planningNodeSize.height}px;
40
+ }
41
+
42
+ &.answer {
43
+ width: ${answerNodeSize.width}px;
44
+ height: ${answerNodeSize.height}px;
45
+ }
46
+
47
+ header {
48
+ display: flex;
49
+ gap: 4px;
50
+ align-items: center;
51
+ .step-index {
52
+ flex: 1;
53
+ white-space: nowrap;
54
+ text-overflow: ellipsis;
55
+ }
56
+ }
57
+
58
+ .step-title {
59
+ white-space: nowrap;
60
+ text-overflow: ellipsis;
61
+ color: ${theme.color.light[700]};
62
+ overflow-x: clip;
63
+ }
64
+
65
+ .step-details {
66
+ background-color: ${theme.color.light[400]};
67
+ border-radius: 2px;
68
+ padding: 8px;
69
+ flex: 1;
70
+ display: flex;
71
+ flex-direction: column;
72
+ align-items: start;
73
+ justify-content: space-between;
74
+ gap: 10px;
75
+ overflow: hidden;
76
+
77
+ .step-description {
78
+ flex: 1;
79
+ line-height: 18px;
80
+ max-height: 72px; // line-height * 4
81
+ overflow: hidden;
82
+ display: -webkit-box;
83
+ -webkit-line-clamp: 4;
84
+ -webkit-box-orient: vertical;
85
+
86
+ &.with-tools {
87
+ max-height: 36px; // line-height * 2
88
+ -webkit-line-clamp: 2;
89
+ }
90
+ }
91
+
92
+ .step-tools {
93
+ border-radius: 25px;
94
+ background-color: ${theme.color.light[600]};
95
+ border: 1px solid ${theme.color.light[500]};
96
+ color: ${theme.color.light[700]};
97
+ padding: 2px 7px;
98
+ display: flex;
99
+ gap: 5px;
100
+ align-items: center;
101
+
102
+ small {
103
+ line-height: 0.75rem;
104
+ }
105
+
106
+ ul {
107
+ list-style: none;
108
+ margin: 0;
109
+ padding: 0;
110
+ display: flex;
111
+ flex-direction: row;
112
+
113
+ li {
114
+ border: 1px solid ${theme.color.light[600]};
115
+ background-color: ${theme.color.light[400]};
116
+ border-radius: 50%;
117
+ width: 16px;
118
+ height: 16px;
119
+ overflow: hidden;
120
+ display: flex;
121
+ align-items: center;
122
+ justify-content: center;
123
+
124
+ &:not(:first-child) {
125
+ margin-left: -12px;
126
+ }
127
+ }
128
+
129
+ i {
130
+ width: 12px;
131
+ height: 12px;
132
+ }
133
+
134
+ img, svg {
135
+ width: 100%;
136
+ height: 100%;
137
+ }
138
+ }
139
+ }
140
+ }
141
+ }
142
+
143
+ .source-handle {
144
+ background-color: ${theme.color.light[700]};
145
+ border: none;
146
+ }
147
+
148
+ .target-handle {
149
+ opacity: 0;
150
+ }
151
+
152
+ .controls {
153
+ background-color: ${theme.color.light[300]};
154
+ border-radius: 4px;
155
+ border: none;
156
+ display: flex;
157
+
158
+ button {
159
+ background: transparent;
160
+ border: none;
161
+ }
162
+ }
163
+
164
+ .edge {
165
+ path {
166
+ stroke: ${theme.color.light[700]};
167
+ stroke-width: 2px;
168
+ }
169
+ &.pending path {
170
+ opacity: 0.3;
171
+ }
172
+ &.running path {
173
+ stroke: ${runningColor};
174
+ stroke-dasharray: 5, 5;
175
+ }
176
+ }
177
+
178
+ .react-flow__attribution {
179
+ background-color: transparent;
180
+ opacity: 0.1;
181
+ }
182
+ `
@@ -0,0 +1,14 @@
1
+ import { ChatEntryStep } from '../../../state/ChatEntry'
2
+
3
+ export interface NodeData {
4
+ nextStatus: ChatEntryStep['status'] | undefined,
5
+ onClick?: () => void,
6
+ step: ChatEntryStep,
7
+ index: number,
8
+ }
9
+
10
+ export interface NodeWithoutLayout {
11
+ id: string,
12
+ type: 'step' | 'planning' | 'answer',
13
+ data?: NodeData,
14
+ }