@stack-spot/ai-chat-widget 1.9.0 → 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 (115) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/StackspotAIWidget.d.ts.map +1 -1
  3. package/dist/StackspotAIWidget.js +2 -1
  4. package/dist/StackspotAIWidget.js.map +1 -1
  5. package/dist/app-metadata.json +11 -3
  6. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  7. package/dist/chat-interceptors/send-message.js +3 -1
  8. package/dist/chat-interceptors/send-message.js.map +1 -1
  9. package/dist/components/AnimatedOpacity.d.ts +8 -0
  10. package/dist/components/AnimatedOpacity.d.ts.map +1 -0
  11. package/dist/components/AnimatedOpacity.js +46 -0
  12. package/dist/components/AnimatedOpacity.js.map +1 -0
  13. package/dist/components/Code.d.ts +2 -1
  14. package/dist/components/Code.d.ts.map +1 -1
  15. package/dist/components/Code.js +4 -4
  16. package/dist/components/Code.js.map +1 -1
  17. package/dist/components/Modal.d.ts +9 -0
  18. package/dist/components/Modal.d.ts.map +1 -0
  19. package/dist/components/Modal.js +58 -0
  20. package/dist/components/Modal.js.map +1 -0
  21. package/dist/layout.css +21 -0
  22. package/dist/state/ChatEntry.d.ts +21 -2
  23. package/dist/state/ChatEntry.d.ts.map +1 -1
  24. package/dist/state/ChatEntry.js.map +1 -1
  25. package/dist/state/WidgetState.d.ts +8 -1
  26. package/dist/state/WidgetState.d.ts.map +1 -1
  27. package/dist/state/WidgetState.js.map +1 -1
  28. package/dist/utils/error.d.ts +2 -0
  29. package/dist/utils/error.d.ts.map +1 -0
  30. package/dist/utils/error.js +54 -0
  31. package/dist/utils/error.js.map +1 -0
  32. package/dist/views/Chat/ChatMessage.d.ts +1 -1
  33. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  34. package/dist/views/Chat/ChatMessage.js +2 -1
  35. package/dist/views/Chat/ChatMessage.js.map +1 -1
  36. package/dist/views/Chat/StepsList.d.ts +9 -0
  37. package/dist/views/Chat/StepsList.d.ts.map +1 -0
  38. package/dist/views/Chat/StepsList.js +51 -0
  39. package/dist/views/Chat/StepsList.js.map +1 -0
  40. package/dist/views/Chat/styled.d.ts +3 -1
  41. package/dist/views/Chat/styled.d.ts.map +1 -1
  42. package/dist/views/Chat/styled.js +56 -0
  43. package/dist/views/Chat/styled.js.map +1 -1
  44. package/dist/views/Stacks.js +1 -0
  45. package/dist/views/Stacks.js.map +1 -1
  46. package/dist/views/Tools/FlowChart/HandleGroup.d.ts +7 -0
  47. package/dist/views/Tools/FlowChart/HandleGroup.d.ts.map +1 -0
  48. package/dist/views/Tools/FlowChart/HandleGroup.js +4 -0
  49. package/dist/views/Tools/FlowChart/HandleGroup.js.map +1 -0
  50. package/dist/views/Tools/FlowChart/NodeStep.d.ts +7 -0
  51. package/dist/views/Tools/FlowChart/NodeStep.d.ts.map +1 -0
  52. package/dist/views/Tools/FlowChart/NodeStep.js +15 -0
  53. package/dist/views/Tools/FlowChart/NodeStep.js.map +1 -0
  54. package/dist/views/Tools/FlowChart/index.d.ts +9 -0
  55. package/dist/views/Tools/FlowChart/index.d.ts.map +1 -0
  56. package/dist/views/Tools/FlowChart/index.js +52 -0
  57. package/dist/views/Tools/FlowChart/index.js.map +1 -0
  58. package/dist/views/Tools/FlowChart/layout.d.ts +17 -0
  59. package/dist/views/Tools/FlowChart/layout.d.ts.map +1 -0
  60. package/dist/views/Tools/FlowChart/layout.js +40 -0
  61. package/dist/views/Tools/FlowChart/layout.js.map +1 -0
  62. package/dist/views/Tools/FlowChart/styled.d.ts +15 -0
  63. package/dist/views/Tools/FlowChart/styled.d.ts.map +1 -0
  64. package/dist/views/Tools/FlowChart/styled.js +181 -0
  65. package/dist/views/Tools/FlowChart/styled.js.map +1 -0
  66. package/dist/views/Tools/FlowChart/types.d.ts +13 -0
  67. package/dist/views/Tools/FlowChart/types.d.ts.map +1 -0
  68. package/dist/views/Tools/FlowChart/types.js +2 -0
  69. package/dist/views/Tools/FlowChart/types.js.map +1 -0
  70. package/dist/views/Tools/StepModal.d.ts +9 -0
  71. package/dist/views/Tools/StepModal.d.ts.map +1 -0
  72. package/dist/views/Tools/StepModal.js +156 -0
  73. package/dist/views/Tools/StepModal.js.map +1 -0
  74. package/dist/views/Tools/ToolsPanel.d.ts +6 -0
  75. package/dist/views/Tools/ToolsPanel.d.ts.map +1 -0
  76. package/dist/views/Tools/ToolsPanel.js +14 -0
  77. package/dist/views/Tools/ToolsPanel.js.map +1 -0
  78. package/dist/views/Tools/dictionary.d.ts +41 -0
  79. package/dist/views/Tools/dictionary.d.ts.map +1 -0
  80. package/dist/views/Tools/dictionary.js +43 -0
  81. package/dist/views/Tools/dictionary.js.map +1 -0
  82. package/dist/views/Tools/index.d.ts +5 -0
  83. package/dist/views/Tools/index.d.ts.map +1 -0
  84. package/dist/views/Tools/index.js +31 -0
  85. package/dist/views/Tools/index.js.map +1 -0
  86. package/dist/views/Tools/utils.d.ts +6 -0
  87. package/dist/views/Tools/utils.d.ts.map +1 -0
  88. package/dist/views/Tools/utils.js +32 -0
  89. package/dist/views/Tools/utils.js.map +1 -0
  90. package/package.json +5 -3
  91. package/src/StackspotAIWidget.tsx +2 -0
  92. package/src/app-metadata.json +11 -3
  93. package/src/chat-interceptors/send-message.ts +8 -3
  94. package/src/components/AnimatedOpacity.tsx +55 -0
  95. package/src/components/Code.tsx +5 -3
  96. package/src/components/Modal.tsx +87 -0
  97. package/src/layout.css +21 -0
  98. package/src/state/ChatEntry.ts +25 -1
  99. package/src/state/WidgetState.ts +5 -1
  100. package/src/utils/error.ts +56 -0
  101. package/src/views/Chat/ChatMessage.tsx +5 -3
  102. package/src/views/Chat/StepsList.tsx +97 -0
  103. package/src/views/Chat/styled.ts +62 -1
  104. package/src/views/Stacks.tsx +1 -0
  105. package/src/views/Tools/FlowChart/HandleGroup.tsx +12 -0
  106. package/src/views/Tools/FlowChart/NodeStep.tsx +57 -0
  107. package/src/views/Tools/FlowChart/index.tsx +71 -0
  108. package/src/views/Tools/FlowChart/layout.ts +49 -0
  109. package/src/views/Tools/FlowChart/styled.ts +182 -0
  110. package/src/views/Tools/FlowChart/types.ts +14 -0
  111. package/src/views/Tools/StepModal.tsx +247 -0
  112. package/src/views/Tools/ToolsPanel.tsx +24 -0
  113. package/src/views/Tools/dictionary.ts +46 -0
  114. package/src/views/Tools/index.tsx +37 -0
  115. package/src/views/Tools/utils.tsx +34 -0
