@stack-spot/ai-chat-widget 0.2.0 → 0.4.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 +2 -1
- package/dist/StackspotAIWidget.js.map +1 -1
- package/dist/chat-interceptors/quick-commands.js +2 -2
- package/dist/chat-interceptors/quick-commands.js.map +1 -1
- package/dist/chat-interceptors/send-message.d.ts.map +1 -1
- package/dist/chat-interceptors/send-message.js +6 -9
- 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/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 +5 -2
- package/dist/components/Tooltip/TooltipAPI.d.ts.map +1 -1
- package/dist/components/Tooltip/TooltipAPI.js +50 -8
- package/dist/components/Tooltip/TooltipAPI.js.map +1 -1
- package/dist/components/Tooltip/context.js +1 -1
- package/dist/components/Tooltip/context.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/features.d.ts.map +1 -1
- package/dist/features.js +1 -0
- package/dist/features.js.map +1 -1
- package/dist/layout.css +6 -0
- package/dist/state/ChatEntry.d.ts +3 -2
- package/dist/state/ChatEntry.d.ts.map +1 -1
- package/dist/state/ChatEntry.js +2 -2
- package/dist/state/ChatEntry.js.map +1 -1
- package/dist/state/ChatState.d.ts +1 -7
- package/dist/state/ChatState.d.ts.map +1 -1
- package/dist/state/ChatState.js.map +1 -1
- package/dist/state/types.d.ts +8 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +2 -0
- package/dist/state/types.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.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 +3 -1
- package/dist/utils/knowledge-source.d.ts.map +1 -1
- package/dist/utils/knowledge-source.js +8 -0
- package/dist/utils/knowledge-source.js.map +1 -1
- package/dist/views/Agents.js +2 -1
- package/dist/views/Agents.js.map +1 -1
- package/dist/views/Chat/AgentInfo.d.ts +3 -2
- package/dist/views/Chat/AgentInfo.d.ts.map +1 -1
- package/dist/views/Chat/AgentInfo.js +3 -3
- package/dist/views/Chat/AgentInfo.js.map +1 -1
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +3 -3
- 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 +8 -1
- 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 +116 -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 +5 -0
- package/dist/views/ChatHistory/utils.d.ts.map +1 -0
- package/dist/views/ChatHistory/utils.js +39 -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/KnowledgeSources.d.ts.map +1 -1
- package/dist/views/KnowledgeSources.js +30 -21
- package/dist/views/KnowledgeSources.js.map +1 -1
- package/dist/views/MessageInput/dictionary.d.ts +1 -1
- package/dist/views/Stacks.js +2 -1
- package/dist/views/Stacks.js.map +1 -1
- package/dist/views/Workspaces.d.ts.map +1 -1
- package/dist/views/Workspaces.js +3 -2
- package/dist/views/Workspaces.js.map +1 -1
- package/package.json +3 -2
- package/src/StackspotAIWidget.tsx +3 -1
- package/src/chat-interceptors/quick-commands.ts +2 -2
- package/src/chat-interceptors/send-message.ts +6 -9
- package/src/components/HistoryList.tsx +80 -7
- package/src/components/OverlayMenu.tsx +70 -3
- package/src/components/Tooltip/Tooltip.tsx +13 -7
- package/src/components/Tooltip/TooltipAPI.ts +47 -9
- package/src/components/Tooltip/context.tsx +1 -1
- package/src/components/Tooltip/style.tsx +0 -1
- package/src/components/Tooltip/types.ts +7 -0
- package/src/features.ts +1 -0
- package/src/layout.css +6 -0
- package/src/state/ChatEntry.ts +5 -4
- package/src/state/ChatState.ts +1 -9
- package/src/state/types.ts +8 -0
- package/src/types.ts +1 -1
- package/src/utils/date.ts +4 -0
- package/src/utils/download.ts +12 -0
- package/src/utils/knowledge-source.ts +13 -1
- package/src/views/Agents.tsx +2 -1
- package/src/views/Chat/AgentInfo.tsx +8 -8
- package/src/views/Chat/ChatMessage.tsx +4 -4
- package/src/{hooks → views/Chat}/chat-scroll.ts +6 -3
- package/src/views/Chat/styled.ts +8 -1
- package/src/views/ChatHistory/ChatHistoryPanel.tsx +28 -0
- package/src/views/ChatHistory/HistoryItem.tsx +139 -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 +37 -0
- package/src/views/ChatTabSelection.tsx +1 -1
- package/src/views/KnowledgeSources.tsx +39 -20
- package/src/views/Stacks.tsx +2 -1
- package/src/views/Workspaces.tsx +3 -2
- 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
|
@@ -1,16 +1,89 @@
|
|
|
1
1
|
/* eslint-disable no-empty-pattern */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { Text } from '@citric/core'
|
|
3
|
+
import { theme, WithStyle } from '@stack-spot/portal-theme'
|
|
4
|
+
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
5
|
+
import { groupBy, map } from 'lodash'
|
|
6
|
+
import { useMemo } from 'react'
|
|
7
|
+
import { styled } from 'styled-components'
|
|
8
|
+
import { subtractDays } from '../utils/date'
|
|
9
|
+
|
|
10
|
+
type HistorySectionName = 'today' | 'yesterday' | 'last7' | 'last30' | 'older'
|
|
4
11
|
|
|
5
12
|
interface Props<T> extends WithStyle {
|
|
6
13
|
items: T[],
|
|
7
|
-
|
|
14
|
+
renderItem: (item: T) => React.ReactNode,
|
|
8
15
|
getDate: (item: T) => Date,
|
|
9
16
|
keygen: (item: T) => React.Key,
|
|
10
|
-
getActions?: (item: T) => ButtonAction[],
|
|
11
|
-
onSelect?: (item: T) => void,
|
|
12
17
|
}
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
const HistoryBox = styled.div`
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 4px;
|
|
23
|
+
|
|
24
|
+
> section {
|
|
25
|
+
> header {
|
|
26
|
+
background-color: ${theme.color.light[500]};
|
|
27
|
+
padding: 8px;
|
|
28
|
+
border-radius: 4px;
|
|
29
|
+
color: ${theme.color.light[700]};
|
|
30
|
+
margin-bottom: 4px;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
> ul {
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: 0;
|
|
36
|
+
list-style: none;
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
gap: 2px;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
|
|
44
|
+
function dateToSectionName(date: Date): HistorySectionName {
|
|
45
|
+
const now = new Date()
|
|
46
|
+
if (date.toDateString() === now.toDateString()) return 'today'
|
|
47
|
+
const yesterday = subtractDays(now, 1)
|
|
48
|
+
if (date.toDateString() === yesterday.toDateString()) return 'yesterday'
|
|
49
|
+
const todayAtMidnight = new Date(now.toDateString())
|
|
50
|
+
const last7Days = subtractDays(todayAtMidnight, 7)
|
|
51
|
+
if (date.getTime() >= last7Days.getTime()) return 'last7'
|
|
52
|
+
const last30Days = subtractDays(todayAtMidnight, 30)
|
|
53
|
+
return date.getTime() >= last30Days.getTime() ? 'last30' : 'older'
|
|
16
54
|
}
|
|
55
|
+
|
|
56
|
+
export function HistoryList<T>({ getDate, items, keygen, className, style, renderItem }: Props<T>) {
|
|
57
|
+
const t = useTranslate(dictionary)
|
|
58
|
+
const sections = useMemo(() => {
|
|
59
|
+
const byDate = groupBy(items, item => dateToSectionName(getDate(item)))
|
|
60
|
+
return map(byDate, (value: T[], key: HistorySectionName) => (
|
|
61
|
+
<section key={key}>
|
|
62
|
+
<header>
|
|
63
|
+
<Text>{t[key]}</Text>
|
|
64
|
+
</header>
|
|
65
|
+
<ul>{value.map(item => <li key={keygen(item)}>{renderItem(item)}</li>)}</ul>
|
|
66
|
+
</section>
|
|
67
|
+
))
|
|
68
|
+
}, [items])
|
|
69
|
+
|
|
70
|
+
return <HistoryBox className={className} style={style}>{sections}</HistoryBox>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const dictionary = {
|
|
74
|
+
en: {
|
|
75
|
+
today: 'Today',
|
|
76
|
+
yesterday: 'Yesterday',
|
|
77
|
+
last7: 'Last 7 days',
|
|
78
|
+
last30: 'Last 30 days',
|
|
79
|
+
older: 'Older',
|
|
80
|
+
},
|
|
81
|
+
pt: {
|
|
82
|
+
today: 'Hoje',
|
|
83
|
+
yesterday: 'Ontem',
|
|
84
|
+
last7: 'Últimos 7 dias',
|
|
85
|
+
last30: 'Últimos 30 dias',
|
|
86
|
+
older: 'Mais antigo',
|
|
87
|
+
},
|
|
88
|
+
} satisfies Dictionary
|
|
89
|
+
|
|
@@ -1,10 +1,77 @@
|
|
|
1
1
|
/* eslint-disable no-empty-pattern */
|
|
2
|
-
import {
|
|
2
|
+
import { IconBox, Text } from '@citric/core'
|
|
3
|
+
import { theme, WithStyle } from '@stack-spot/portal-theme'
|
|
4
|
+
import { useMemo } from 'react'
|
|
5
|
+
import { styled } from 'styled-components'
|
|
3
6
|
import { ButtonAction, WithChildren } from '../types'
|
|
7
|
+
import { Tooltip } from './Tooltip'
|
|
8
|
+
import { useTooltip } from './Tooltip/context'
|
|
9
|
+
import { TooltipPosition } from './Tooltip/types'
|
|
4
10
|
|
|
5
11
|
interface Props extends WithStyle, WithChildren {
|
|
6
|
-
position?:
|
|
12
|
+
position?: TooltipPosition,
|
|
7
13
|
actions: ButtonAction[],
|
|
8
14
|
}
|
|
9
15
|
|
|
10
|
-
|
|
16
|
+
const MenuList = styled.ul`
|
|
17
|
+
margin: 0;
|
|
18
|
+
padding: 0;
|
|
19
|
+
list-style: none;
|
|
20
|
+
border-radius: 8px;
|
|
21
|
+
background-color: ${theme.color.light[400]};
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
|
|
26
|
+
> li {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
|
|
30
|
+
> button {
|
|
31
|
+
padding: 8px 12px;
|
|
32
|
+
transition: background-color 0.2s;
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
display: flex;
|
|
35
|
+
flex-direction: row;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 8px;
|
|
38
|
+
background-color: transparent;
|
|
39
|
+
border: none;
|
|
40
|
+
|
|
41
|
+
&:hover {
|
|
42
|
+
background-color: ${theme.color.light[500]};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
`
|
|
47
|
+
|
|
48
|
+
const StyledButton = styled.button<{ $color: string | undefined }>`
|
|
49
|
+
color: ${({ $color }) => $color || theme.color.light.contrastText};
|
|
50
|
+
|
|
51
|
+
svg {
|
|
52
|
+
fill: ${({ $color }) => $color || theme.color.light.contrastText};
|
|
53
|
+
}
|
|
54
|
+
`
|
|
55
|
+
|
|
56
|
+
export const OverlayMenu = ({ actions, children, className, position, style }: Props) => {
|
|
57
|
+
const tooltip = useTooltip()
|
|
58
|
+
const menu = useMemo(() => {
|
|
59
|
+
const items = actions.map(({ label, onClick, className, color, icon, style }) => (
|
|
60
|
+
<li key={label} className={className} style={style}>
|
|
61
|
+
<StyledButton $color={color} onClick={() => {
|
|
62
|
+
onClick()
|
|
63
|
+
tooltip.hide()
|
|
64
|
+
}}>
|
|
65
|
+
<IconBox>{icon}</IconBox>
|
|
66
|
+
<Text>{label}</Text>
|
|
67
|
+
</StyledButton>
|
|
68
|
+
</li>
|
|
69
|
+
))
|
|
70
|
+
return <MenuList>{items}</MenuList>
|
|
71
|
+
}, [actions])
|
|
72
|
+
return (
|
|
73
|
+
<Tooltip content={menu} custom position={position} className={className} style={style} triggeredBy="click">
|
|
74
|
+
{children}
|
|
75
|
+
</Tooltip>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
@@ -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,40 +1,77 @@
|
|
|
1
1
|
import { animationTimeMS } from './style'
|
|
2
|
-
import {
|
|
2
|
+
import { ShowOptions } from './types'
|
|
3
3
|
|
|
4
|
+
const MARGIN_TO_CORNERS_PX = 10
|
|
5
|
+
|
|
6
|
+
function isRelative(element: HTMLElement) {
|
|
7
|
+
return ['relative', 'absolute', 'fixed'].includes(element.computedStyleMap().get('position')?.toString() ?? '')
|
|
8
|
+
}
|
|
4
9
|
|
|
5
10
|
export class TooltipAPI {
|
|
6
11
|
private tooltipRef: React.RefObject<HTMLDivElement>
|
|
7
12
|
private setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>
|
|
8
13
|
private hideTimeoutId: number | undefined
|
|
14
|
+
private clickListener: ((e: MouseEvent) => void) | undefined
|
|
15
|
+
private relativeTo: HTMLElement | undefined
|
|
9
16
|
|
|
10
17
|
constructor(tooltipRef: React.RefObject<HTMLDivElement>, setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>) {
|
|
11
18
|
this.tooltipRef = tooltipRef
|
|
12
19
|
this.setContent = setContent
|
|
13
20
|
}
|
|
14
21
|
|
|
15
|
-
|
|
22
|
+
private computeRelativeTo() {
|
|
23
|
+
if (this.relativeTo) return
|
|
24
|
+
this.relativeTo = this.tooltipRef.current?.parentElement as HTMLElement
|
|
25
|
+
while (this.relativeTo && this.relativeTo !== document.body && !isRelative(this.relativeTo)) {
|
|
26
|
+
this.relativeTo = this.relativeTo.parentElement as HTMLElement
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
show({ content, anchor, position = 'bottom', hideOnClickOutside }: ShowOptions): void {
|
|
16
31
|
window.clearTimeout(this.hideTimeoutId)
|
|
17
32
|
this.hideTimeoutId = undefined
|
|
33
|
+
if (this.clickListener) document.removeEventListener('click', this.clickListener)
|
|
18
34
|
this.setContent(content)
|
|
19
35
|
setTimeout(() => {
|
|
20
36
|
if (!this.tooltipRef.current) return
|
|
21
|
-
const
|
|
37
|
+
const anchorRect = anchor.getClientRects()[0]
|
|
22
38
|
this.tooltipRef.current.classList.add('visible')
|
|
23
39
|
const tooltipWidth = this.tooltipRef.current.clientWidth
|
|
24
40
|
const tooltipHeight = this.tooltipRef.current.clientHeight
|
|
25
41
|
let top = 0
|
|
26
42
|
let left = 0
|
|
27
43
|
if (position === 'left' || position === 'right') {
|
|
28
|
-
top =
|
|
29
|
-
if (position === 'left') left =
|
|
30
|
-
else left =
|
|
44
|
+
top = anchorRect.top + anchorRect.height / 2 - tooltipHeight / 2
|
|
45
|
+
if (position === 'left') left = anchorRect.left - tooltipWidth
|
|
46
|
+
else left = anchorRect.left + anchorRect.width
|
|
31
47
|
} else {
|
|
32
|
-
left =
|
|
33
|
-
if (position === 'top') top =
|
|
34
|
-
else top =
|
|
48
|
+
left = anchorRect.left + anchorRect.width / 2 - tooltipWidth / 2
|
|
49
|
+
if (position === 'top') top = anchorRect.top - tooltipHeight
|
|
50
|
+
else top = anchorRect.top + anchorRect.height
|
|
51
|
+
}
|
|
52
|
+
// takes the parent the tooltip is positioned relative to into consideration
|
|
53
|
+
this.computeRelativeTo()
|
|
54
|
+
const relativeRect = this.relativeTo?.getClientRects()[0] ?? { top: 0, left: 0 }
|
|
55
|
+
top -= relativeRect.top
|
|
56
|
+
left -= relativeRect.left
|
|
57
|
+
// adjusts positions in order to avoid overflowing the window and leaving a margin to the corners
|
|
58
|
+
if (top <= 0) top += MARGIN_TO_CORNERS_PX
|
|
59
|
+
else if (top + tooltipHeight >= document.body.clientHeight - MARGIN_TO_CORNERS_PX) {
|
|
60
|
+
top = document.body.clientHeight - MARGIN_TO_CORNERS_PX + tooltipHeight
|
|
61
|
+
}
|
|
62
|
+
if (left <= 0) left += MARGIN_TO_CORNERS_PX
|
|
63
|
+
else if (left + tooltipWidth >= document.body.clientWidth - MARGIN_TO_CORNERS_PX) {
|
|
64
|
+
left = document.body.clientWidth - MARGIN_TO_CORNERS_PX - tooltipWidth
|
|
35
65
|
}
|
|
36
66
|
this.tooltipRef.current.style.top = `${top}px`
|
|
37
67
|
this.tooltipRef.current.style.left = `${left}px`
|
|
68
|
+
if (hideOnClickOutside) {
|
|
69
|
+
this.clickListener = (e: MouseEvent) => {
|
|
70
|
+
if (this.tooltipRef.current?.contains(e.target as HTMLElement)) return
|
|
71
|
+
this.hide()
|
|
72
|
+
}
|
|
73
|
+
document.addEventListener('click', this.clickListener)
|
|
74
|
+
}
|
|
38
75
|
}, 10)
|
|
39
76
|
}
|
|
40
77
|
|
|
@@ -42,5 +79,6 @@ export class TooltipAPI {
|
|
|
42
79
|
if (!this.tooltipRef.current) return
|
|
43
80
|
this.tooltipRef.current.classList.remove('visible')
|
|
44
81
|
this.hideTimeoutId = window.setTimeout(() => this.setContent(undefined), animationTimeMS)
|
|
82
|
+
if (this.clickListener) document.removeEventListener('click', this.clickListener)
|
|
45
83
|
}
|
|
46
84
|
}
|
|
@@ -12,7 +12,7 @@ export const TooltipProvider = ({ children }: Required<WithChildren>) => {
|
|
|
12
12
|
return (
|
|
13
13
|
<Context.Provider value={api}>
|
|
14
14
|
{children}
|
|
15
|
-
<TooltipBox ref={ref}
|
|
15
|
+
<TooltipBox ref={ref}>{content}</TooltipBox>
|
|
16
16
|
</Context.Provider>
|
|
17
17
|
)
|
|
18
18
|
}
|
package/src/features.ts
CHANGED
package/src/layout.css
CHANGED
package/src/state/ChatEntry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { pull } from 'lodash'
|
|
2
|
+
import { LabeledWithImage } from './types'
|
|
2
3
|
|
|
3
4
|
export interface SerializableAction {
|
|
4
5
|
title: string,
|
|
@@ -25,14 +26,14 @@ export interface KnowledgeSource {
|
|
|
25
26
|
|
|
26
27
|
export interface TextChatEntry {
|
|
27
28
|
type: 'text' | 'md',
|
|
28
|
-
|
|
29
|
+
agentType: 'bot' | 'user' | 'system',
|
|
29
30
|
// image?: string,
|
|
30
31
|
actions?: ChatAction[],
|
|
31
32
|
subtitle?: string,
|
|
32
33
|
content: string,
|
|
33
34
|
knowledgeSources?: KnowledgeSource[],
|
|
34
35
|
updated?: string,
|
|
35
|
-
|
|
36
|
+
agent?: LabeledWithImage,
|
|
36
37
|
messageId?: string,
|
|
37
38
|
error?: string,
|
|
38
39
|
// customInput?: CustomInputResponse,
|
|
@@ -63,7 +64,7 @@ export class ChatEntry {
|
|
|
63
64
|
|
|
64
65
|
static createUserEntry(content: string) {
|
|
65
66
|
return new ChatEntry({
|
|
66
|
-
|
|
67
|
+
agentType: 'user',
|
|
67
68
|
type: 'text',
|
|
68
69
|
content,
|
|
69
70
|
updated: new Date().toISOString(),
|
|
@@ -71,7 +72,7 @@ export class ChatEntry {
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
static createStreamedBotEntry(abort: () => void) {
|
|
74
|
-
return new ChatEntry({
|
|
75
|
+
return new ChatEntry({ agentType: 'bot', type: 'md', content: '' }, true, abort)
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
setValue(value: TextChatEntry) {
|
package/src/state/ChatState.ts
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
import { dropRight, last, pull } from 'lodash'
|
|
2
2
|
import { ChatEntry } from './ChatEntry'
|
|
3
3
|
import { ObservableState } from './ObservableState'
|
|
4
|
-
|
|
5
|
-
interface Labeled {
|
|
6
|
-
id: string,
|
|
7
|
-
label: string,
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface LabeledWithImage extends Labeled {
|
|
11
|
-
image?: string,
|
|
12
|
-
}
|
|
4
|
+
import { Labeled, LabeledWithImage } from './types'
|
|
13
5
|
|
|
14
6
|
export interface ChatProperties {
|
|
15
7
|
label: string,
|
package/src/types.ts
CHANGED
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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { DocumentResponse } from '@stack-spot/portal-network/api/ai'
|
|
1
|
+
import { DocumentResponse, SourceKnowledgeSource4, SourceProjectFile4, SourceStackAi } from '@stack-spot/portal-network/api/ai'
|
|
2
|
+
import { KnowledgeSource } from '../state/ChatEntry'
|
|
2
3
|
|
|
3
4
|
function attemptToParseMalFormedJson(str: string) {
|
|
4
5
|
try {
|
|
@@ -41,3 +42,14 @@ export function extractCodeFromKSDocument(document: DocumentResponse): { languag
|
|
|
41
42
|
|
|
42
43
|
return typeof document === 'object' ? { language: 'json', snippet: JSON.stringify(document, null, 2) } : { snippet: String(document) }
|
|
43
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
|
@@ -83,8 +83,9 @@ export const Agents = () => {
|
|
|
83
83
|
|
|
84
84
|
const AgentsPanel = () => {
|
|
85
85
|
const t = useTranslate(dictionary)
|
|
86
|
+
const chat = useCurrentChat()
|
|
86
87
|
|
|
87
|
-
return <RightPanelTabs tabs={[
|
|
88
|
+
return <RightPanelTabs key={chat.id} tabs={[
|
|
88
89
|
{ title: t.personal, content: <AgentsTab key="personal" visibility="PERSONAL" /> },
|
|
89
90
|
{ title: t.builtin, content: <AgentsTab key="builtin" visibility="BUILT-IN" /> },
|
|
90
91
|
{ title: t.shared, content: <AgentsTab key="shared" visibility="SHARED" /> },
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { Text } from '@citric/core'
|
|
2
2
|
import { MiniLogo } from '@stack-spot/portal-components/svg'
|
|
3
|
+
import { LabeledWithImage } from '../../state/types'
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
|
-
|
|
6
|
+
agent?: LabeledWithImage,
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
10
|
-
export const AgentInfo = ({ agentId: _agentId }: Props) => (
|
|
9
|
+
export const AgentInfo = ({ agent }: Props) => (
|
|
11
10
|
<>
|
|
12
|
-
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
{agent?.image
|
|
12
|
+
? <img src={agent.image} className="custom-agent-image" />
|
|
13
|
+
: <div className="default-image-wrapper"><MiniLogo className="agent-image" /></div>
|
|
14
|
+
}
|
|
15
|
+
<Text appearance="body2">{agent?.label || 'Stackspot AI'}</Text>
|
|
16
16
|
</>
|
|
17
17
|
)
|
|
@@ -6,17 +6,17 @@ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
|
6
6
|
import { useCallback, useMemo, useRef, useState } from 'react'
|
|
7
7
|
import { Markdown } from '../../components/Markdown'
|
|
8
8
|
import { useChatEntry, useWidget } from '../../context/hooks'
|
|
9
|
-
import { useChatScrollToBottomEffect } from '../../hooks/chat-scroll'
|
|
10
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)
|
|
16
16
|
const [liked, setLiked] = useState<boolean | undefined>()
|
|
17
17
|
const entry = useChatEntry(message)
|
|
18
18
|
const dateFormatter = useDateFormatter()
|
|
19
|
-
const userInfo = entry.
|
|
19
|
+
const userInfo = entry.agentType === 'user' ? <Avatar size="xs">{username}</Avatar> : <AgentInfo agent={entry.agent} />
|
|
20
20
|
const date = new Date(entry.updated ?? '')
|
|
21
21
|
const shouldShowDate = entry.updated && !isNaN(date.getTime())
|
|
22
22
|
const ref = useRef<HTMLLIElement>(null)
|
|
@@ -55,7 +55,7 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
|
|
|
55
55
|
}, [entry.messageId, liked])
|
|
56
56
|
|
|
57
57
|
return (entry.content || entry.error) && (
|
|
58
|
-
<li className={entry.
|
|
58
|
+
<li className={entry.agentType} ref={ref}>
|
|
59
59
|
<div className="chat-message">
|
|
60
60
|
<div className="user-info">{userInfo}</div>
|
|
61
61
|
{entry.content && <div className="message-content">
|
|
@@ -77,7 +77,7 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
|
|
|
77
77
|
))}</ul>
|
|
78
78
|
</div>}
|
|
79
79
|
<div className="message-footer">
|
|
80
|
-
{entry.
|
|
80
|
+
{entry.agentType === 'bot' && entry.messageId && !entry.error && <div className="message-actions">
|
|
81
81
|
<IconButton title={t.like} aria-label={t.like} onClick={like}>
|
|
82
82
|
{liked === true ? <LikeFill /> : <Like />}
|
|
83
83
|
</IconButton>
|
|
@@ -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
|
}
|
package/src/views/Chat/styled.ts
CHANGED
|
@@ -76,7 +76,7 @@ export const ChatList = styled.ul`
|
|
|
76
76
|
flex-direction: column;
|
|
77
77
|
gap: 4px;
|
|
78
78
|
|
|
79
|
-
.
|
|
79
|
+
.default-image-wrapper {
|
|
80
80
|
width: 24px;
|
|
81
81
|
height: 24px;
|
|
82
82
|
border-radius: 4px;
|
|
@@ -90,6 +90,12 @@ export const ChatList = styled.ul`
|
|
|
90
90
|
height: 18px;
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
+
|
|
94
|
+
.custom-agent-image {
|
|
95
|
+
width: 24px;
|
|
96
|
+
height: 24px;
|
|
97
|
+
border-radius: 50%;
|
|
98
|
+
}
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
|
|
@@ -100,6 +106,7 @@ export const ChatList = styled.ul`
|
|
|
100
106
|
display: flex;
|
|
101
107
|
flex-direction: row;
|
|
102
108
|
gap: 8px;
|
|
109
|
+
align-items: center;
|
|
103
110
|
|
|
104
111
|
.message-content {
|
|
105
112
|
padding: 10px;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { aiClient } from '@stack-spot/portal-network'
|
|
2
|
+
import InfiniteScroll from 'react-infinite-scroll-component'
|
|
3
|
+
import { HistoryList } from '../../components/HistoryList'
|
|
4
|
+
import { MessageInterceptor } from '../../state/ChatState'
|
|
5
|
+
import { HistoryItem } from './HistoryItem'
|
|
6
|
+
|
|
7
|
+
export const ChatHistoryPanel = ({ interceptors }: { interceptors: MessageInterceptor[] }) => {
|
|
8
|
+
const [chats, { fetchNextPage, hasNextPage }] = aiClient.chats.useInfiniteQuery({ size: 40 })
|
|
9
|
+
return (
|
|
10
|
+
<div id="chatHistoryList" style={{ height: '100%', overflow: 'auto' }}>
|
|
11
|
+
<InfiniteScroll
|
|
12
|
+
scrollableTarget="chatHistoryList"
|
|
13
|
+
dataLength={chats.length}
|
|
14
|
+
next={fetchNextPage}
|
|
15
|
+
hasMore={hasNextPage}
|
|
16
|
+
loader={<div></div>}
|
|
17
|
+
>
|
|
18
|
+
<HistoryList
|
|
19
|
+
items={chats}
|
|
20
|
+
getDate={c => new Date(c.updated || c.created || '')}
|
|
21
|
+
keygen={c => c.id}
|
|
22
|
+
renderItem={c => <HistoryItem item={c} interceptors={interceptors} />}
|
|
23
|
+
style={{ marginRight: '6px' }}
|
|
24
|
+
/>
|
|
25
|
+
</InfiniteScroll>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|