@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.
- package/CHANGELOG.md +7 -0
- package/dist/StackspotAIWidget.d.ts.map +1 -1
- package/dist/StackspotAIWidget.js +2 -1
- package/dist/StackspotAIWidget.js.map +1 -1
- package/dist/app-metadata.json +11 -3
- 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/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/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/layout.css +21 -0
- package/dist/state/ChatEntry.d.ts +21 -2
- package/dist/state/ChatEntry.d.ts.map +1 -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.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/Chat/ChatMessage.d.ts +1 -1
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +2 -1
- package/dist/views/Chat/ChatMessage.js.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/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/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 +5 -3
- package/src/StackspotAIWidget.tsx +2 -0
- package/src/app-metadata.json +11 -3
- 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 +21 -0
- package/src/state/ChatEntry.ts +25 -1
- package/src/state/WidgetState.ts +5 -1
- package/src/utils/error.ts +56 -0
- package/src/views/Chat/ChatMessage.tsx +5 -3
- 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
|
@@ -17,6 +17,7 @@ import { KSDocument } from './views/KSDocument'
|
|
|
17
17
|
import { MessageInput } from './views/MessageInput'
|
|
18
18
|
import { MinimizedHeader } from './views/MinimizedHeader'
|
|
19
19
|
import { Stacks } from './views/Stacks'
|
|
20
|
+
import { Tools } from './views/Tools'
|
|
20
21
|
import { Workspaces } from './views/Workspaces'
|
|
21
22
|
|
|
22
23
|
export interface AIWidgetProps extends WithStyle {
|
|
@@ -75,6 +76,7 @@ export const StackspotAIWidget = ({ username, minimizedActions = {}, children, c
|
|
|
75
76
|
<Agents />
|
|
76
77
|
<Editor />
|
|
77
78
|
<ChatHistory />
|
|
79
|
+
<Tools />
|
|
78
80
|
<div className="chat-right-panel" ref={rightPanelRef}><RightPanel /></div>
|
|
79
81
|
</div>
|
|
80
82
|
</RightPanelProvider>
|
package/src/app-metadata.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stack-spot/ai-chat-widget",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"date": "Tue Mar 25 2025
|
|
3
|
+
"version": "1.10.0",
|
|
4
|
+
"date": "Tue Mar 25 2025 17:02:17 GMT+0000 (Coordinated Universal Time)",
|
|
5
5
|
"dependencies": [
|
|
6
6
|
{
|
|
7
7
|
"name": "@stack-spot/app-metadata",
|
|
@@ -99,6 +99,10 @@
|
|
|
99
99
|
"name": "@citric/ui",
|
|
100
100
|
"version": "6.7.0(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.9.0(react@18.2.0))(lodash@4.17.21)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
|
|
101
101
|
},
|
|
102
|
+
{
|
|
103
|
+
"name": "@dagrejs/dagre",
|
|
104
|
+
"version": "1.1.4"
|
|
105
|
+
},
|
|
102
106
|
{
|
|
103
107
|
"name": "@monaco-editor/react",
|
|
104
108
|
"version": "4.7.0(monaco-editor@0.52.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
|
|
@@ -109,7 +113,7 @@
|
|
|
109
113
|
},
|
|
110
114
|
{
|
|
111
115
|
"name": "@stack-spot/portal-network",
|
|
112
|
-
"version": "0.
|
|
116
|
+
"version": "0.102.0(@stack-spot/auth@5.3.3)(@stack-spot/opa@2.6.1(@stack-spot/auth@5.3.3)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@stack-spot/portal-translate@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@tanstack/react-query@5.69.0(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
|
|
113
117
|
},
|
|
114
118
|
{
|
|
115
119
|
"name": "@stack-spot/portal-theme",
|
|
@@ -119,6 +123,10 @@
|
|
|
119
123
|
"name": "@stack-spot/portal-translate",
|
|
120
124
|
"version": "1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
|
|
121
125
|
},
|
|
126
|
+
{
|
|
127
|
+
"name": "@xyflow/react",
|
|
128
|
+
"version": "12.4.4(@types/react@18.3.20)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
|
|
129
|
+
},
|
|
122
130
|
{
|
|
123
131
|
"name": "lodash",
|
|
124
132
|
"version": "4.17.21"
|
|
@@ -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,6 +129,27 @@
|
|
|
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 {
|
package/src/state/ChatEntry.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { OneOfColorSchemes } from '@citric/core'
|
|
1
2
|
import { ColorPaletteName } from '@stack-spot/portal-theme'
|
|
2
3
|
import { pull } from 'lodash'
|
|
3
|
-
import { OneOfColorSchemes } from '@citric/core'
|
|
4
4
|
import { LabeledWithImage } from './types'
|
|
5
5
|
|
|
6
6
|
export interface ActionDataClick {
|
|
@@ -53,6 +53,26 @@ export interface KnowledgeSource {
|
|
|
53
53
|
documentId: string,
|
|
54
54
|
}
|
|
55
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
|
+
|
|
56
76
|
export interface TextChatEntry {
|
|
57
77
|
/**
|
|
58
78
|
* "text" for simple unformatted paragraphs. "md" for markdown.
|
|
@@ -105,6 +125,10 @@ export interface TextChatEntry {
|
|
|
105
125
|
*/
|
|
106
126
|
card?: boolean,
|
|
107
127
|
/**
|
|
128
|
+
* This entry may contain steps. If so, specify them in this array.
|
|
129
|
+
*/
|
|
130
|
+
steps?: ChatEntryStep[],
|
|
131
|
+
/*
|
|
108
132
|
* Options for radio or button type.
|
|
109
133
|
*/
|
|
110
134
|
options?: { color?: OneOfColorSchemes, label: string, value?: string}[],
|
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
|
*/
|
|
@@ -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
|
+
}
|
|
@@ -13,6 +13,7 @@ import { useDateFormatter } from '../../utils/date'
|
|
|
13
13
|
import { AgentInfo } from './AgentInfo'
|
|
14
14
|
import { useChatScrollToBottomEffect } from './chat-scroll'
|
|
15
15
|
import { onCopyAll, onCopyCode, onLikeOrDislike } from './events'
|
|
16
|
+
import { StepsList } from './StepsList'
|
|
16
17
|
|
|
17
18
|
interface Props {
|
|
18
19
|
/**
|
|
@@ -185,12 +186,12 @@ export const ChatMessage = ({ message, username, isLast }: Props) => {
|
|
|
185
186
|
return <RenderInputsEntry entry={entry} isLast={isLast} value={value} setValue={setValue} setLabels={setLabels} labels={labels} />
|
|
186
187
|
}
|
|
187
188
|
|
|
188
|
-
return (entry.content || entry.error) && (
|
|
189
|
+
return (entry.content || entry.error || !!entry.steps?.length) && (
|
|
189
190
|
<li className={entry.agentType} ref={ref}>
|
|
190
191
|
<div className="chat-message" ref={chatRef} onKeyDown={handleKeyDown} tabIndex={0}>
|
|
191
192
|
<div className="user-info">{userInfo}</div>
|
|
192
|
-
{entry.content && <div className={listToClass(['message-content', entry.card && 'card'])}>
|
|
193
|
-
{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">
|
|
194
195
|
{entry.badges.map((b, index) => <Badge key={index} palette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
|
|
195
196
|
</div>}
|
|
196
197
|
{renderContent()}
|
|
@@ -212,6 +213,7 @@ export const ChatMessage = ({ message, username, isLast }: Props) => {
|
|
|
212
213
|
)}
|
|
213
214
|
</div>
|
|
214
215
|
)}
|
|
216
|
+
{!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
|
|
215
217
|
</div>}
|
|
216
218
|
</div>
|
|
217
219
|
{entry.error && (
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Button, IconBox, Text } from '@citric/core'
|
|
2
|
+
import { CheckCircleFill, Circle, PlayFill, TimesCircleFill } from '@citric/icons'
|
|
3
|
+
import { LoadingCircular } from '@citric/ui'
|
|
4
|
+
import { AnimatedHeight } from '@stack-spot/portal-components/AnimatedHeight'
|
|
5
|
+
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
6
|
+
import { findLastIndex } from 'lodash'
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
import { useWidget } from '../../context/hooks'
|
|
9
|
+
import { ChatEntryStep } from '../../state/ChatEntry'
|
|
10
|
+
import { PropsOf } from '../../types'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
steps: ChatEntryStep[],
|
|
14
|
+
messageId: number,
|
|
15
|
+
chatId: string,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface StepProps {
|
|
19
|
+
step: ChatEntryStep,
|
|
20
|
+
index: number,
|
|
21
|
+
total: number,
|
|
22
|
+
onClick?: () => void,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getStatusIcon(status: ChatEntryStep['status']) {
|
|
26
|
+
const iconBoxProps: PropsOf<typeof IconBox> = { colorIcon: 'light.700', size: 'xs' }
|
|
27
|
+
switch (status) {
|
|
28
|
+
case 'error': return <IconBox {...iconBoxProps}><TimesCircleFill /></IconBox>
|
|
29
|
+
case 'success': return <IconBox {...iconBoxProps}><CheckCircleFill /></IconBox>
|
|
30
|
+
case 'pending': return <IconBox {...iconBoxProps}><Circle /></IconBox>
|
|
31
|
+
case 'running': return <LoadingCircular className="loading" colorScheme="inverse" size="xs" />
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const Step = ({ step, index, total, onClick }: StepProps) => {
|
|
36
|
+
const t = useTranslate(dictionary)
|
|
37
|
+
return (
|
|
38
|
+
<li tabIndex={onClick ? 0 : undefined} onClick={onClick} role={onClick ? 'button' : 'listitem'}>
|
|
39
|
+
<div className="step-status-icon">{getStatusIcon(step.status)}</div>
|
|
40
|
+
<Text className="step-title" appearance="microtext1" colorScheme="light.700">
|
|
41
|
+
{t.step} {index}/{total}: {step.input}
|
|
42
|
+
</Text>
|
|
43
|
+
</li>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const StepsList = ({ steps, chatId, messageId }: Props) => {
|
|
48
|
+
const t = useTranslate(dictionary)
|
|
49
|
+
const [isExpanded, setExpanded] = useState(false)
|
|
50
|
+
const actualSteps = steps.filter(s => s.type === 'step')
|
|
51
|
+
let currentStepIndex = findLastIndex(actualSteps, s => s.status === 'running' || s.status === 'success')
|
|
52
|
+
if (currentStepIndex === -1) currentStepIndex = 0
|
|
53
|
+
const widget = useWidget()
|
|
54
|
+
|
|
55
|
+
function openToolsPanel() {
|
|
56
|
+
widget.set('currentMessageInToolsPanel', { chatId, messageId })
|
|
57
|
+
widget.set('panel', 'tools')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<AnimatedHeight>
|
|
62
|
+
<div className="steps">
|
|
63
|
+
<ul>
|
|
64
|
+
{isExpanded
|
|
65
|
+
? actualSteps.map((s, i) => <Step step={s} key={i} index={i + 1} total={actualSteps.length} />)
|
|
66
|
+
: <Step
|
|
67
|
+
step={actualSteps[currentStepIndex]}
|
|
68
|
+
index={currentStepIndex + 1}
|
|
69
|
+
total={actualSteps.length}
|
|
70
|
+
onClick={() => setExpanded(true)}
|
|
71
|
+
/>
|
|
72
|
+
}
|
|
73
|
+
</ul>
|
|
74
|
+
{isExpanded && <div className="step-actions">
|
|
75
|
+
<Button colorScheme="light" size="sm" onClick={() => setExpanded(false)}>{t.hideSteps}</Button>
|
|
76
|
+
<Button colorScheme="light" size="sm" className="icon-button" onClick={openToolsPanel}>
|
|
77
|
+
<IconBox size="xs"><PlayFill /></IconBox>
|
|
78
|
+
{t.detailed}
|
|
79
|
+
</Button>
|
|
80
|
+
</div>}
|
|
81
|
+
</div>
|
|
82
|
+
</AnimatedHeight>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dictionary = {
|
|
87
|
+
en: {
|
|
88
|
+
step: 'Step',
|
|
89
|
+
hideSteps: 'Hide steps',
|
|
90
|
+
detailed: 'View detailed mode',
|
|
91
|
+
},
|
|
92
|
+
pt: {
|
|
93
|
+
step: 'Passo',
|
|
94
|
+
hideSteps: 'Esconder passos',
|
|
95
|
+
detailed: 'Ver modo detalhado',
|
|
96
|
+
},
|
|
97
|
+
} satisfies Dictionary
|
package/src/views/Chat/styled.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { theme } from '@stack-spot/portal-theme'
|
|
2
|
+
import { DetailedHTMLProps, HTMLAttributes } from 'react'
|
|
2
3
|
import { styled } from 'styled-components'
|
|
4
|
+
import { FastOmit, IStyledComponentBase } from 'styled-components/dist/types'
|
|
3
5
|
|
|
4
|
-
export const ChatList
|
|
6
|
+
export const ChatList: IStyledComponentBase<
|
|
7
|
+
'web',
|
|
8
|
+
FastOmit<DetailedHTMLProps<HTMLAttributes<HTMLUListElement>, HTMLUListElement>, never>
|
|
9
|
+
> = styled.ul`
|
|
5
10
|
display: flex;
|
|
6
11
|
flex-direction: column;
|
|
7
12
|
justify-content: end;
|
|
@@ -183,4 +188,60 @@ export const ChatList = styled.ul`
|
|
|
183
188
|
}
|
|
184
189
|
}
|
|
185
190
|
}
|
|
191
|
+
|
|
192
|
+
.steps {
|
|
193
|
+
ul {
|
|
194
|
+
list-style: none;
|
|
195
|
+
margin: 0;
|
|
196
|
+
padding: 0;
|
|
197
|
+
display: flex;
|
|
198
|
+
flex-direction: column;
|
|
199
|
+
gap: 6px;
|
|
200
|
+
|
|
201
|
+
li {
|
|
202
|
+
display: flex;
|
|
203
|
+
flex-direction: row;
|
|
204
|
+
gap: 4px;
|
|
205
|
+
align-items: center;
|
|
206
|
+
|
|
207
|
+
&[role="button"] {
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.loading {
|
|
212
|
+
width: 12px;
|
|
213
|
+
height: 12px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.step-status-icon {
|
|
217
|
+
width: 20px;
|
|
218
|
+
height: 20px;
|
|
219
|
+
display: flex;
|
|
220
|
+
justify-content: center;
|
|
221
|
+
align-items: center;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.step-title {
|
|
225
|
+
line-height: 0.75rem;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
text-overflow: ellipsis;
|
|
228
|
+
display: -webkit-box;
|
|
229
|
+
-webkit-line-clamp: 1;
|
|
230
|
+
-webkit-box-orient: vertical;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.step-actions {
|
|
236
|
+
margin-top: 8px;
|
|
237
|
+
display: flex;
|
|
238
|
+
gap: 6px;
|
|
239
|
+
|
|
240
|
+
.icon-button {
|
|
241
|
+
display: flex;
|
|
242
|
+
gap: 6px;
|
|
243
|
+
align-items: center;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
186
247
|
`
|
package/src/views/Stacks.tsx
CHANGED
|
@@ -46,6 +46,7 @@ const StacksTab = ({ visibility }: { visibility: VisibilityLevelEnum }) => {
|
|
|
46
46
|
const { close } = useRightPanel()
|
|
47
47
|
const chat = useCurrentChat()
|
|
48
48
|
const [filter, setFilter] = useState('')
|
|
49
|
+
// @ts-ignore type in backend (openapi) is incorrect. fixme.
|
|
49
50
|
const stacks = aiClient.aiStacks.useQuery({ visibility, order: 'a-to-z' })
|
|
50
51
|
const [value, setValue] = useState<GetAiStackResponse | undefined>(stacks.find(s => s.id === chat.get('stack')?.id))
|
|
51
52
|
const filtered = useMemo(
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Handle, Position } from '@xyflow/react'
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
renderSource?: boolean,
|
|
5
|
+
renderTarget?: boolean,
|
|
6
|
+
}
|
|
7
|
+
export const HandleGroup = ({ renderSource = true, renderTarget = true }: Props) => (
|
|
8
|
+
<>
|
|
9
|
+
{renderTarget && <Handle type="target" position={Position.Left} isConnectable className="target-handle" />}
|
|
10
|
+
{renderSource && <Handle type="source" position={Position.Right} isConnectable className="source-handle" />}
|
|
11
|
+
</>
|
|
12
|
+
)
|