@stack-spot/ai-chat-widget 0.1.0 → 0.3.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/dist/StackspotAIWidget.d.ts.map +1 -1
- package/dist/StackspotAIWidget.js +4 -1
- package/dist/StackspotAIWidget.js.map +1 -1
- package/dist/chat-interceptors/send-message.d.ts.map +1 -1
- package/dist/chat-interceptors/send-message.js +15 -7
- package/dist/chat-interceptors/send-message.js.map +1 -1
- package/dist/components/HistoryList.d.ts +2 -5
- package/dist/components/HistoryList.d.ts.map +1 -1
- package/dist/components/HistoryList.js +70 -2
- package/dist/components/HistoryList.js.map +1 -1
- package/dist/components/OverlayMenu.d.ts +3 -2
- package/dist/components/OverlayMenu.d.ts.map +1 -1
- package/dist/components/OverlayMenu.js +57 -1
- package/dist/components/OverlayMenu.js.map +1 -1
- package/dist/components/RightPanelTabs.d.ts.map +1 -1
- package/dist/components/RightPanelTabs.js +1 -0
- package/dist/components/RightPanelTabs.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.d.ts +2 -1
- package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/components/Tooltip/Tooltip.js +10 -2
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tooltip/TooltipAPI.d.ts +3 -2
- package/dist/components/Tooltip/TooltipAPI.d.ts.map +1 -1
- package/dist/components/Tooltip/TooltipAPI.js +26 -1
- package/dist/components/Tooltip/TooltipAPI.js.map +1 -1
- package/dist/components/Tooltip/style.d.ts.map +1 -1
- package/dist/components/Tooltip/style.js +0 -1
- package/dist/components/Tooltip/style.js.map +1 -1
- package/dist/components/Tooltip/types.d.ts +6 -0
- package/dist/components/Tooltip/types.d.ts.map +1 -1
- package/dist/components/form/styled.d.ts.map +1 -1
- package/dist/components/form/styled.js +2 -1
- package/dist/components/form/styled.js.map +1 -1
- package/dist/context/hooks.d.ts.map +1 -1
- package/dist/context/hooks.js +1 -5
- package/dist/context/hooks.js.map +1 -1
- package/dist/features.d.ts.map +1 -1
- package/dist/features.js +2 -0
- package/dist/features.js.map +1 -1
- package/dist/right-panel/DefaultPanel.d.ts +2 -2
- package/dist/right-panel/DefaultPanel.d.ts.map +1 -1
- package/dist/right-panel/DefaultPanel.js +2 -1
- package/dist/right-panel/DefaultPanel.js.map +1 -1
- package/dist/right-panel/hooks.d.ts +2 -2
- package/dist/right-panel/hooks.d.ts.map +1 -1
- package/dist/state/ChatEntry.d.ts +7 -0
- package/dist/state/ChatEntry.d.ts.map +1 -1
- package/dist/state/ChatEntry.js +0 -3
- package/dist/state/ChatEntry.js.map +1 -1
- package/dist/state/ChatState.d.ts +4 -1
- package/dist/state/ChatState.d.ts.map +1 -1
- package/dist/state/ChatState.js.map +1 -1
- package/dist/state/WidgetState.d.ts +19 -8
- package/dist/state/WidgetState.d.ts.map +1 -1
- package/dist/state/WidgetState.js +0 -19
- package/dist/state/WidgetState.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/chat.js +1 -1
- package/dist/utils/chat.js.map +1 -1
- package/dist/utils/date.d.ts +1 -0
- package/dist/utils/date.d.ts.map +1 -1
- package/dist/utils/date.js +3 -0
- package/dist/utils/date.js.map +1 -1
- package/dist/utils/download.d.ts +2 -0
- package/dist/utils/download.d.ts.map +1 -0
- package/dist/utils/download.js +10 -0
- package/dist/utils/download.js.map +1 -0
- package/dist/utils/knowledge-source.d.ts +9 -0
- package/dist/utils/knowledge-source.d.ts.map +1 -0
- package/dist/utils/knowledge-source.js +46 -0
- package/dist/utils/knowledge-source.js.map +1 -0
- package/dist/views/Agents.d.ts.map +1 -1
- package/dist/views/Agents.js +130 -1
- package/dist/views/Agents.js.map +1 -1
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +10 -5
- package/dist/views/Chat/ChatMessage.js.map +1 -1
- package/dist/views/Chat/chat-scroll.d.ts.map +1 -0
- package/dist/{hooks → views/Chat}/chat-scroll.js +5 -3
- package/dist/views/Chat/chat-scroll.js.map +1 -0
- package/dist/views/Chat/styled.d.ts.map +1 -1
- package/dist/views/Chat/styled.js +24 -0
- package/dist/views/Chat/styled.js.map +1 -1
- package/dist/views/ChatHistory/ChatHistoryPanel.d.ts +5 -0
- package/dist/views/ChatHistory/ChatHistoryPanel.d.ts.map +1 -0
- package/dist/views/ChatHistory/ChatHistoryPanel.js +10 -0
- package/dist/views/ChatHistory/ChatHistoryPanel.js.map +1 -0
- package/dist/views/ChatHistory/HistoryItem.d.ts +7 -0
- package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -0
- package/dist/views/ChatHistory/HistoryItem.js +109 -0
- package/dist/views/ChatHistory/HistoryItem.js.map +1 -0
- package/dist/views/ChatHistory/dictionary.d.ts +2 -0
- package/dist/views/ChatHistory/dictionary.d.ts.map +1 -0
- package/dist/views/ChatHistory/dictionary.js +19 -0
- package/dist/views/ChatHistory/dictionary.js.map +1 -0
- package/dist/views/ChatHistory/index.d.ts +5 -0
- package/dist/views/ChatHistory/index.d.ts.map +1 -0
- package/dist/views/ChatHistory/index.js +23 -0
- package/dist/views/ChatHistory/index.js.map +1 -0
- package/dist/views/ChatHistory/styled.d.ts +2 -0
- package/dist/views/ChatHistory/styled.d.ts.map +1 -0
- package/dist/views/ChatHistory/styled.js +60 -0
- package/dist/views/ChatHistory/styled.js.map +1 -0
- package/dist/views/ChatHistory/utils.d.ts +4 -0
- package/dist/views/ChatHistory/utils.d.ts.map +1 -0
- package/dist/views/ChatHistory/utils.js +28 -0
- package/dist/views/ChatHistory/utils.js.map +1 -0
- package/dist/views/ChatTabSelection.js +1 -1
- package/dist/views/ChatTabSelection.js.map +1 -1
- package/dist/views/KSDocument.d.ts +2 -0
- package/dist/views/KSDocument.d.ts.map +1 -0
- package/dist/views/KSDocument.js +40 -0
- package/dist/views/KSDocument.js.map +1 -0
- package/dist/views/KnowledgeSources.d.ts.map +1 -1
- package/dist/views/KnowledgeSources.js +35 -24
- package/dist/views/KnowledgeSources.js.map +1 -1
- package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
- package/dist/views/MessageInput/ButtonGroup.js +5 -3
- package/dist/views/MessageInput/ButtonGroup.js.map +1 -1
- package/dist/views/MessageInput/dictionary.d.ts +1 -1
- package/dist/views/MessageInput/index.d.ts.map +1 -1
- package/dist/views/MessageInput/index.js +2 -4
- package/dist/views/MessageInput/index.js.map +1 -1
- package/dist/views/MessageInput/styled.d.ts +2 -0
- package/dist/views/MessageInput/styled.d.ts.map +1 -1
- package/dist/views/MessageInput/styled.js +11 -3
- package/dist/views/MessageInput/styled.js.map +1 -1
- package/dist/views/Stacks.js +9 -6
- package/dist/views/Stacks.js.map +1 -1
- package/dist/views/Workspaces.d.ts.map +1 -1
- package/dist/views/Workspaces.js +8 -5
- package/dist/views/Workspaces.js.map +1 -1
- package/package.json +3 -2
- package/src/StackspotAIWidget.tsx +6 -0
- package/src/chat-interceptors/send-message.ts +16 -8
- package/src/components/HistoryList.tsx +80 -7
- package/src/components/OverlayMenu.tsx +70 -3
- package/src/components/RightPanelTabs.tsx +1 -0
- package/src/components/Tooltip/Tooltip.tsx +13 -7
- package/src/components/Tooltip/TooltipAPI.ts +22 -2
- package/src/components/Tooltip/style.tsx +0 -1
- package/src/components/Tooltip/types.ts +7 -0
- package/src/components/form/styled.ts +2 -1
- package/src/context/hooks.ts +1 -4
- package/src/features.ts +2 -0
- package/src/right-panel/DefaultPanel.tsx +5 -4
- package/src/right-panel/hooks.tsx +2 -2
- package/src/state/ChatEntry.ts +8 -3
- package/src/state/ChatState.ts +5 -1
- package/src/state/WidgetState.ts +14 -26
- package/src/types.ts +1 -1
- package/src/utils/chat.ts +1 -1
- package/src/utils/date.ts +4 -0
- package/src/utils/download.ts +12 -0
- package/src/utils/knowledge-source.ts +55 -0
- package/src/views/Agents.tsx +187 -1
- package/src/views/Chat/ChatMessage.tsx +19 -5
- package/src/{hooks → views/Chat}/chat-scroll.ts +6 -3
- package/src/views/Chat/styled.ts +24 -0
- package/src/views/ChatHistory/ChatHistoryPanel.tsx +28 -0
- package/src/views/ChatHistory/HistoryItem.tsx +127 -0
- package/src/views/ChatHistory/dictionary.ts +20 -0
- package/src/views/ChatHistory/index.tsx +31 -0
- package/src/views/ChatHistory/styled.ts +60 -0
- package/src/views/ChatHistory/utils.ts +26 -0
- package/src/views/ChatTabSelection.tsx +1 -1
- package/src/views/KSDocument.tsx +58 -0
- package/src/views/KnowledgeSources.tsx +47 -25
- package/src/views/MessageInput/ButtonGroup.tsx +9 -7
- package/src/views/MessageInput/index.tsx +2 -5
- package/src/views/MessageInput/styled.ts +11 -3
- package/src/views/Stacks.tsx +10 -6
- package/src/views/Workspaces.tsx +10 -6
- package/dist/hooks/chat-scroll.d.ts.map +0 -1
- package/dist/hooks/chat-scroll.js.map +0 -1
- /package/dist/{hooks → views/Chat}/chat-scroll.d.ts +0 -0
|
@@ -8,21 +8,27 @@ import { TooltipPosition } from './types'
|
|
|
8
8
|
interface Props extends WithChildren, WithStyle {
|
|
9
9
|
content: React.ReactNode,
|
|
10
10
|
position?: TooltipPosition,
|
|
11
|
+
triggeredBy?: 'click' | 'hover',
|
|
11
12
|
custom?: boolean,
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
export const Tooltip = ({ content, custom, position = '
|
|
15
|
+
export const Tooltip = ({ content, custom, position, triggeredBy = 'hover', children, className, style }: Props) => {
|
|
15
16
|
const api = useTooltip()
|
|
17
|
+
|
|
18
|
+
function show(e: React.MouseEvent<HTMLDivElement, MouseEvent>, hideOnClickOutside?: boolean) {
|
|
19
|
+
api.show({
|
|
20
|
+
content: custom ? content : <DefaultTooltip><Text appearance="microtext1">{content}</Text></DefaultTooltip>,
|
|
21
|
+
anchor: e.target as HTMLElement,
|
|
22
|
+
position,
|
|
23
|
+
hideOnClickOutside,
|
|
24
|
+
})
|
|
25
|
+
}
|
|
16
26
|
return (
|
|
17
27
|
<div
|
|
18
|
-
onMouseEnter
|
|
19
|
-
custom ? content : <DefaultTooltip><Text appearance="microtext1">{content}</Text></DefaultTooltip>,
|
|
20
|
-
e.target as HTMLElement,
|
|
21
|
-
position,
|
|
22
|
-
)}
|
|
23
|
-
onMouseLeave={() => api.hide()}
|
|
28
|
+
{...(triggeredBy === 'hover' ? { onMouseEnter: show, onMouseLeave: () => api.hide() } : { onClick: (e) => show(e, true) })}
|
|
24
29
|
className={className}
|
|
25
30
|
style={style}
|
|
31
|
+
tabIndex={triggeredBy === 'click' ? 0 : undefined}
|
|
26
32
|
>
|
|
27
33
|
{children}
|
|
28
34
|
</div>
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
import { animationTimeMS } from './style'
|
|
2
|
-
import {
|
|
2
|
+
import { ShowOptions } from './types'
|
|
3
3
|
|
|
4
|
+
const MARGIN_TO_CORNERS_PX = 10
|
|
4
5
|
|
|
5
6
|
export class TooltipAPI {
|
|
6
7
|
private tooltipRef: React.RefObject<HTMLDivElement>
|
|
7
8
|
private setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>
|
|
8
9
|
private hideTimeoutId: number | undefined
|
|
10
|
+
private clickListener: ((e: MouseEvent) => void) | undefined
|
|
9
11
|
|
|
10
12
|
constructor(tooltipRef: React.RefObject<HTMLDivElement>, setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>) {
|
|
11
13
|
this.tooltipRef = tooltipRef
|
|
12
14
|
this.setContent = setContent
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
show(content
|
|
17
|
+
show({ content, anchor, position = 'bottom', hideOnClickOutside }: ShowOptions): void {
|
|
16
18
|
window.clearTimeout(this.hideTimeoutId)
|
|
17
19
|
this.hideTimeoutId = undefined
|
|
20
|
+
if (this.clickListener) document.removeEventListener('click', this.clickListener)
|
|
18
21
|
this.setContent(content)
|
|
19
22
|
setTimeout(() => {
|
|
20
23
|
if (!this.tooltipRef.current) return
|
|
@@ -33,8 +36,24 @@ export class TooltipAPI {
|
|
|
33
36
|
if (position === 'top') top = rect.top - tooltipHeight
|
|
34
37
|
else top = rect.top + rect.height
|
|
35
38
|
}
|
|
39
|
+
// adjusts positions in order to avoid overflowing the window and leaving a margin to the corners
|
|
40
|
+
if (top <= 0) top += MARGIN_TO_CORNERS_PX
|
|
41
|
+
else if (top + tooltipHeight >= document.body.clientHeight - MARGIN_TO_CORNERS_PX) {
|
|
42
|
+
top = document.body.clientHeight - MARGIN_TO_CORNERS_PX + tooltipHeight
|
|
43
|
+
}
|
|
44
|
+
if (left <= 0) left += MARGIN_TO_CORNERS_PX
|
|
45
|
+
else if (left + tooltipWidth >= document.body.clientWidth - MARGIN_TO_CORNERS_PX) {
|
|
46
|
+
left = document.body.clientWidth - MARGIN_TO_CORNERS_PX - tooltipWidth
|
|
47
|
+
}
|
|
36
48
|
this.tooltipRef.current.style.top = `${top}px`
|
|
37
49
|
this.tooltipRef.current.style.left = `${left}px`
|
|
50
|
+
if (hideOnClickOutside) {
|
|
51
|
+
this.clickListener = (e: MouseEvent) => {
|
|
52
|
+
if (this.tooltipRef.current?.contains(e.target as HTMLElement)) return
|
|
53
|
+
this.hide()
|
|
54
|
+
}
|
|
55
|
+
document.addEventListener('click', this.clickListener)
|
|
56
|
+
}
|
|
38
57
|
}, 10)
|
|
39
58
|
}
|
|
40
59
|
|
|
@@ -42,5 +61,6 @@ export class TooltipAPI {
|
|
|
42
61
|
if (!this.tooltipRef.current) return
|
|
43
62
|
this.tooltipRef.current.classList.remove('visible')
|
|
44
63
|
this.hideTimeoutId = window.setTimeout(() => this.setContent(undefined), animationTimeMS)
|
|
64
|
+
if (this.clickListener) document.removeEventListener('click', this.clickListener)
|
|
45
65
|
}
|
|
46
66
|
}
|
|
@@ -9,7 +9,7 @@ export const RadioCheckBox = styled.ul`
|
|
|
9
9
|
flex-direction: column;
|
|
10
10
|
gap: 6px;
|
|
11
11
|
|
|
12
|
-
li {
|
|
12
|
+
> li {
|
|
13
13
|
display: flex;
|
|
14
14
|
flex-direction: column;
|
|
15
15
|
gap: 8px;
|
|
@@ -26,6 +26,7 @@ export const RadioCheckBox = styled.ul`
|
|
|
26
26
|
input[type="radio"], input[type="checkbox"] {
|
|
27
27
|
border-color: ${theme.color.light[700]};
|
|
28
28
|
background-color: transparent;
|
|
29
|
+
flex-shrink: 0;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
input[type="checkbox"]:checked {
|
package/src/context/hooks.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable react-hooks/rules-of-hooks */
|
|
2
|
-
import { useContext, useEffect,
|
|
2
|
+
import { useContext, useEffect, useState } from 'react'
|
|
3
3
|
import { ChatEntry } from '../state/ChatEntry'
|
|
4
4
|
import { ChatProperties, ChatState, MessageInterceptor } from '../state/ChatState'
|
|
5
5
|
import { ObservableState } from '../state/ObservableState'
|
|
@@ -78,9 +78,6 @@ export function useCurrentChatMessages(): ChatEntry[] {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
export function useChatEntry(entry: ChatEntry) {
|
|
81
|
-
const immutable = useMemo(() => entry.hasFinished(), [])
|
|
82
|
-
// the following condition is not a problem for react hooks, it will always be true or always false.
|
|
83
|
-
if (immutable) return entry.getValue()
|
|
84
81
|
const [content, setContent] = useState(entry.getValue())
|
|
85
82
|
useEffect(() => entry.onChange(setContent), [])
|
|
86
83
|
return content
|
package/src/features.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { styled } from 'styled-components'
|
|
|
6
6
|
import { WithChildren } from '../types'
|
|
7
7
|
|
|
8
8
|
interface Props extends WithChildren {
|
|
9
|
-
title:
|
|
10
|
-
description:
|
|
9
|
+
title: React.ReactNode,
|
|
10
|
+
description: React.ReactNode,
|
|
11
11
|
onClose: () => void,
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -22,6 +22,7 @@ const PanelBox = styled.div`
|
|
|
22
22
|
header {
|
|
23
23
|
display: flex;
|
|
24
24
|
flex-direction: row;
|
|
25
|
+
gap: 10px;
|
|
25
26
|
|
|
26
27
|
.title {
|
|
27
28
|
display: flex;
|
|
@@ -45,8 +46,8 @@ export const DefaultPanel = ({ description, onClose, title, children }: Props) =
|
|
|
45
46
|
<PanelBox>
|
|
46
47
|
<header>
|
|
47
48
|
<div className="title">
|
|
48
|
-
<Text appearance="h5">{title}</Text>
|
|
49
|
-
<Text colorScheme="light.700">{description}</Text>
|
|
49
|
+
{typeof title === 'string' ? <Text appearance="h5">{title}</Text> : title}
|
|
50
|
+
{typeof description === 'string' ? <Text colorScheme="light.700">{description}</Text> : description}
|
|
50
51
|
</div>
|
|
51
52
|
<IconButton title={t.close} aria-label={t.close} onClick={onClose}>
|
|
52
53
|
<Times />
|
|
@@ -3,8 +3,8 @@ import { DefaultPanel } from './DefaultPanel'
|
|
|
3
3
|
import { RightPanelContext } from './RightPanelProvider'
|
|
4
4
|
|
|
5
5
|
interface RightPanelOptions {
|
|
6
|
-
title:
|
|
7
|
-
description:
|
|
6
|
+
title: React.ReactNode,
|
|
7
|
+
description: React.ReactNode,
|
|
8
8
|
onClose?: () => void,
|
|
9
9
|
}
|
|
10
10
|
|
package/src/state/ChatEntry.ts
CHANGED
|
@@ -16,6 +16,13 @@ export interface ChatAction extends SerializableAction {
|
|
|
16
16
|
appearance?: 'primary' | 'secondary',
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
export interface KnowledgeSource {
|
|
20
|
+
name: string,
|
|
21
|
+
slug: string,
|
|
22
|
+
documentScore: number,
|
|
23
|
+
documentId: string,
|
|
24
|
+
}
|
|
25
|
+
|
|
19
26
|
export interface TextChatEntry {
|
|
20
27
|
type: 'text' | 'md',
|
|
21
28
|
agent: 'bot' | 'user' | 'system',
|
|
@@ -23,7 +30,7 @@ export interface TextChatEntry {
|
|
|
23
30
|
actions?: ChatAction[],
|
|
24
31
|
subtitle?: string,
|
|
25
32
|
content: string,
|
|
26
|
-
|
|
33
|
+
knowledgeSources?: KnowledgeSource[],
|
|
27
34
|
updated?: string,
|
|
28
35
|
agentId?: string,
|
|
29
36
|
messageId?: string,
|
|
@@ -68,7 +75,6 @@ export class ChatEntry {
|
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
setValue(value: TextChatEntry) {
|
|
71
|
-
if (this.streamFinished) return
|
|
72
78
|
this.value = value
|
|
73
79
|
this.listeners.forEach(l => l(this.value))
|
|
74
80
|
}
|
|
@@ -79,7 +85,6 @@ export class ChatEntry {
|
|
|
79
85
|
|
|
80
86
|
finish() {
|
|
81
87
|
this.streamFinished = true
|
|
82
|
-
this.listeners = []
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
hasFinished() {
|
package/src/state/ChatState.ts
CHANGED
|
@@ -7,9 +7,13 @@ interface Labeled {
|
|
|
7
7
|
label: string,
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
interface LabeledWithImage extends Labeled {
|
|
11
|
+
image?: string,
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
export interface ChatProperties {
|
|
11
15
|
label: string,
|
|
12
|
-
agent?:
|
|
16
|
+
agent?: LabeledWithImage,
|
|
13
17
|
workspace?: Labeled,
|
|
14
18
|
stack?: Labeled,
|
|
15
19
|
knowledgeSources?: Labeled[],
|
package/src/state/WidgetState.ts
CHANGED
|
@@ -2,41 +2,29 @@ import { ChatTabsController } from './ChatTabsController'
|
|
|
2
2
|
import { ObservableState } from './ObservableState'
|
|
3
3
|
|
|
4
4
|
export interface WidgetProperties {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Current content of the right panel. Undefined for closed right panel.
|
|
7
|
+
*/
|
|
8
|
+
panel?: 'stack' | 'workspace' | 'agent' | 'ks' | 'editor' | 'history' | 'ks-details',
|
|
9
|
+
/**
|
|
10
|
+
* KS to use when the right panel "ks-details" is open.
|
|
11
|
+
*/
|
|
12
|
+
currentKSInPanel?: { name: string, slug: string, score: number, documentId: string },
|
|
13
|
+
/**
|
|
14
|
+
* Whether or not the widget is in its minimized version.
|
|
15
|
+
*/
|
|
11
16
|
isMinimized?: boolean,
|
|
17
|
+
/**
|
|
18
|
+
* The current code in the editor.
|
|
19
|
+
*/
|
|
12
20
|
code?: string,
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
const panelKeys: (keyof WidgetProperties)[] = [
|
|
16
|
-
'isAgentSelectionOpen', 'isChatHistoryOpen', 'isCodeEditorOpen', 'isKnowledgeSourceSelectionOpen', 'isStackSelectionOpen',
|
|
17
|
-
'isWorkspaceSelectionOpen',
|
|
18
|
-
]
|
|
19
|
-
|
|
20
23
|
export class WidgetState extends ObservableState<WidgetProperties> {
|
|
21
24
|
readonly chatTabs: ChatTabsController
|
|
22
25
|
|
|
23
26
|
constructor(initial: WidgetProperties = {}, chatTabs?: ChatTabsController) {
|
|
24
27
|
super(initial)
|
|
25
28
|
this.chatTabs = chatTabs ?? new ChatTabsController()
|
|
26
|
-
this.createCloseOthersBehavior()
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
private createCloseOthersBehavior() {
|
|
30
|
-
panelKeys.forEach((key) => {
|
|
31
|
-
this.onChange(key, (value) => {
|
|
32
|
-
if (value) this.closeOtherPanels(key)
|
|
33
|
-
})
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
private closeOtherPanels(leaveOpen: keyof WidgetProperties) {
|
|
38
|
-
panelKeys.forEach((key) => {
|
|
39
|
-
if (key !== leaveOpen) this.set(key, false)
|
|
40
|
-
})
|
|
41
29
|
}
|
|
42
30
|
}
|
package/src/types.ts
CHANGED
package/src/utils/chat.ts
CHANGED
|
@@ -21,7 +21,7 @@ export function buildConversationContext(state: ChatState): FixedChatRequest['co
|
|
|
21
21
|
language: getLanguage(),
|
|
22
22
|
knowledge_sources: state.get('knowledgeSources')?.map(ks => ks.id),
|
|
23
23
|
agent_id: state.get('agent')?.id,
|
|
24
|
-
agent_built_in:
|
|
24
|
+
agent_built_in: !state.get('agent'),
|
|
25
25
|
os: navigator.userAgent,
|
|
26
26
|
platform: 'web-widget',
|
|
27
27
|
platform_version: navigator.userAgent,
|
package/src/utils/date.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function download(filename: string, text: string) {
|
|
2
|
+
const element = document.createElement('a')
|
|
3
|
+
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text))
|
|
4
|
+
element.setAttribute('download', filename)
|
|
5
|
+
|
|
6
|
+
element.style.display = 'none'
|
|
7
|
+
document.body.appendChild(element)
|
|
8
|
+
|
|
9
|
+
element.click()
|
|
10
|
+
|
|
11
|
+
document.body.removeChild(element)
|
|
12
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DocumentResponse, SourceKnowledgeSource4, SourceProjectFile4, SourceStackAi } from '@stack-spot/portal-network/api/ai'
|
|
2
|
+
import { KnowledgeSource } from '../state/ChatEntry'
|
|
3
|
+
|
|
4
|
+
function attemptToParseMalFormedJson(str: string) {
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(str)
|
|
7
|
+
} catch {
|
|
8
|
+
try {
|
|
9
|
+
const fixed = str
|
|
10
|
+
.replace(/"/g, '-_-')
|
|
11
|
+
.replace(/'/g, '"')
|
|
12
|
+
.replace(/-_-/g, "'")
|
|
13
|
+
.replace(
|
|
14
|
+
/": (\w+)/g,
|
|
15
|
+
(_, value) => (['null', 'true', 'false'].includes(value) || value.match(/\d+/)) ? `": ${value}` : `": "${value}"`,
|
|
16
|
+
)
|
|
17
|
+
return JSON.parse(fixed)
|
|
18
|
+
} catch {
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function extractCodeFromKSDocument(document: DocumentResponse): { language?: string, snippet: string, text?: string } {
|
|
25
|
+
const language = (document?.metadata as any)?.language
|
|
26
|
+
if (language) {
|
|
27
|
+
const pageContent: string = document?.page_content ?? ''
|
|
28
|
+
let text: string | undefined = undefined
|
|
29
|
+
let snippet = pageContent
|
|
30
|
+
const match = pageContent.match(/^use case: .*\n/i)
|
|
31
|
+
if (match) {
|
|
32
|
+
text = match[0]
|
|
33
|
+
snippet = pageContent.replace(text, '')
|
|
34
|
+
}
|
|
35
|
+
return { language, snippet, text }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (document?.page_content) {
|
|
39
|
+
const parsed = attemptToParseMalFormedJson(document.page_content)
|
|
40
|
+
return parsed ? { language: 'json', snippet: JSON.stringify(parsed, null, 2) } : { snippet: document.page_content }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return typeof document === 'object' ? { language: 'json', snippet: JSON.stringify(document, null, 2) } : { snippet: String(document) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function genericSourcesToKnowledgeSources(
|
|
47
|
+
sources: (SourceStackAi | SourceKnowledgeSource4 | SourceProjectFile4)[] | undefined,
|
|
48
|
+
): KnowledgeSource[] | undefined {
|
|
49
|
+
return sources?.filter(s => s.type === 'knowledge_source').map(ks => ({
|
|
50
|
+
documentId: ks.document_id,
|
|
51
|
+
documentScore: ks.document_score,
|
|
52
|
+
name: ks.name,
|
|
53
|
+
slug: ks.slug,
|
|
54
|
+
}))
|
|
55
|
+
}
|
package/src/views/Agents.tsx
CHANGED
|
@@ -1 +1,187 @@
|
|
|
1
|
-
|
|
1
|
+
import { Button, Text } from '@citric/core'
|
|
2
|
+
import { Search } from '@citric/icons'
|
|
3
|
+
import { Badge } from '@citric/ui'
|
|
4
|
+
import { Placeholder } from '@stack-spot/portal-components/Placeholder'
|
|
5
|
+
import { agentClient } from '@stack-spot/portal-network'
|
|
6
|
+
import { AgentResponse, VisibilityLevel } from '@stack-spot/portal-network/api/agent'
|
|
7
|
+
import { theme } from '@stack-spot/portal-theme'
|
|
8
|
+
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
9
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
10
|
+
import { styled } from 'styled-components'
|
|
11
|
+
import { DescribedRadioGroup } from '../components/form/DescribedRadioGroup'
|
|
12
|
+
import { IconInput } from '../components/IconInput'
|
|
13
|
+
import { RightPanelTabs } from '../components/RightPanelTabs'
|
|
14
|
+
import { useCurrentChat, useWidget, useWidgetState } from '../context/hooks'
|
|
15
|
+
import { useRightPanel } from '../right-panel/hooks'
|
|
16
|
+
|
|
17
|
+
const AgentLabel = styled.div`
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: row;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: 6px;
|
|
22
|
+
|
|
23
|
+
img {
|
|
24
|
+
width: 20px;
|
|
25
|
+
height: 20px;
|
|
26
|
+
border-radius: 50%;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
}
|
|
29
|
+
`
|
|
30
|
+
|
|
31
|
+
const AgentDescription = styled.div`
|
|
32
|
+
color: ${theme.color.light[700]};
|
|
33
|
+
line-height: 18px;
|
|
34
|
+
|
|
35
|
+
section {
|
|
36
|
+
border-bottom: 1px solid ${theme.color.light[600]};
|
|
37
|
+
padding-bottom: 10px;
|
|
38
|
+
margin-bottom: 10px;
|
|
39
|
+
|
|
40
|
+
&:last-child {
|
|
41
|
+
padding-bottom: 0;
|
|
42
|
+
margin-bottom: 0;
|
|
43
|
+
border-bottom: none;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.title {
|
|
47
|
+
display: block;
|
|
48
|
+
margin-bottom: 6px;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
ul {
|
|
53
|
+
padding: 0;
|
|
54
|
+
margin: 0;
|
|
55
|
+
list-style: none;
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: row;
|
|
58
|
+
flex-wrap: wrap;
|
|
59
|
+
white-space: nowrap;
|
|
60
|
+
gap: 6px;
|
|
61
|
+
|
|
62
|
+
li {
|
|
63
|
+
margin: 3px 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
`
|
|
67
|
+
|
|
68
|
+
export const Agents = () => {
|
|
69
|
+
const t = useTranslate(dictionary)
|
|
70
|
+
const panel = useWidgetState('panel')
|
|
71
|
+
const { open } = useRightPanel()
|
|
72
|
+
const widget = useWidget()
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (panel === 'agent') open(
|
|
76
|
+
<AgentsPanel />,
|
|
77
|
+
{ title: t.title, description: t.description, onClose: () => widget.set('panel', undefined) },
|
|
78
|
+
)
|
|
79
|
+
}, [panel, t])
|
|
80
|
+
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const AgentsPanel = () => {
|
|
85
|
+
const t = useTranslate(dictionary)
|
|
86
|
+
const chat = useCurrentChat()
|
|
87
|
+
|
|
88
|
+
return <RightPanelTabs key={chat.id} tabs={[
|
|
89
|
+
{ title: t.personal, content: <AgentsTab key="personal" visibility="PERSONAL" /> },
|
|
90
|
+
{ title: t.builtin, content: <AgentsTab key="builtin" visibility="BUILT-IN" /> },
|
|
91
|
+
{ title: t.shared, content: <AgentsTab key="shared" visibility="SHARED" /> },
|
|
92
|
+
{ title: t.account, content: <AgentsTab key="account" visibility="ACCOUNT" /> },
|
|
93
|
+
]} />
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const AgentsTab = ({ visibility }: { visibility: VisibilityLevel | 'BUILT-IN' }) => {
|
|
97
|
+
const t = useTranslate(dictionary)
|
|
98
|
+
const { close } = useRightPanel()
|
|
99
|
+
const chat = useCurrentChat()
|
|
100
|
+
const [filter, setFilter] = useState('')
|
|
101
|
+
const agents = visibility === 'BUILT-IN' ? agentClient.publicAgents.useQuery({}) : agentClient.agents.useQuery({ visibility })
|
|
102
|
+
const [value, setValue] = useState<AgentResponse | undefined>(agents.find(a => a.id === chat.get('agent')?.id))
|
|
103
|
+
const filtered = useMemo(
|
|
104
|
+
() => filter ? agents.filter(a => a === value || a.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase())) : agents,
|
|
105
|
+
[agents, filter, value],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
function submit() {
|
|
109
|
+
if (value) chat.set('agent', { id: value.id, label: value.name, image: value.avatar })
|
|
110
|
+
close()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<>
|
|
115
|
+
<div className="content">
|
|
116
|
+
<IconInput icon={<Search />} value={filter} onChange={setFilter} className="search" />
|
|
117
|
+
{!!filtered.length && <DescribedRadioGroup
|
|
118
|
+
options={filtered}
|
|
119
|
+
keygen={a => a.id}
|
|
120
|
+
value={value}
|
|
121
|
+
onChange={setValue}
|
|
122
|
+
renderLabel={({ name, avatar }) => (
|
|
123
|
+
<AgentLabel>
|
|
124
|
+
{avatar && <img src={avatar} />}
|
|
125
|
+
<Text>{name}</Text>
|
|
126
|
+
</AgentLabel>
|
|
127
|
+
)}
|
|
128
|
+
renderDescription={({ description, llm_config: llmc, knowledge_sources_config: ksc }) => (
|
|
129
|
+
<AgentDescription>
|
|
130
|
+
{description && <section>
|
|
131
|
+
<Text appearance="microtext1" className="title">Description</Text>
|
|
132
|
+
<Text>{description}</Text>
|
|
133
|
+
</section>}
|
|
134
|
+
{!!ksc?.knowledge_sources?.length && <section>
|
|
135
|
+
<Text appearance="microtext1" className="title">Knowledge sources</Text>
|
|
136
|
+
<ul>
|
|
137
|
+
{ksc.knowledge_sources.map((ks, index) => <li key={index}><Badge palette="teal" appearance="square">{ks}</Badge></li>)}
|
|
138
|
+
</ul>
|
|
139
|
+
</section>}
|
|
140
|
+
{llmc?.model_slug && <section>
|
|
141
|
+
<Text appearance="microtext1" className="title">LLM</Text>
|
|
142
|
+
<Badge palette="orange" appearance="square">{llmc.model_slug}</Badge>
|
|
143
|
+
</section>}
|
|
144
|
+
</AgentDescription>
|
|
145
|
+
)}
|
|
146
|
+
optionClassName={a => (a === value && filter && !a.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase()))
|
|
147
|
+
? 'filtered-out'
|
|
148
|
+
: ''
|
|
149
|
+
}
|
|
150
|
+
className="option-list"
|
|
151
|
+
/>}
|
|
152
|
+
{!!agents.length && !filtered.length && <Placeholder title={t.noSearchResults} description={t.noSearchResultsDescription} />}
|
|
153
|
+
{!agents.length && <Placeholder title={t.noData} description={t.noDataDescription} />}
|
|
154
|
+
</div>
|
|
155
|
+
<Button onClick={submit} disabled={!value}>{t.apply}</Button>
|
|
156
|
+
</>
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const dictionary = {
|
|
161
|
+
en: {
|
|
162
|
+
title: 'Agents',
|
|
163
|
+
description: 'By selecting an Agent, it will be consulted to generate the answers.',
|
|
164
|
+
personal: 'Personal',
|
|
165
|
+
builtin: 'Built-in',
|
|
166
|
+
shared: 'Shared',
|
|
167
|
+
account: 'Account',
|
|
168
|
+
apply: 'Apply',
|
|
169
|
+
noSearchResults: "Your search didn't yield results.",
|
|
170
|
+
noSearchResultsDescription: 'Please, try another search term.',
|
|
171
|
+
noData: 'There are no agents in this category yet.',
|
|
172
|
+
noDataDescription: 'Use the tabs above to try other categories or use the AI portal to create new agents.',
|
|
173
|
+
},
|
|
174
|
+
pt: {
|
|
175
|
+
title: 'Agentes',
|
|
176
|
+
description: 'Ao selecionar um Agente, ele será consultado para gerar as respostas.',
|
|
177
|
+
personal: 'Pessoal',
|
|
178
|
+
builtin: 'Embutido',
|
|
179
|
+
shared: 'Compartilhado',
|
|
180
|
+
account: 'Conta',
|
|
181
|
+
apply: 'Aplicar',
|
|
182
|
+
noSearchResults: 'Sua busca não produziu resultados.',
|
|
183
|
+
noSearchResultsDescription: 'Por favor, tente outra busca.',
|
|
184
|
+
noData: 'Ainda não há agentes nesta categoria.',
|
|
185
|
+
noDataDescription: 'Use as abas acima para tentar outras categorias ou use o Portal AI para criar novos agentes.',
|
|
186
|
+
},
|
|
187
|
+
} satisfies Dictionary
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { IconBox, Text } from '@citric/core'
|
|
1
|
+
import { Button, IconBox, Text } from '@citric/core'
|
|
2
2
|
import { Dislike, DislikeFill, Like, LikeFill, TimesCircle } from '@citric/icons'
|
|
3
3
|
import { Avatar, IconButton } from '@citric/ui'
|
|
4
4
|
import { aiClient } from '@stack-spot/portal-network'
|
|
5
5
|
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
6
|
-
import { useMemo, useRef, useState } from 'react'
|
|
6
|
+
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
7
7
|
import { Markdown } from '../../components/Markdown'
|
|
8
|
-
import { useChatEntry } from '../../context/hooks'
|
|
9
|
-
import {
|
|
10
|
-
import { ChatEntry } from '../../state/ChatEntry'
|
|
8
|
+
import { useChatEntry, useWidget } from '../../context/hooks'
|
|
9
|
+
import { ChatEntry, TextChatEntry } from '../../state/ChatEntry'
|
|
11
10
|
import { useDateFormatter } from '../../utils/date'
|
|
12
11
|
import { AgentInfo } from './AgentInfo'
|
|
12
|
+
import { useChatScrollToBottomEffect } from './chat-scroll'
|
|
13
13
|
|
|
14
14
|
export const ChatMessage = ({ message, username }: { message: ChatEntry, username: string }) => {
|
|
15
15
|
const t = useTranslate(dictionary)
|
|
@@ -20,8 +20,14 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
|
|
|
20
20
|
const date = new Date(entry.updated ?? '')
|
|
21
21
|
const shouldShowDate = entry.updated && !isNaN(date.getTime())
|
|
22
22
|
const ref = useRef<HTMLLIElement>(null)
|
|
23
|
+
const widget = useWidget()
|
|
23
24
|
useChatScrollToBottomEffect(ref, [entry])
|
|
24
25
|
|
|
26
|
+
const detailKS = useCallback(({ name, slug, documentScore, documentId }: Required<TextChatEntry>['knowledgeSources'][number]) => {
|
|
27
|
+
widget.set('currentKSInPanel', { name, slug, score: documentScore, documentId })
|
|
28
|
+
widget.set('panel', 'ks-details')
|
|
29
|
+
}, [])
|
|
30
|
+
|
|
25
31
|
const { like, dislike } = useMemo(() => {
|
|
26
32
|
async function feedback(like: boolean) {
|
|
27
33
|
if (!entry.messageId || like === liked) return
|
|
@@ -62,6 +68,14 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
|
|
|
62
68
|
<Text appearance="microtext1">{entry.error}</Text>
|
|
63
69
|
</div>
|
|
64
70
|
)}
|
|
71
|
+
{!!entry.knowledgeSources?.length && <div className="ks-box">
|
|
72
|
+
<Text appearance="microtext1" colorScheme="light.700">Knowledge Sources:</Text>
|
|
73
|
+
<ul>{entry.knowledgeSources.map(ks => (
|
|
74
|
+
<li key={ks.slug}>
|
|
75
|
+
<Button size="sm" colorScheme="light" onClick={() => detailKS(ks)}>{ks.name}</Button>
|
|
76
|
+
</li>
|
|
77
|
+
))}</ul>
|
|
78
|
+
</div>}
|
|
65
79
|
<div className="message-footer">
|
|
66
80
|
{entry.agent === 'bot' && entry.messageId && !entry.error && <div className="message-actions">
|
|
67
81
|
<IconButton title={t.like} aria-label={t.like} onClick={like}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Scrolls the closest chat (upwards in the tree) to its bottom.
|
|
@@ -6,9 +6,12 @@ import { useEffect } from 'react'
|
|
|
6
6
|
* @param deps when the deps changes, the chat is scrolled.
|
|
7
7
|
*/
|
|
8
8
|
export function useChatScrollToBottomEffect(ref: React.RefObject<HTMLElement>, deps: any[]) {
|
|
9
|
+
const prevScrollTop = useRef(0)
|
|
10
|
+
|
|
9
11
|
useEffect(() => {
|
|
10
12
|
const chat = ref.current?.closest('.chat-content')
|
|
11
|
-
if (!chat) return
|
|
12
|
-
chat.scrollTop = chat.scrollHeight
|
|
13
|
+
if (!chat || chat.scrollTop < prevScrollTop.current) return
|
|
14
|
+
chat.scrollTop = chat.scrollHeight - chat.clientHeight
|
|
15
|
+
prevScrollTop.current = chat.scrollTop
|
|
13
16
|
}, deps)
|
|
14
17
|
}
|