@@ -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
+ }
@@ -0,0 +1,247 @@
1
+ import { Flex, IconBox, Text } from '@citric/core'
2
+ import { ChevronDown, ChevronLeft, ChevronRight, Cog } from '@citric/icons'
3
+ import { Badge, IconButton } from '@citric/ui'
4
+ import { AnimatedHeight } from '@stack-spot/portal-components/AnimatedHeight'
5
+ import { listToClass, theme } from '@stack-spot/portal-theme'
6
+ import { interpolate } from '@stack-spot/portal-translate'
7
+ import { useEffect, useMemo, useState } from 'react'
8
+ import { styled } from 'styled-components'
9
+ import { Code } from '../../components/Code'
10
+ import { Markdown } from '../../components/Markdown'
11
+ import { Modal } from '../../components/Modal'
12
+ import { useChatEntry } from '../../context/hooks'
13
+ import { AgentTool, ChatEntry } from '../../state/ChatEntry'
14
+ import { useToolsDictionary } from './dictionary'
15
+ import { getTitle, toPrecision } from './utils'
16
+
17
+ interface Props {
18
+ message: ChatEntry,
19
+ stepId: string | undefined,
20
+ onClose: () => void,
21
+ }
22
+
23
+ const StyledSection = styled.section`
24
+ padding: 18px 14px;
25
+ border-bottom: 1px solid var(--light-600);
26
+ display: flex;
27
+ flex-direction: column;
28
+ gap: 12px;
29
+ align-items: start;
30
+
31
+ &:last-child {
32
+ border-bottom: none;
33
+ }
34
+
35
+ &.restrict-image-size img {
36
+ max-width: 100%;
37
+ }
38
+
39
+ .tool {
40
+ display: flex;
41
+ flex-direction: column;
42
+ align-items: start;
43
+ align-self: stretch;
44
+ gap: 6px;
45
+ background-color: ${theme.color.light[500]};
46
+ border-radius: 5px;
47
+ padding: 6px;
48
+
49
+ &.output {
50
+ padding: 0;
51
+ background-color: transparent;
52
+ }
53
+
54
+ .tool-header-wrapper {
55
+ background-color: ${theme.color.light[300]};
56
+ border-radius: 8px;
57
+ }
58
+
59
+ .tool-header {
60
+ padding: 4px 8px 4px 4px;
61
+ display: flex;
62
+ flex-direction: column;
63
+ gap: 12px;
64
+ position: relative;
65
+
66
+ &:before {
67
+ content: '';
68
+ top: 32px;
69
+ bottom: 8px;
70
+ left: 15px;
71
+ width: 1px;
72
+ background-color: ${theme.color.light[700]};
73
+ opacity: 0.3;
74
+ position: absolute;
75
+ }
76
+
77
+ .title {
78
+ display: flex;
79
+ gap: 8px;
80
+ align-items: center;
81
+ .tool-image {
82
+ width: 24px;
83
+ height: 24px;
84
+ border-radius: 50%;
85
+ overflow: hidden;
86
+ flex-shrink: 0;
87
+ }
88
+ }
89
+
90
+ .btn-expand {
91
+ transition: transform 0.3s ease-in-out;
92
+ &.open {
93
+ transform: rotate(180deg);
94
+ }
95
+ }
96
+
97
+ .duration {
98
+ opacity: 0.7;
99
+ text-align: right;
100
+ }
101
+
102
+ .description {
103
+ padding: 0 0 10px 20px;
104
+ opacity: 0.7;
105
+ }
106
+ }
107
+
108
+ .tool-input {
109
+ align-self: stretch;
110
+ &, .highlighter {
111
+ background: ${theme.color.light[300]} !important;
112
+ }
113
+ }
114
+ }
115
+ `
116
+
117
+ const ExecutionBox = styled.div`
118
+ border-radius: 4px;
119
+ background-color: ${theme.color.light[500]};
120
+ color: ${theme.color.light[700]};
121
+ display: flex;
122
+ align-items: center;
123
+ .time {
124
+ padding: 6px 8px;
125
+ border-right: 1px solid ${theme.color.light[600]};
126
+ }
127
+ .navigator {
128
+ display: flex;
129
+ gap: 4px;
130
+ align-items: center;
131
+ padding: 2px 8px;
132
+ button {
133
+ width: 12px;
134
+ height: 12px;
135
+ padding: 0;
136
+ }
137
+ small {
138
+ line-height: 0.75rem;
139
+ }
140
+ }
141
+ `
142
+
143
+ const ToolHeader = ({ tool }: { tool: AgentTool }) => {
144
+ const t = useToolsDictionary()
145
+ const [showDescription, setShowDescription] = useState(false)
146
+ return (
147
+ <AnimatedHeight className="tool-header-wrapper">
148
+ <div className="tool-header">
149
+ <div className="title">
150
+ {tool.image ? <img src={tool.image} className="tool-image" /> : <IconBox className="tool-image"><Cog /></IconBox>}
151
+ <Text colorScheme="light.700">{tool.name}</Text>
152
+ {tool.duration && <Text colorScheme="light.700" className="duration">
153
+ {interpolate(t.thoughtFor, toPrecision(tool.duration))}
154
+ </Text>}
155
+ {tool.description && <IconButton
156
+ size="xs"
157
+ className={listToClass(['btn-expand', showDescription && 'open'])}
158
+ onClick={() => setShowDescription(v => !v)}
159
+ aria-label={showDescription ? t.close : t.open}
160
+ >
161
+ <ChevronDown />
162
+ </IconButton>}
163
+ </div>
164
+ {showDescription && <Text className="description" colorScheme="light.700">{tool.description}</Text>}
165
+ </div>
166
+ </AnimatedHeight>
167
+ )
168
+ }
169
+
170
+ export const StepModal = ({ message, stepId: initialStepId, onClose }: Props) => {
171
+ const t = useToolsDictionary()
172
+ const [stepId, setStepId] = useState(initialStepId)
173
+ const entry = useChatEntry(message)
174
+ const stepIndex = useMemo(() => entry.steps?.findIndex(s => s.id === stepId) ?? -1, [entry, stepId])
175
+ const step = entry.steps?.[stepIndex]
176
+ useEffect(() => setStepId(initialStepId), [initialStepId])
177
+
178
+ const inputTools = step?.tools?.map(tool => (
179
+ <div className="tool" key={tool.id}>
180
+ <ToolHeader tool={tool} />
181
+ <Text appearance="microtext1" colorScheme="light.700">{t.input}:</Text>
182
+ {tool.input && <Code language="json" className="tool-input" showLineNumbers={false}>{tool.input}</Code>}
183
+ </div>
184
+ ))
185
+
186
+ const outputTools = step?.tools?.filter(tool => !!tool.output)?.map(tool => (
187
+ <div className="tool output" key={tool.id}>
188
+ <Badge appearance="square" palette="moss">{t.response}</Badge>
189
+ <ToolHeader tool={tool} />
190
+ <Text appearance="microtext1" colorScheme="light.700">{t.response}:</Text>
191
+ <Text appearance="microtext1">{tool.output}</Text>
192
+ </div>
193
+ ))
194
+
195
+ function changeStep(amount: number) {
196
+ const next = entry.steps?.[stepIndex + amount]?.id
197
+ if (next) setStepId(next)
198
+ }
199
+
200
+ const title = (
201
+ <Flex flex={1} justifyContent="space-between" alignItems="center">
202
+ <Text appearance="h6">{getTitle(t, step, stepIndex)}</Text>
203
+ <ExecutionBox>
204
+ <Text className="time" appearance="microtext1">
205
+ {step?.status === 'running' && t.running}
206
+ {step?.status === 'pending' && t.pending}
207
+ {step?.status === 'success' && `${t.executionTime}: ${step?.duration ? `${toPrecision(step?.duration)}s` : t.unknown}`}
208
+ </Text>
209
+ <div className="navigator">
210
+ {step?.type !== 'planning' && (
211
+ <IconButton size="xs" appearance="text" title={t.previousStep} aria-label={t.previousStep} onClick={() => changeStep(-1)}>
212
+ <ChevronLeft />
213
+ </IconButton>
214
+ )}
215
+ {step?.type === 'step' && <Text appearance="microtext1">{stepIndex}/{(entry.steps?.length ?? 0) - 2}</Text>}
216
+ {step?.type !== 'answer' && (
217
+ <IconButton size="xs" appearance="text" title={t.nextStep} aria-label={t.nextStep} onClick={() => changeStep(1)}>
218
+ <ChevronRight />
219
+ </IconButton>
220
+ )}
221
+ </div>
222
+ </ExecutionBox>
223
+ </Flex>
224
+ )
225
+
226
+ return (
227
+ <Modal open={!!step} onClose={onClose} title={title}>
228
+ {step?.type === 'answer' && <StyledSection className="restrict-image-size">
229
+ {entry.type === 'md' ? <Markdown>{entry.content}</Markdown> : <Text>{entry.content}</Text>}
230
+ </StyledSection>}
231
+ {step?.input && <StyledSection>
232
+ <Badge appearance="square" palette="blue">Prompt</Badge>
233
+ <Text>{step.input}</Text>
234
+ {inputTools}
235
+ </StyledSection>}
236
+ {(outputTools?.length || step?.output) && (
237
+ <StyledSection>
238
+ {outputTools}
239
+ {step?.output && !outputTools?.length && <>
240
+ <Badge appearance="square" palette="moss">{t.response}</Badge>
241
+ <Text>{step.output}</Text>
242
+ </>}
243
+ </StyledSection>
244
+ )}
245
+ </Modal>
246
+ )
247
+ }
@@ -0,0 +1,24 @@
1
+ /* eslint-disable import/no-default-export */
2
+
3
+ import { useMemo, useState } from 'react'
4
+ import { useWidget } from '../../context/hooks'
5
+ import { FlowChart } from './FlowChart'
6
+ import { StepModal } from './StepModal'
7
+
8
+ const ToolsPanel = ({ chatId, messageId }: { chatId: string, messageId: number }) => {
9
+ const [currentStepId, setCurrentStepId] = useState<string | undefined>()
10
+ const widget = useWidget()
11
+ const message = useMemo(
12
+ () => widget.chatTabs.getAll().find(c => c.id === chatId)?.getMessages().find(m => m.id === messageId),
13
+ [chatId, messageId],
14
+ )
15
+
16
+ return message ? (
17
+ <>
18
+ <FlowChart message={message} onClick={(step) => setCurrentStepId(step.id)} />
19
+ <StepModal message={message} stepId={currentStepId} onClose={() => setCurrentStepId(undefined)} />
20
+ </>
21
+ ) : null
22
+ }
23
+
24
+ export default ToolsPanel
@@ -0,0 +1,46 @@
1
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
2
+
3
+ export const dictionary = {
4
+ en: {
5
+ toolsDescription: 'A description for tools',
6
+ step: 'Step',
7
+ response: 'Response',
8
+ tools: 'Tools',
9
+ planning: 'Planning',
10
+ answer: 'Final answer',
11
+ executionTime: 'Time',
12
+ unknown: 'unknown',
13
+ running: 'Running',
14
+ pending: 'Pending',
15
+ nextStep: 'Next step',
16
+ previousStep: 'Previous step',
17
+ thoughtFor: 'Thought for $0 seconds',
18
+ open: 'Open',
19
+ close: 'Close',
20
+ input: 'Input',
21
+ },
22
+ pt: {
23
+ toolsDescription: 'Uma descrição para as Ferramentas',
24
+ step: 'Passo',
25
+ response: 'Resposta',
26
+ tools: 'Ferramentas',
27
+ planning: 'Planejamento',
28
+ answer: 'Resposta final',
29
+ executionTime: 'Tempo',
30
+ unknown: 'desconhecido',
31
+ running: 'Executando',
32
+ pending: 'Aguardando',
33
+ nextStep: 'Próximo passo',
34
+ previousStep: 'Passo anterior',
35
+ thoughtFor: 'Pensado por $0 segundos',
36
+ open: 'Open',
37
+ close: 'Close',
38
+ input: 'Entrada',
39
+ },
40
+ } satisfies Dictionary
41
+
42
+ export function useToolsDictionary() {
43
+ return useTranslate(dictionary)
44
+ }
45
+
46
+ export type ToolsDictionary = typeof dictionary['en']