@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,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']
@@ -0,0 +1,37 @@
1
+ import { Flex } from '@citric/core'
2
+ import { LoadingCircular } from '@citric/ui'
3
+ import { lazy, Suspense, useEffect } from 'react'
4
+ import { useWidget, useWidgetState } from '../../context/hooks'
5
+ import { useRightPanel } from '../../right-panel/hooks'
6
+ import { useToolsDictionary } from './dictionary'
7
+
8
+ const LazyToolsPanel = lazy(() => import('./ToolsPanel'))
9
+
10
+ /**
11
+ * Renders the Stack selection form in the Right Panel if this is the panel that is currently opened.
12
+ */
13
+ export const Tools = () => {
14
+ const t = useToolsDictionary()
15
+ const panel = useWidgetState('panel')
16
+ const message = useWidgetState('currentMessageInToolsPanel')
17
+ const { open } = useRightPanel()
18
+ const widget = useWidget()
19
+
20
+ useEffect(() => {
21
+ if (panel === 'tools' && message) open(
22
+ <Suspense fallback={<Flex alignItems="center" justifyContent="center" flex={1}><LoadingCircular /></Flex>}>
23
+ <LazyToolsPanel key={message.messageId} chatId={message.chatId} messageId={message.messageId} />
24
+ </Suspense>,
25
+ {
26
+ title: t.tools,
27
+ description: t.toolsDescription,
28
+ onClose: () => {
29
+ widget.set('panel', undefined)
30
+ widget.set('currentMessageInToolsPanel', undefined)
31
+ },
32
+ },
33
+ )
34
+ }, [panel, t, message])
35
+
36
+ return null
37
+ }
@@ -0,0 +1,34 @@
1
+ import { IconBox } from '@citric/core'
2
+ import { CheckCircleFill, ListUnordered, PlayFill, TimesCircleFill } from '@citric/icons'
3
+ import { LoadingCircular } from '@citric/ui'
4
+ import { ChatEntryStep } from '../../state/ChatEntry'
5
+
6
+ export function getStatusIcon(status: ChatEntryStep['status']) {
7
+ switch (status) {
8
+ case 'success': return <IconBox colorIcon="success.500"><CheckCircleFill /></IconBox>
9
+ case 'error': return <IconBox colorIcon="danger.500"><TimesCircleFill /></IconBox>
10
+ case 'running': return <LoadingCircular colorScheme="inverse" size="xs" />
11
+ default: return null
12
+ }
13
+ }
14
+
15
+ export function getTypeIcon(type: ChatEntryStep['type']) {
16
+ switch (type) {
17
+ case 'planning': return <ListUnordered />
18
+ default: return <PlayFill />
19
+ }
20
+ }
21
+
22
+ export function getTitle(translation: Record<'planning' | 'step' | 'answer', string>, step: ChatEntryStep | undefined, index: number) {
23
+ if (!step) return ''
24
+ switch (step.type) {
25
+ case 'planning': return translation.planning
26
+ case 'step': return `${translation.step} ${index}`
27
+ case 'answer': return translation.answer
28
+ }
29
+ }
30
+
31
+ export function toPrecision(n: number, precisionDigits = 1) {
32
+ const factor = Math.pow(10, precisionDigits)
33
+ return Math.round(n * factor) / factor
34
+ }