@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
@@ -1,9 +1,10 @@
1
- import { aiClient, StackspotAPIError, StreamCanceledError } from '@stack-spot/portal-network'
1
+ import { aiClient, ChatResponseWithSteps, StackspotAPIError, StreamCanceledError } from '@stack-spot/portal-network'
2
2
  import { ChatResponse3 } from '@stack-spot/portal-network/api/ai'
3
3
  import { ChatEntry, KnowledgeSource, TextChatEntry } from '../state/ChatEntry'
4
4
  import { ChatState } from '../state/ChatState'
5
5
  import { LabeledWithImage } from '../state/types'
6
6
  import { buildConversationContext } from '../utils/chat'
7
+ import { treatHTMLInErrorMessage } from '../utils/error'
7
8
  import { genericSourcesToKnowledgeSources } from '../utils/knowledge-source'
8
9
 
9
10
  /**
@@ -15,7 +16,7 @@ import { genericSourcesToKnowledgeSources } from '../utils/knowledge-source'
15
16
  * @returns the TextChatEntry to build a ChatEntry.
16
17
  */
17
18
  function createEntryValueFromChatResponse(
18
- response: Partial<ChatResponse3>,
19
+ response: Partial<ChatResponseWithSteps>,
19
20
  knowledgeSources: KnowledgeSource[] | undefined,
20
21
  agent: LabeledWithImage | undefined,
21
22
  includeDate = false,
@@ -28,6 +29,7 @@ function createEntryValueFromChatResponse(
28
29
  knowledgeSources,
29
30
  agent: agent,
30
31
  updated: includeDate ? new Date().toISOString() : undefined,
32
+ steps: response.steps,
31
33
  }
32
34
  }
33
35
 
@@ -72,7 +74,10 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
72
74
  } else {
73
75
  botEntry.setValue({
74
76
  ...botEntry.getValue(),
75
- error: error instanceof StackspotAPIError ? error.translate() : (error.message ?? `${error}`),
77
+ error: treatHTMLInErrorMessage(
78
+ error instanceof StackspotAPIError ? error.translate() : (error.message ?? `${error}`),
79
+ error.status ?? 500,
80
+ ),
76
81
  })
77
82
  }
78
83
  }
@@ -0,0 +1,55 @@
1
+ /* eslint-disable react/display-name */
2
+ import { listToClass } from '@stack-spot/portal-theme'
3
+ import { forwardRef, useEffect, useRef, useState } from 'react'
4
+ import { styled } from 'styled-components'
5
+
6
+ interface Props {
7
+ children: React.ReactNode | React.ReactNode[],
8
+ durationMs?: number,
9
+ visible: boolean,
10
+ }
11
+
12
+ const Styled = styled.div<{ $durationMs: number }>`
13
+ opacity: 0;
14
+ transition: opacity ${({ $durationMs }) => $durationMs / 1000}s;
15
+ &.visible {
16
+ opacity: 1;
17
+ }
18
+ `
19
+
20
+ export const AnimatedOpacity = forwardRef<HTMLDivElement, Props>(({ children, visible, durationMs = 300 }, ref) => {
21
+ const [content, setContent] = useState(visible ? children : null)
22
+ const animation = useRef<number | undefined>()
23
+ const previousVisible = useRef<boolean | undefined>()
24
+
25
+ useEffect(() => {
26
+ const isFirstRender = previousVisible === undefined
27
+ const isChangingVisibility = visible !== previousVisible.current
28
+ previousVisible.current = visible
29
+ if (isFirstRender) return
30
+ if (isChangingVisibility) {
31
+ if (animation.current) {
32
+ window.clearTimeout(animation.current)
33
+ animation.current = undefined
34
+ return setContent(visible ? children : null)
35
+ }
36
+ if (visible) setContent(children)
37
+ animation.current = window.setTimeout(() => {
38
+ if (!visible) setContent(null)
39
+ animation.current = undefined
40
+ }, durationMs)
41
+ } else if (visible || !animation.current) {
42
+ setContent(visible ? children : null)
43
+ }
44
+
45
+ return () => {
46
+ window.clearTimeout(animation.current)
47
+ }
48
+ }, [visible, children])
49
+
50
+ return (
51
+ <Styled ref={ref} $durationMs={durationMs} className={listToClass(['fade-animation', visible && 'visible'])}>
52
+ {content}
53
+ </Styled>
54
+ )
55
+ })
@@ -29,6 +29,7 @@ export interface Props extends WithChildren {
29
29
  onNewFile?: CodeAction,
30
30
  onCopyCode?: CodeAction,
31
31
  language?: string,
32
+ showLineNumbers?: boolean,
32
33
  }
