@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.
- package/CHANGELOG.md +14 -0
- package/dist/StackspotAIWidget.d.ts +5 -1
- package/dist/StackspotAIWidget.d.ts.map +1 -1
- package/dist/StackspotAIWidget.js +6 -5
- package/dist/StackspotAIWidget.js.map +1 -1
- package/dist/app-metadata.json +31 -19
- package/dist/chat-interceptors/send-message.d.ts.map +1 -1
- package/dist/chat-interceptors/send-message.js +3 -1
- package/dist/chat-interceptors/send-message.js.map +1 -1
- package/dist/components/Accordion.d.ts.map +1 -1
- package/dist/components/AnimatedOpacity.d.ts +8 -0
- package/dist/components/AnimatedOpacity.d.ts.map +1 -0
- package/dist/components/AnimatedOpacity.js +46 -0
- package/dist/components/AnimatedOpacity.js.map +1 -0
- package/dist/components/Code.d.ts +2 -1
- package/dist/components/Code.d.ts.map +1 -1
- package/dist/components/Code.js +4 -4
- package/dist/components/Code.js.map +1 -1
- package/dist/components/FadingOverflow.d.ts.map +1 -1
- package/dist/components/FallbackBoundary/index.d.ts.map +1 -1
- package/dist/components/IconInput.d.ts.map +1 -1
- package/dist/components/Markdown.d.ts.map +1 -1
- package/dist/components/Modal.d.ts +9 -0
- package/dist/components/Modal.d.ts.map +1 -0
- package/dist/components/Modal.js +58 -0
- package/dist/components/Modal.js.map +1 -0
- package/dist/components/ProgressBar.d.ts.map +1 -1
- package/dist/components/QuickStartButton.d.ts.map +1 -1
- package/dist/components/RightPanelForm.d.ts.map +1 -1
- package/dist/components/RightPanelTabs.d.ts.map +1 -1
- package/dist/components/Selector/index.d.ts.map +1 -1
- package/dist/components/Tooltip/context.d.ts.map +1 -1
- package/dist/layout.css +34 -0
- package/dist/right-panel/DefaultPanel.d.ts.map +1 -1
- package/dist/right-panel/RightPanelProvider.d.ts.map +1 -1
- package/dist/state/ChatEntry.d.ts +74 -3
- package/dist/state/ChatEntry.d.ts.map +1 -1
- package/dist/state/ChatEntry.js +4 -1
- package/dist/state/ChatEntry.js.map +1 -1
- package/dist/state/WidgetState.d.ts +8 -1
- package/dist/state/WidgetState.d.ts.map +1 -1
- package/dist/state/WidgetState.js +2 -2
- package/dist/state/WidgetState.js.map +1 -1
- package/dist/utils/error.d.ts +2 -0
- package/dist/utils/error.d.ts.map +1 -0
- package/dist/utils/error.js +54 -0
- package/dist/utils/error.js.map +1 -0
- package/dist/views/Agents/AgentDescription.d.ts.map +1 -1
- package/dist/views/Agents/AgentsTab.d.ts.map +1 -1
- package/dist/views/Chat/AgentInfo.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.d.ts +2 -1
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +65 -7
- package/dist/views/Chat/ChatMessage.js.map +1 -1
- package/dist/views/Chat/ChatMessages.d.ts.map +1 -1
- package/dist/views/Chat/StepsList.d.ts +9 -0
- package/dist/views/Chat/StepsList.d.ts.map +1 -0
- package/dist/views/Chat/StepsList.js +51 -0
- package/dist/views/Chat/StepsList.js.map +1 -0
- package/dist/views/Chat/index.d.ts.map +1 -1
- package/dist/views/Chat/styled.d.ts +3 -1
- package/dist/views/Chat/styled.d.ts.map +1 -1
- package/dist/views/Chat/styled.js +56 -0
- package/dist/views/Chat/styled.js.map +1 -1
- package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -1
- package/dist/views/Home/BuiltInAgent.d.ts.map +1 -1
- package/dist/views/Home/index.d.ts.map +1 -1
- package/dist/views/MessageInput/AgentSelector.d.ts.map +1 -1
- package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
- package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -1
- package/dist/views/MinimizedHeader.d.ts.map +1 -1
- package/dist/views/Stacks.js +1 -0
- package/dist/views/Stacks.js.map +1 -1
- package/dist/views/Tools/FlowChart/HandleGroup.d.ts +7 -0
- package/dist/views/Tools/FlowChart/HandleGroup.d.ts.map +1 -0
- package/dist/views/Tools/FlowChart/HandleGroup.js +4 -0
- package/dist/views/Tools/FlowChart/HandleGroup.js.map +1 -0
- package/dist/views/Tools/FlowChart/NodeStep.d.ts +7 -0
- package/dist/views/Tools/FlowChart/NodeStep.d.ts.map +1 -0
- package/dist/views/Tools/FlowChart/NodeStep.js +15 -0
- package/dist/views/Tools/FlowChart/NodeStep.js.map +1 -0
- package/dist/views/Tools/FlowChart/index.d.ts +9 -0
- package/dist/views/Tools/FlowChart/index.d.ts.map +1 -0
- package/dist/views/Tools/FlowChart/index.js +52 -0
- package/dist/views/Tools/FlowChart/index.js.map +1 -0
- package/dist/views/Tools/FlowChart/layout.d.ts +17 -0
- package/dist/views/Tools/FlowChart/layout.d.ts.map +1 -0
- package/dist/views/Tools/FlowChart/layout.js +40 -0
- package/dist/views/Tools/FlowChart/layout.js.map +1 -0
- package/dist/views/Tools/FlowChart/styled.d.ts +15 -0
- package/dist/views/Tools/FlowChart/styled.d.ts.map +1 -0
- package/dist/views/Tools/FlowChart/styled.js +181 -0
- package/dist/views/Tools/FlowChart/styled.js.map +1 -0
- package/dist/views/Tools/FlowChart/types.d.ts +13 -0
- package/dist/views/Tools/FlowChart/types.d.ts.map +1 -0
- package/dist/views/Tools/FlowChart/types.js +2 -0
- package/dist/views/Tools/FlowChart/types.js.map +1 -0
- package/dist/views/Tools/StepModal.d.ts +9 -0
- package/dist/views/Tools/StepModal.d.ts.map +1 -0
- package/dist/views/Tools/StepModal.js +156 -0
- package/dist/views/Tools/StepModal.js.map +1 -0
- package/dist/views/Tools/ToolsPanel.d.ts +6 -0
- package/dist/views/Tools/ToolsPanel.d.ts.map +1 -0
- package/dist/views/Tools/ToolsPanel.js +14 -0
- package/dist/views/Tools/ToolsPanel.js.map +1 -0
- package/dist/views/Tools/dictionary.d.ts +41 -0
- package/dist/views/Tools/dictionary.d.ts.map +1 -0
- package/dist/views/Tools/dictionary.js +43 -0
- package/dist/views/Tools/dictionary.js.map +1 -0
- package/dist/views/Tools/index.d.ts +5 -0
- package/dist/views/Tools/index.d.ts.map +1 -0
- package/dist/views/Tools/index.js +31 -0
- package/dist/views/Tools/index.js.map +1 -0
- package/dist/views/Tools/utils.d.ts +6 -0
- package/dist/views/Tools/utils.d.ts.map +1 -0
- package/dist/views/Tools/utils.js +32 -0
- package/dist/views/Tools/utils.js.map +1 -0
- package/package.json +9 -6
- package/src/StackspotAIWidget.tsx +13 -4
- package/src/app-metadata.json +31 -19
- package/src/chat-interceptors/send-message.ts +8 -3
- package/src/components/AnimatedOpacity.tsx +55 -0
- package/src/components/Code.tsx +5 -3
- package/src/components/Modal.tsx +87 -0
- package/src/layout.css +34 -0
- package/src/state/ChatEntry.ts +79 -4
- package/src/state/WidgetState.ts +6 -2
- package/src/utils/error.ts +56 -0
- package/src/views/Chat/ChatMessage.tsx +121 -18
- package/src/views/Chat/StepsList.tsx +97 -0
- package/src/views/Chat/styled.ts +62 -1
- package/src/views/Stacks.tsx +1 -0
- package/src/views/Tools/FlowChart/HandleGroup.tsx +12 -0
- package/src/views/Tools/FlowChart/NodeStep.tsx +57 -0
- package/src/views/Tools/FlowChart/index.tsx +71 -0
- package/src/views/Tools/FlowChart/layout.ts +49 -0
- package/src/views/Tools/FlowChart/styled.ts +182 -0
- package/src/views/Tools/FlowChart/types.ts +14 -0
- package/src/views/Tools/StepModal.tsx +247 -0
- package/src/views/Tools/ToolsPanel.tsx +24 -0
- package/src/views/Tools/dictionary.ts +46 -0
- package/src/views/Tools/index.tsx +37 -0
- 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<
|
|
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:
|
|
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
|
+
})
|
package/src/components/Code.tsx
CHANGED
|
@@ -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(
|
|
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 (
|
|
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
|
}
|
package/src/state/ChatEntry.ts
CHANGED
|
@@ -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
|
|
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
|
}
|
package/src/state/WidgetState.ts
CHANGED
|
@@ -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 =
|
|
142
|
+
const runAction = (action: SerializableAction) => {
|
|
54
143
|
if (action.type === 'link') {
|
|
55
144
|
window.open(action.exec, '_blank')
|
|
56
145
|
} else {
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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=
|
|
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 && (
|