33
34
 
34
35
  const CodeBox = styled.code`
@@ -98,11 +99,12 @@ export const Code = ({
98
99
  language,
99
100
  children,
100
101
  showActionBar,
102
+ showLineNumbers = true,
101
103
  ...props
102
104
  }: Pick<CodeProps, 'className' | 'messageId'> & Props) => {
103
105
  const t = useTranslate(dictionary)
104
106
  const themeKind = useThemeKind()
105
- const [showLines, setShowLines] = useState(true)
107
+ const [showLines, setShowLines] = useState(showLineNumbers)
106
108
  const match = /language-(\w+)/.exec(className || '')
107
109
  const computedLanguage = language ?? (match ?? [])[1]?.toLowerCase() ?? 'txt'
108
110
  const content = String(children ?? '').replaceAll(/\n\t/g, '\n').trim()
@@ -124,7 +126,7 @@ export const Code = ({
124
126
  }
125
127
 
126
128
  if (children === undefined) return <></>
127
- if (!(/language-(\w+)/.exec(className || ''))) {
129
+ if (computedLanguage === 'txt') {
128
130
  return (
129
131
  <code {...props} className={className}>
130
132
  {children}
@@ -133,7 +135,7 @@ export const Code = ({
133
135
  }
134
136
 
135
137
  return (
136
- <CodeBox className={['code-box', themeKind].join(' ')}>
138
+ <CodeBox className={['code-box', themeKind, className].join(' ')}>
137
139
  {showActionBar && (
138
140
  <div className="action-bar" role="toolbar">
139
141
  <IconButton
@@ -0,0 +1,87 @@
1
+ import { Text } from '@citric/core'
2
+ import { Times } from '@citric/icons'
3
+ import { IconButton } from '@citric/ui'
4
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
5
+ import { useEffect, useRef, useState } from 'react'
6
+ import { createPortal } from 'react-dom'
7
+ import { AnimatedOpacity } from './AnimatedOpacity'
8
+
9
+ interface Props {
10
+ children: React.ReactNode | React.ReactNode[],
11
+ title: React.ReactNode,
12
+ open: boolean,
13
+ onClose?: () => void,
14
+ }
15
+
16
+ const ModalContent = ({ title, children, onClose }: Omit<Props, 'open'>) => {
17
+ const t = useTranslate(dictionary)
18
+ return (
19
+ <div className="chat-modal">
20
+ <header>
21
+ {typeof title === 'string' ? <Text appearance="h6">{title}</Text> : title}
22
+ <IconButton aria-label={t.close} title={t.close} appearance="circle" onClick={onClose}><Times /></IconButton>
23
+ </header>
24
+ <article>{children}</article>
25
+ </div>
26
+ )
27
+ }
28
+
29
+ export const Modal = ({ title, onClose, open, children }: Props) => {
30
+ const ref = useRef<HTMLDivElement>(null)
31
+ const [closestChat, setClosestChat] = useState<HTMLDivElement | undefined>()
32
+
33
+ useEffect(() => {
34
+ if (!closestChat) {
35
+ const chat = ref.current?.closest('.ai-chat-widget')
36
+ if (!chat) throw new Error('Could not find the chat widget (.ai-chat-widget) in the HTML tree.')
37
+ return setClosestChat(chat as HTMLDivElement)
38
+ }
39
+ if (!open) return
40
+ const chatWindow = closestChat?.querySelector('.chat-window')
41
+ const rightPanel = closestChat?.querySelector('.chat-right-panel')
42
+
43
+ function closeOnClickOutside(event: Event) {
44
+ const modal = closestChat?.querySelector('.chat-modal')
45
+ if (modal?.contains(event.target as Node)) return
46
+ onClose?.()
47
+ }
48
+
49
+ function onPressEsc(event: KeyboardEvent) {
50
+ if (event.key === 'Escape') onClose?.()
51
+ }
52
+
53
+ chatWindow?.setAttribute('inert', '')
54
+ rightPanel?.setAttribute('inert', '')
55
+ closestChat?.addEventListener('click', closeOnClickOutside)
56
+ document.addEventListener('keydown', onPressEsc)
57
+
58
+ return () => {
59
+ chatWindow?.removeAttribute('inert')
60
+ rightPanel?.removeAttribute('inert')
61
+ closestChat?.removeEventListener('click', closeOnClickOutside)
62
+ document.removeEventListener('keydown', onPressEsc)
63
+ }
64
+ }, [open])
65
+
66
+ return (
67
+ <div ref={ref}>
68
+ {closestChat && createPortal(
69
+ <AnimatedOpacity visible={open}>
70
+ <ModalContent title={title} onClose={onClose}>
71
+ {children}
72
+ </ModalContent>
73
+ </AnimatedOpacity>,
74
+ closestChat,
75
+ )}
76
+ </div>
77
+ )
78
+ }
79
+
80
+ const dictionary = {
81
+ en: {
82
+ close: 'Close',
83
+ },
84
+ pt: {
85
+ close: 'Fechar',
86
+ },
87
+ } satisfies Dictionary
package/src/layout.css CHANGED
@@ -129,9 +129,43 @@
129
129
  }
130
130
  }
131
131
 
132
+ .chat-modal {
133
+ position: absolute;
134
+ top: 90px;
135
+ bottom: 20px;
136
+ right: 20px;
137
+ width: 20%;
138
+ min-width: 400px;
139
+ border-radius: 6px;
140
+ background-color: var(--light-400);
141
+ border: 1px solid var(--light-600);
142
+ overflow: auto;
143
+
144
+ > header {
145
+ padding: 14px;
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: space-between;
149
+ gap: 6px;
150
+ }
151
+ }
152
+
132
153
  /* fixes placeholder description */
133
154
  .no-data-placeholder {
134
155
  > div > p {
135
156
  text-align: center;
136
157
  }
158
+ }
159
+
160
+ .input-phone .react-international-phone-country-selector-button {
161
+ background-color: var(--light-300);
162
+ border: 1px solid var(--primary-500);
163
+ border-right: none;
164
+ }
165
+
166
+ .input-phone.react-international-phone-input-container .react-international-phone-input {
167
+ background-color: var(--light-300);
168
+ border: 1px solid var(--primary-500);
169
+ border-left: none;
170
+ color: var(--light-contrastText);
137
171
  }
@@ -1,7 +1,13 @@
1
+ import { OneOfColorSchemes } from '@citric/core'
1
2
  import { ColorPaletteName } from '@stack-spot/portal-theme'
2
3
  import { pull } from 'lodash'
3
4
  import { LabeledWithImage } from './types'
4
5
 
6
+ export interface ActionDataClick {
7
+ name?: string,
8
+ value?: string[],
9
+ }
10
+
5
11
  export interface SerializableAction {
6
12
  /**
7
13
  * The text for the button or anchor.
@@ -14,7 +20,7 @@ export interface SerializableAction {
14
20
  /**
15
21
  * The URL if the action is a link. The content of the user message otherwise.
16
22
  */
17
- exec: string,
23
+ exec?: string,
18
24
  }
19
25
 
20
26
  export interface ChatAction extends SerializableAction {
@@ -22,6 +28,22 @@ export interface ChatAction extends SerializableAction {
22
28
  * @default primary
23
29
  */
24
30
  appearance?: 'primary' | 'secondary',
31
+ /**
32
+ * @default inverse
33
+ */
34
+ colorScheme?: OneOfColorSchemes,
35
+ /**
36
+ * @default false
37
+ */
38
+ hideWhenNotLast?: boolean,
39
+ /**
40
+ * @default false
41
+ */
42
+ disabled?: boolean,
43
+ /**
44
+ * @default button
45
+ */
46
+ buttonType?: 'submit' | 'button',
25
47
  }
26
48
 
27
49
  export interface KnowledgeSource {
@@ -31,11 +53,33 @@ export interface KnowledgeSource {
31
53
  documentId: string,
32
54
  }
33
55
 
56
+ export interface AgentTool {
57
+ id: string,
58
+ name: string,
59
+ description?: string,
60
+ image?: string,
61
+ duration?: number,
62
+ input?: string,
63
+ output?: string,
64
+ }
65
+
66
+ export interface ChatEntryStep {
67
+ id: string,
68
+ type: 'planning' | 'step' | 'answer',
69
+ input?: string,
70
+ output?: string,
71
+ status: 'pending' | 'running' | 'success' | 'error',
72
+ duration?: number,
73
+ tools?: AgentTool[],
74
+ }
75
+
34
76
  export interface TextChatEntry {
35
77
  /**
36
- * "text" for simple unformatted paragraphs. "md" for markdown.
78
+ * "text" for simple unformatted paragraphs. "md" for markdown.
79
+ * "input-text" for text inputs, 'input-radio' and "input-checkbox" for radio and checkbox inputs,
80
+ * "input-phone" for phone formatted inputs and "button-list" for a list of buttons in which only one is selected
37
81
  */
38
- type: 'text' | 'md',
82
+ type: 'text' | 'md' | 'input-text' | 'input-radio' | 'button-list' | 'input-checkbox' | 'input-phone',
39
83
  /**
40
84
  * If the message was typed by the AI agent (bot), the user or the system (not currently in use).
41
85
  */
@@ -48,6 +92,10 @@ export interface TextChatEntry {
48
92
  * The content of the message.
49
93
  */
50
94
  content: string,
95
+ /**
96
+ * A content that is not shown to the user, it is used only to share data.
97
+ */
98
+ hiddenContent?: string[],
51
99
  /**
52
100
  * The knowledge sources used to create the message.
53
101
  */
@@ -76,6 +124,30 @@ export interface TextChatEntry {
76
124
  * Whether or not to show this chat entry as a card.
77
125
  */
78
126
  card?: boolean,
127
+ /**
128
+ * This entry may contain steps. If so, specify them in this array.
129
+ */
130
+ steps?: ChatEntryStep[],
131
+ /*
132
+ * Options for radio or button type.
133
+ */
134
+ options?: { color?: OneOfColorSchemes, label: string, value?: string}[],
135
+ /**
136
+ * Name to be used in input type fields.
137
+ */
138
+ name?: string,
139
+ /**
140
+ * Whether or not a input field is required.
141
+ */
142
+ required?: boolean,
143
+ /**
144
+ * The validations of input fields.
145
+ */
146
+ validations?: {
147
+ minLength?: number,
148
+ maxLength?: number,
149
+ pattern?: string,
150
+ },
79
151
  }
80
152
 
81
153
  type ChatEntryListener = (value: TextChatEntry) => void
@@ -104,13 +176,16 @@ export class ChatEntry {
104
176
  * Utility function to create a user entry.
105
177
  * @param content the message's content.
106
178
  * @param isMd whether or not this should be rendered as markdown.
179
+ * @param hiddenContent the message's content.
107
180
  * @returns a new ChatEntry.
108
181
  */
109
- static createUserEntry(content: string, isMd = false) {
182
+ static createUserEntry(content: string, isMd = false, fieldName?: string, hiddenContent?: string[]) {
110
183
  return new ChatEntry({
111
184
  agentType: 'user',
112
185
  type: isMd ? 'md' : 'text',
113
186
  content,
187
+ name: fieldName,
188
+ hiddenContent,
114
189
  updated: new Date().toISOString(),
115
190
  })
116
191
  }
@@ -13,11 +13,15 @@ export interface WidgetProperties {
13
13
  /**
14
14
  * Current content of the right panel. Undefined for closed right panel.
15
15
  */
16
- panel?: 'stack' | 'workspace' | 'agent' | 'ks' | 'editor' | 'history' | 'ks-details',
16
+ panel?: 'stack' | 'workspace' | 'agent' | 'ks' | 'editor' | 'history' | 'ks-details' | 'tools',
17
17
  /**
18
18
  * KS to use when the right panel "ks-details" is open.
19
19
  */
20
20
  currentKSInPanel?: { name: string, slug: string, score: number, documentId: string },
21
+ /**
22
+ * The message to show in the tools panel.
23
+ */
24
+ currentMessageInToolsPanel?: { chatId: string, messageId: number },
21
25
  /**
22
26
  * Whether or not the widget is in its minimized version.
23
27
  */
@@ -82,7 +86,7 @@ export class WidgetState extends ObservableState<WidgetProperties> {
82
86
  quickCommandQuestionsInterceptor,
83
87
  createQuickCommandInterceptor(this, () => loader.__getMonacoInstance()?.editor),
84
88
  sendMessageInterceptor,
85
- ]
89
+ ],
86
90
  this.createChat()
87
91
  }
88
92
 
@@ -0,0 +1,56 @@
1
+ import { getLanguage } from '@stack-spot/portal-translate'
2
+
3
+ const httpErrors: Record<'en' | 'pt', Record<number, string>> = {
4
+ en: {
5
+ 400: 'Bad Request',
6
+ 401: 'Unauthorized',
7
+ 403: 'Forbidden',
8
+ 404: 'Not Found',
9
+ 405: 'Method Not Allowed',
10
+ 406: 'Not Acceptable',
11
+ 408: 'Request Timeout',
12
+ 409: 'Conflict',
13
+ 410: 'Gone',
14
+ 411: 'Length Required',
15
+ 413: 'Payload Too Large',
16
+ 414: 'URI Too Long',
17
+ 415: 'Unsupported Media Type',
18
+ 429: 'Too Many Requests',
19
+ 500: 'Internal Server Error',
20
+ 501: 'Not Implemented',
21
+ 502: 'Bad Gateway',
22
+ 503: 'Service Unavailable',
23
+ 504: 'Gateway Timeout',
24
+ 505: 'HTTP Version Not Supported',
25
+ },
26
+ pt: {
27
+ 400: 'Requisição Inválida',
28
+ 401: 'Não Autorizado',
29
+ 403: 'Proibido',
30
+ 404: 'Não Encontrado',
31
+ 405: 'Método Não Permitido',
32
+ 406: 'Não Aceitável',
33
+ 408: 'Tempo de Requisição Esgotado',
34
+ 409: 'Conflito',
35
+ 410: 'Indisponível',
36
+ 411: 'Comprimento Necessário',
37
+ 413: 'Carga Muito Grande',
38
+ 414: 'URI Muito Longa',
39
+ 415: 'Tipo de Mídia Não Suportado',
40
+ 429: 'Muitas Requisições',
41
+ 500: 'Erro Interno do Servidor',
42
+ 501: 'Não Implementado',
43
+ 502: 'Gateway Inválido',
44
+ 503: 'Serviço Indisponível',
45
+ 504: 'Tempo de Gateway Esgotado',
46
+ 505: 'Versão HTTP Não Suportada',
47
+ },
48
+ }
49
+
50
+ function getGenericErrorBasedOnStatus(status: number) {
51
+ return httpErrors[getLanguage()][status] ?? `Unknown error. Status code: ${status}.`
52
+ }
53
+
54
+ export function treatHTMLInErrorMessage(text: string, status: number) {
55
+ return (!text.includes('<html>')) ? text : getGenericErrorBasedOnStatus(status)
56
+ }
@@ -1,9 +1,11 @@
1
- import { Button, IconBox, Text } from '@citric/core'
1
+ import { Box, Button, Checkbox, Flex, IconBox, Input, Label, Radio, Text } from '@citric/core'
2
2
  import { Copy, Dislike, DislikeFill, Like, LikeFill, TimesCircle } from '@citric/icons'
3
3
  import { Avatar, Badge, IconButton } from '@citric/ui'
4
4
  import { listToClass } from '@stack-spot/portal-theme'
5
5
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
6
- import { useCallback, useMemo, useRef, useState } from 'react'
6
+ import { Dispatch, useCallback, useMemo, useRef, useState } from 'react'
7
+ import { PhoneInput } from 'react-international-phone'
8
+ import 'react-international-phone/style.css'
7
9
  import { Markdown } from '../../components/Markdown'
8
10
  import { useChatEntry, useCurrentChat, useWidget } from '../../context/hooks'
9
11
  import { ChatEntry, SerializableAction, TextChatEntry } from '../../state/ChatEntry'
@@ -11,6 +13,7 @@ import { useDateFormatter } from '../../utils/date'
11
13
  import { AgentInfo } from './AgentInfo'
12
14
  import { useChatScrollToBottomEffect } from './chat-scroll'
13
15
  import { onCopyAll, onCopyCode, onLikeOrDislike } from './events'
16
+ import { StepsList } from './StepsList'
14
17
 
15
18
  interface Props {
16
19
  /**
@@ -28,12 +31,98 @@ interface Props {
28
31
  isLast: boolean,
29
32
  }
30
33
 
34
+ interface RenderInputsEntryProps {
35
+ isLast: boolean,
36
+ entry: TextChatEntry,
37
+ value: string[],
38
+ setValue: Dispatch<React.SetStateAction<string[]>>,
39
+ labels: string[],
40
+ setLabels: Dispatch<React.SetStateAction<string[]>>,
41
+ }
42
+
43
+ const RenderInputsEntry = ({ isLast, entry, value, setValue, setLabels }: RenderInputsEntryProps) => {
44
+ const chat = useCurrentChat()
45
+
46
+ const renderInputs = () => {
47
+ if (entry.type === 'input-text') {
48
+ return <Input name={entry.name} {...entry.validations} onChange={(data) => setValue([data.target.value])} required={entry.required} />
49
+ }
50
+
51
+ if (entry.type === 'input-radio') {
52
+ return <Flex>
53
+ {entry.options?.map((option) => (<Box w={6} key={option.value}>
54
+ <Label htmlFor={option.value} colorScheme="light.contrastText">
55
+ <Radio name={entry.name} id={option.value} onChange={(data) => {
56
+ if (data.target.checked) {
57
+ setValue([option.label])
58
+ option.value && setLabels([option.value])
59
+ } else {
60
+ setValue([])
61
+ }
62
+ }} />
63
+ <Text ml={3}>{option.label}</Text>
64
+ </Label>
65
+ </Box>))}
66
+ </Flex>
67
+ }
68
+
69
+ if (entry.type === 'button-list') {
70
+ return <Flex>
71
+ {entry.options?.map((item) => (<Button key={item.label} colorScheme={item.color}
72
+ onClick={() => {
73
+ item.value && chat.pushMessage(
74
+ ChatEntry.createUserEntry(item.value, false, entry.name, item?.label ? [item?.label] : undefined),
75
+ )
76
+ }}>
77
+ {item.label}
78
+ </Button>))
79
+ }</Flex>
80
+ }
81
+
82
+ if (entry.type === 'input-checkbox') {
83
+ return <Flex>
84
+ {entry.options?.map((option) => (
85
+ <Flex w={6} key={option.label}>
86
+ <Checkbox name={entry.name} key={option.label} onChange={(data) => {
87
+ if (data.target.checked) {
88
+ setValue([...value, option.label])
89
+ option.value && setLabels([option.value])
90
+ } else {
91
+ const newValue = value.filter(((item) => item !== option.label))
92
+ setValue([...newValue])
93
+ }
94
+ }}/>
95
+ <Text ml={3}>{option.label}</Text>
96
+ </Flex>))}
97
+ </Flex>
98
+ }
99
+
100
+ if (entry.type === 'input-phone') {
101
+ return (<PhoneInput
102
+ defaultCountry="br"
103
+ value={value[0]}
104
+ onChange={(phone) => setValue([phone])}
105
+ className="input-phone"
106
+ placeholder="11961234567"
107
+ />)
108
+ }
109
+ return <p className="plain-text">{entry.content}</p>
110
+ }
111
+
112
+ return <Flex flexDirection="column">
113
+ <Text appearance="body2" mb={4}>{entry.content}</Text>
114
+ {isLast && renderInputs()}
115
+ </Flex>
116
+ }
117
+
31
118
  /**
32
119
  * Renders a message (ChatEntry) in the chat.
33
120
  */
34
121
  export const ChatMessage = ({ message, username, isLast }: Props) => {
35
122
  const t = useTranslate(dictionary)
36
123
  const [liked, setLiked] = useState<boolean | undefined>()
124
+ const [value, setValue] = useState<string[]>([])
125
+ const [labels, setLabels] = useState<string[]>([])
37
126
  const entry = useChatEntry(message)
38
127
  const dateFormatter = useDateFormatter()
39
128
  const userInfo = entry.agentType === 'user' ? <Avatar size="xs">{username}</Avatar> : <AgentInfo agent={entry.agent} />
@@ -50,13 +139,17 @@ export const ChatMessage = ({ message, username, isLast }: Props) => {
50
139
  widget.set('panel', 'ks-details')
51
140
  }, [])
52
141
 
53
- const runAction = useCallback((action: SerializableAction) => {
142
+ const runAction = (action: SerializableAction) => {
54
143
  if (action.type === 'link') {
55
144
  window.open(action.exec, '_blank')
56
145
  } else {
57
- chat.pushMessage(ChatEntry.createUserEntry(action.exec))
146
+ if (action.exec) {
147
+ chat.pushMessage(ChatEntry.createUserEntry(action.exec))
148
+ } else {
149
+ value && chat.pushMessage(ChatEntry.createUserEntry(value.toString(), false, entry.name, labels))
150
+ }
58
151
  }
59
- }, [])
152
+ }
60
153
 
61
154
  const { like, dislike } = useMemo(() => {
62
155
  async function feedback(like: boolean) {
@@ -82,35 +175,45 @@ export const ChatMessage = ({ message, username, isLast }: Props) => {
82
175
  }
83
176
  }
84
177
 
85
- return (entry.content || entry.error) && (
178
+ const renderContent = () => {
179
+ if (entry.type === 'md') {
180
+ return <Markdown onCopyCode={(code) => onCopyCode(code, entry.messageId ?? '', chat)}>{entry.content}</Markdown>
181
+ }
182
+ if (entry.type === 'text') {
183
+ return <p className="plain-text">{entry.content}</p>
184
+ }
185
+
186
+ return <RenderInputsEntry entry={entry} isLast={isLast} value={value} setValue={setValue} setLabels={setLabels} labels={labels} />
187
+ }
188
+
189
+ return (entry.content || entry.error || !!entry.steps?.length) && (
86
190
  <li className={entry.agentType} ref={ref}>
87
191
  <div className="chat-message" ref={chatRef} onKeyDown={handleKeyDown} tabIndex={0}>
88
192
  <div className="user-info">{userInfo}</div>
89
- {entry.content && <div className={listToClass(['message-content', entry.card && 'card'])}>
90
- {entry.badges?.length && <div className="badges">
193
+ {(entry.content || entry.steps) && <div className={listToClass(['message-content', entry.card && 'card'])}>
194
+ {!!entry.badges?.length && <div className="badges">
91
195
  {entry.badges.map((b, index) => <Badge key={index} palette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
92
196
  </div>}
93
- {entry.type === 'md'
94
- ? <Markdown onCopyCode={(code) => onCopyCode(code, entry.messageId ?? '', chat)}>{entry.content}</Markdown>
95
- : <p className="plain-text">{entry.content}</p>
96
- }
197
+ {renderContent()}
97
198
  {entry.actions?.length && (
98
199
  <div className="actions">
99
200
  {entry.actions.map(
100
- (a, index) => (
101
- <Button
201
+ (a, index) => (<>
202
+ {(!a.hideWhenNotLast || isLast) && <Button
102
203
  key={index}
103
204
  appearance={a.appearance === 'primary' ? 'contained' : 'outlined'}
104
- colorScheme="inverse"
205
+ colorScheme={a.colorScheme ? a.colorScheme : 'inverse'}
105
206
  onClick={() => runAction(a)}
106
- disabled={!isLast}
207
+ disabled={(entry.required && !value.length) ?? !isLast}
208
+ type={a.buttonType ?? 'button'}
107
209
  >
108
210
  {a.title}
109
- </Button>
110
- ),
211
+ </Button>}
212
+ </>),
111
213
  )}
112
214
  </div>
113
215
  )}
216
+ {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
114
217
  </div>}
115
218
  </div>
116
219
  {entry.error && (