@stack-spot/ai-chat-widget 0.11.0 → 1.0.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/chat-interceptors/quick-commands.d.ts.map +1 -1
- package/dist/chat-interceptors/quick-commands.js +23 -22
- 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 +14 -10
- package/dist/chat-interceptors/send-message.js.map +1 -1
- package/dist/components/AdaptiveTextArea.d.ts +1 -1
- package/dist/components/AdaptiveTextArea.d.ts.map +1 -1
- package/dist/components/AdaptiveTextArea.js +6 -4
- package/dist/components/AdaptiveTextArea.js.map +1 -1
- package/dist/components/AutoFocus.d.ts +6 -0
- package/dist/components/AutoFocus.d.ts.map +1 -0
- package/dist/components/AutoFocus.js +15 -0
- package/dist/components/AutoFocus.js.map +1 -0
- package/dist/components/Fading.d.ts +15 -0
- package/dist/components/Fading.d.ts.map +1 -0
- package/dist/components/Fading.js +31 -0
- package/dist/components/Fading.js.map +1 -0
- package/dist/components/FallbackBoundary/ErrorBoundary.d.ts +3 -0
- package/dist/components/FallbackBoundary/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/FallbackBoundary/ErrorBoundary.js +18 -4
- package/dist/components/FallbackBoundary/ErrorBoundary.js.map +1 -1
- package/dist/components/FallbackBoundary/Loading.js +1 -1
- package/dist/components/FallbackBoundary/Loading.js.map +1 -1
- package/dist/components/FallbackBoundary/index.d.ts +6 -1
- package/dist/components/FallbackBoundary/index.d.ts.map +1 -1
- package/dist/components/FallbackBoundary/index.js +1 -1
- package/dist/components/FallbackBoundary/index.js.map +1 -1
- package/dist/components/OverlayMenu.d.ts +1 -1
- package/dist/components/OverlayMenu.d.ts.map +1 -1
- package/dist/components/OverlayMenu.js +26 -9
- package/dist/components/OverlayMenu.js.map +1 -1
- package/dist/components/RightPanelForm.d.ts.map +1 -1
- package/dist/components/RightPanelForm.js +5 -4
- package/dist/components/RightPanelForm.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.d.ts +3 -1
- package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/components/Tooltip/Tooltip.js +14 -5
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tooltip/TooltipAPI.d.ts +2 -2
- package/dist/components/Tooltip/TooltipAPI.d.ts.map +1 -1
- package/dist/components/Tooltip/TooltipAPI.js +51 -51
- package/dist/components/Tooltip/TooltipAPI.js.map +1 -1
- package/dist/layout.css +5 -0
- package/dist/regex.d.ts +2 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +2 -0
- package/dist/regex.js.map +1 -0
- package/dist/right-panel/DefaultPanel.d.ts.map +1 -1
- package/dist/right-panel/DefaultPanel.js +3 -1
- package/dist/right-panel/DefaultPanel.js.map +1 -1
- package/dist/right-panel/constants.d.ts +2 -0
- package/dist/right-panel/constants.d.ts.map +1 -0
- package/dist/right-panel/constants.js +2 -0
- package/dist/right-panel/constants.js.map +1 -0
- package/dist/right-panel/hooks.d.ts.map +1 -1
- package/dist/right-panel/hooks.js +2 -1
- package/dist/right-panel/hooks.js.map +1 -1
- package/dist/utils/chat.js +1 -1
- package/dist/utils/url.d.ts +2 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +8 -0
- package/dist/utils/url.js.map +1 -0
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +24 -2
- package/dist/views/Chat/ChatMessage.js.map +1 -1
- package/dist/views/ChatHistory/ChatHistoryPanel.d.ts.map +1 -1
- package/dist/views/ChatHistory/ChatHistoryPanel.js +2 -1
- package/dist/views/ChatHistory/ChatHistoryPanel.js.map +1 -1
- package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -1
- package/dist/views/ChatHistory/HistoryItem.js +10 -1
- package/dist/views/ChatHistory/HistoryItem.js.map +1 -1
- package/dist/views/MessageInput/QuickCommandSelector.d.ts +6 -0
- package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -0
- package/dist/views/MessageInput/QuickCommandSelector.js +137 -0
- package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -0
- package/dist/views/MessageInput/index.d.ts.map +1 -1
- package/dist/views/MessageInput/index.js +6 -4
- package/dist/views/MessageInput/index.js.map +1 -1
- package/dist/views/MessageInput/styled.d.ts.map +1 -1
- package/dist/views/MessageInput/styled.js +137 -0
- package/dist/views/MessageInput/styled.js.map +1 -1
- package/package.json +2 -2
- package/src/chat-interceptors/quick-commands.ts +24 -23
- package/src/chat-interceptors/send-message.ts +23 -11
- package/src/components/AdaptiveTextArea.tsx +9 -4
- package/src/components/AutoFocus.tsx +20 -0
- package/src/components/Fading.tsx +46 -0
- package/src/components/FallbackBoundary/ErrorBoundary.tsx +26 -3
- package/src/components/FallbackBoundary/Loading.tsx +1 -1
- package/src/components/FallbackBoundary/index.tsx +7 -2
- package/src/components/OverlayMenu.tsx +59 -19
- package/src/components/RightPanelForm.tsx +12 -9
- package/src/components/Tooltip/Tooltip.tsx +24 -5
- package/src/components/Tooltip/TooltipAPI.ts +42 -42
- package/src/layout.css +5 -0
- package/src/regex.ts +1 -0
- package/src/right-panel/DefaultPanel.tsx +14 -9
- package/src/right-panel/constants.ts +1 -0
- package/src/right-panel/hooks.tsx +2 -1
- package/src/utils/chat.ts +1 -1
- package/src/utils/url.ts +8 -0
- package/src/views/Chat/ChatMessage.tsx +29 -5
- package/src/views/ChatHistory/ChatHistoryPanel.tsx +3 -2
- package/src/views/ChatHistory/HistoryItem.tsx +11 -2
- package/src/views/MessageInput/QuickCommandSelector.tsx +210 -0
- package/src/views/MessageInput/index.tsx +8 -4
- package/src/views/MessageInput/styled.ts +137 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* eslint-disable react/display-name */
|
|
2
|
+
import { WithStyle } from '@stack-spot/portal-theme'
|
|
3
|
+
import { forwardRef, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { WithChildren } from '../types'
|
|
5
|
+
|
|
6
|
+
interface Props extends WithChildren, WithStyle {
|
|
7
|
+
visible: boolean,
|
|
8
|
+
/**
|
|
9
|
+
* Duration of the animation in ms.
|
|
10
|
+
* @default 300
|
|
11
|
+
*/
|
|
12
|
+
duration?: number,
|
|
13
|
+
onFadeIn?: () => void,
|
|
14
|
+
onFadeOut?: () => void,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const Fading = forwardRef<HTMLDivElement, Props>((
|
|
18
|
+
{ visible, children, duration = 300, onFadeIn, onFadeOut, className, style },
|
|
19
|
+
ref,
|
|
20
|
+
) => {
|
|
21
|
+
const [isOpaque, setOpaque] = useState(visible)
|
|
22
|
+
const [isRendered, setRendered] = useState(visible)
|
|
23
|
+
const previous = useRef(visible)
|
|
24
|
+
const timeout = useRef<number[]>([])
|
|
25
|
+
const opacity: React.CSSProperties = { transition: `opacity ${duration / 1000}s`, opacity: isOpaque ? 1 : 0 }
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (previous.current === visible) return
|
|
29
|
+
timeout.current.forEach(window.clearTimeout)
|
|
30
|
+
timeout.current = []
|
|
31
|
+
if (visible) {
|
|
32
|
+
setRendered(true)
|
|
33
|
+
timeout.current.push(window.setTimeout(() => setOpaque(true), 0))
|
|
34
|
+
if (onFadeIn) timeout.current.push(window.setTimeout(onFadeIn, duration))
|
|
35
|
+
} else {
|
|
36
|
+
setOpaque(false)
|
|
37
|
+
timeout.current.push(window.setTimeout(() => {
|
|
38
|
+
setRendered(false)
|
|
39
|
+
onFadeOut?.()
|
|
40
|
+
}, duration))
|
|
41
|
+
}
|
|
42
|
+
previous.current = visible
|
|
43
|
+
}, [visible])
|
|
44
|
+
|
|
45
|
+
return isRendered ? <div ref={ref} className={className} style={{ ...style, ...opacity }}>{children}</div> : null
|
|
46
|
+
})
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
+
import { IconBox, Text } from '@citric/core'
|
|
2
|
+
import { TimesCircle } from '@citric/icons'
|
|
1
3
|
import { ErrorDescription, ErrorFeedback } from '@stack-spot/portal-components/error'
|
|
2
4
|
import { StackspotAPIError } from '@stack-spot/portal-network'
|
|
3
5
|
import { Component } from 'react'
|
|
6
|
+
import { styled } from 'styled-components'
|
|
4
7
|
|
|
5
8
|
interface State extends ErrorDescription {
|
|
6
9
|
hasError: boolean,
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
interface Props {
|
|
13
|
+
mini?: boolean,
|
|
14
|
+
message?: string,
|
|
10
15
|
children: React.ReactNode,
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
const ErrorBox = styled.div`
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 100%;
|
|
21
|
+
display: flex;
|
|
22
|
+
flex-direction: column;
|
|
23
|
+
align-items: center;
|
|
24
|
+
justify-content: center;
|
|
25
|
+
`
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* An Error Boundary that renders an ErrorFeedback instead of its content if any of its children throws.
|
|
15
29
|
*
|
|
@@ -40,9 +54,18 @@ export class ErrorBoundary extends Component<Props, State> {
|
|
|
40
54
|
if (this.props.children !== prevProps.children) this.setState({ hasError: false })
|
|
41
55
|
}
|
|
42
56
|
|
|
57
|
+
private renderError() {
|
|
58
|
+
return this.props.mini
|
|
59
|
+
? (
|
|
60
|
+
<ErrorBox className="error">
|
|
61
|
+
<IconBox size="lg" colorIcon="danger.500"><TimesCircle /></IconBox>
|
|
62
|
+
<Text colorScheme="light.700">{this.props.message || this.state.message}</Text>
|
|
63
|
+
</ErrorBox>
|
|
64
|
+
)
|
|
65
|
+
: <ErrorFeedback className="error" code={this.state.code} message={this.props.message || this.state.message} />
|
|
66
|
+
}
|
|
67
|
+
|
|
43
68
|
render() {
|
|
44
|
-
return this.state.hasError
|
|
45
|
-
? <ErrorFeedback code={this.state.code} message={this.state.message} />
|
|
46
|
-
: this.props.children
|
|
69
|
+
return this.state.hasError ? this.renderError() : this.props.children
|
|
47
70
|
}
|
|
48
71
|
}
|
|
@@ -3,11 +3,16 @@ import { WithChildren } from '../../types'
|
|
|
3
3
|
import { ErrorBoundary } from './ErrorBoundary'
|
|
4
4
|
import { Loading } from './Loading'
|
|
5
5
|
|
|
6
|
+
interface Props extends WithChildren {
|
|
7
|
+
mini?: boolean,
|
|
8
|
+
message?: string,
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
/**
|
|
7
12
|
* Fallbacks for errors and loadings (suspense).
|
|
8
13
|
*/
|
|
9
|
-
export const FallbackBoundary = ({ children }:
|
|
10
|
-
<ErrorBoundary>
|
|
14
|
+
export const FallbackBoundary = ({ children, mini, message }: Props) => (
|
|
15
|
+
<ErrorBoundary mini={mini} message={message}>
|
|
11
16
|
<Suspense fallback={<Loading />}>
|
|
12
17
|
{children}
|
|
13
18
|
</Suspense>
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
/* eslint-disable react/display-name */
|
|
1
2
|
/* eslint-disable no-empty-pattern */
|
|
2
3
|
import { IconBox, Text } from '@citric/core'
|
|
4
|
+
import { useKeyboardControls } from '@stack-spot/portal-components'
|
|
3
5
|
import { theme, WithStyle } from '@stack-spot/portal-theme'
|
|
4
|
-
import {
|
|
6
|
+
import { forwardRef, RefObject, useCallback, useRef } from 'react'
|
|
5
7
|
import { styled } from 'styled-components'
|
|
6
8
|
import { ButtonAction, WithChildren } from '../types'
|
|
7
9
|
import { Tooltip } from './Tooltip'
|
|
@@ -13,6 +15,11 @@ interface Props extends WithStyle, WithChildren {
|
|
|
13
15
|
actions: ButtonAction[],
|
|
14
16
|
}
|
|
15
17
|
|
|
18
|
+
interface MenuProps {
|
|
19
|
+
actions: ButtonAction[],
|
|
20
|
+
trigger: RefObject<HTMLDivElement>,
|
|
21
|
+
}
|
|
22
|
+
|
|
16
23
|
const MenuList = styled.ul`
|
|
17
24
|
margin: 0;
|
|
18
25
|
padding: 0;
|
|
@@ -37,8 +44,9 @@ const MenuList = styled.ul`
|
|
|
37
44
|
gap: 8px;
|
|
38
45
|
background-color: transparent;
|
|
39
46
|
border: none;
|
|
47
|
+
outline: none;
|
|
40
48
|
|
|
41
|
-
&:hover {
|
|
49
|
+
&:hover, &:focus {
|
|
42
50
|
background-color: ${theme.color.light[500]};
|
|
43
51
|
}
|
|
44
52
|
}
|
|
@@ -53,25 +61,57 @@ const StyledButton = styled.button<{ $color: string | undefined }>`
|
|
|
53
61
|
}
|
|
54
62
|
`
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
const Menu = ({ actions, trigger }: MenuProps) => {
|
|
57
65
|
const tooltip = useTooltip()
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
))
|
|
70
|
-
return <MenuList>{items}</MenuList>
|
|
71
|
-
}, [actions])
|
|
66
|
+
const ref = useRef<HTMLUListElement>(null)
|
|
67
|
+
|
|
68
|
+
useKeyboardControls({
|
|
69
|
+
querySelectors: 'button',
|
|
70
|
+
onPressEscape: () => {
|
|
71
|
+
tooltip.hide()
|
|
72
|
+
trigger.current?.focus()
|
|
73
|
+
},
|
|
74
|
+
ref,
|
|
75
|
+
}, [])
|
|
76
|
+
|
|
72
77
|
return (
|
|
73
|
-
<
|
|
78
|
+
<MenuList ref={ref}>
|
|
79
|
+
{actions.map(({ label, onClick, className, color, icon, style }) => (
|
|
80
|
+
<li key={label} className={className} style={style}>
|
|
81
|
+
<StyledButton $color={color} onClick={() => {
|
|
82
|
+
onClick()
|
|
83
|
+
tooltip.hide()
|
|
84
|
+
}}>
|
|
85
|
+
<IconBox>{icon}</IconBox>
|
|
86
|
+
<Text>{label}</Text>
|
|
87
|
+
</StyledButton>
|
|
88
|
+
</li>
|
|
89
|
+
))}
|
|
90
|
+
</MenuList>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const OverlayMenu = forwardRef<HTMLDivElement, Props>(({ actions, children, className, position, style }, externalRef) => {
|
|
95
|
+
const localRef = useRef<HTMLDivElement>(null)
|
|
96
|
+
const ref = externalRef as RefObject<HTMLDivElement> ?? localRef
|
|
97
|
+
const tooltip = useTooltip()
|
|
98
|
+
|
|
99
|
+
const onShow = useCallback(() => {
|
|
100
|
+
tooltip.tooltipRef.current?.querySelector('button')?.focus()
|
|
101
|
+
}, [])
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<Tooltip
|
|
105
|
+
content={<Menu actions={actions} trigger={ref} />}
|
|
106
|
+
custom
|
|
107
|
+
position={position}
|
|
108
|
+
className={className}
|
|
109
|
+
style={style}
|
|
110
|
+
triggeredBy="click"
|
|
111
|
+
onShow={onShow}
|
|
112
|
+
ref={ref}
|
|
113
|
+
>
|
|
74
114
|
{children}
|
|
75
115
|
</Tooltip>
|
|
76
116
|
)
|
|
77
|
-
}
|
|
117
|
+
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Card } from '@citric/ui'
|
|
2
2
|
import { styled } from 'styled-components'
|
|
3
3
|
import { PropsOf } from '../types'
|
|
4
|
+
import { AutoFocus } from './AutoFocus'
|
|
4
5
|
import { FallbackBoundary } from './FallbackBoundary'
|
|
5
6
|
|
|
6
7
|
const Form = styled.form`
|
|
@@ -42,14 +43,16 @@ const Form = styled.form`
|
|
|
42
43
|
|
|
43
44
|
export const RightPanelForm = ({ children, onSubmit, ...props }: PropsOf<typeof Form>) => (
|
|
44
45
|
<FallbackBoundary>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
e
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
<AutoFocus>
|
|
47
|
+
<Form
|
|
48
|
+
{...props}
|
|
49
|
+
onSubmit={(e) => {
|
|
50
|
+
e.preventDefault()
|
|
51
|
+
onSubmit?.(e)
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
{children}
|
|
55
|
+
</Form>
|
|
56
|
+
</AutoFocus>
|
|
54
57
|
</FallbackBoundary>
|
|
55
58
|
)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
/* eslint-disable react/display-name */
|
|
1
2
|
import { Text } from '@citric/core'
|
|
2
3
|
import { WithStyle } from '@stack-spot/portal-theme'
|
|
4
|
+
import { forwardRef } from 'react'
|
|
3
5
|
import { WithChildren } from '../../types'
|
|
4
6
|
import { useTooltip } from './context'
|
|
5
7
|
import { DefaultTooltip } from './style'
|
|
@@ -10,27 +12,44 @@ interface Props extends WithChildren, WithStyle {
|
|
|
10
12
|
position?: TooltipPosition,
|
|
11
13
|
triggeredBy?: 'click' | 'hover',
|
|
12
14
|
custom?: boolean,
|
|
15
|
+
onShow?: () => void,
|
|
16
|
+
onHide?: () => void,
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
export const Tooltip =
|
|
19
|
+
export const Tooltip = forwardRef<HTMLDivElement, Props>((
|
|
20
|
+
{ content, custom, position, triggeredBy = 'hover', onHide, onShow, children, className, style },
|
|
21
|
+
ref,
|
|
22
|
+
) => {
|
|
16
23
|
const api = useTooltip()
|
|
17
24
|
|
|
18
|
-
function show(e: React.
|
|
19
|
-
api.show({
|
|
25
|
+
async function show(e: React.UIEvent, hideOnClickOutside?: boolean) {
|
|
26
|
+
await api.show({
|
|
20
27
|
content: custom ? content : <DefaultTooltip><Text appearance="microtext1">{content}</Text></DefaultTooltip>,
|
|
21
28
|
anchor: e.target as HTMLElement,
|
|
22
29
|
position,
|
|
23
30
|
hideOnClickOutside,
|
|
24
31
|
})
|
|
32
|
+
onShow?.()
|
|
25
33
|
}
|
|
34
|
+
|
|
35
|
+
function hide() {
|
|
36
|
+
api.hide()
|
|
37
|
+
onHide?.()
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
return (
|
|
27
41
|
<div
|
|
28
|
-
{...(triggeredBy === 'hover'
|
|
42
|
+
{...(triggeredBy === 'hover'
|
|
43
|
+
? { onMouseEnter: show, onMouseLeave: hide }
|
|
44
|
+
: { onClick: (e) => show(e, true), onKeyDown: (e) => e.key === 'Enter' && show(e, true) }
|
|
45
|
+
)}
|
|
29
46
|
className={className}
|
|
30
47
|
style={style}
|
|
31
48
|
tabIndex={triggeredBy === 'click' ? 0 : undefined}
|
|
49
|
+
role={triggeredBy === 'click' ? 'button' : undefined}
|
|
50
|
+
ref={ref}
|
|
32
51
|
>
|
|
33
52
|
{children}
|
|
34
53
|
</div>
|
|
35
54
|
)
|
|
36
|
-
}
|
|
55
|
+
})
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { delay } from '@stack-spot/portal-components'
|
|
1
2
|
import { animationTimeMS } from './style'
|
|
2
3
|
import { ShowOptions } from './types'
|
|
3
4
|
|
|
@@ -8,7 +9,7 @@ function isRelative(element: HTMLElement) {
|
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export class TooltipAPI {
|
|
11
|
-
|
|
12
|
+
tooltipRef: React.RefObject<HTMLDivElement>
|
|
12
13
|
private setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>
|
|
13
14
|
private hideTimeoutId: number | undefined
|
|
14
15
|
private clickListener: ((e: MouseEvent) => void) | undefined
|
|
@@ -27,52 +28,51 @@ export class TooltipAPI {
|
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
show({ content, anchor, position = 'bottom', hideOnClickOutside }: ShowOptions)
|
|
31
|
+
async show({ content, anchor, position = 'bottom', hideOnClickOutside }: ShowOptions) {
|
|
31
32
|
window.clearTimeout(this.hideTimeoutId)
|
|
32
33
|
this.hideTimeoutId = undefined
|
|
33
34
|
if (this.clickListener) document.removeEventListener('click', this.clickListener)
|
|
34
35
|
this.setContent(content)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
document.addEventListener('click', this.clickListener)
|
|
36
|
+
await delay(10)
|
|
37
|
+
if (!this.tooltipRef.current) return
|
|
38
|
+
const anchorRect = anchor.getClientRects()[0]
|
|
39
|
+
this.tooltipRef.current.classList.add('visible')
|
|
40
|
+
const tooltipWidth = this.tooltipRef.current.clientWidth
|
|
41
|
+
const tooltipHeight = this.tooltipRef.current.clientHeight
|
|
42
|
+
let top = 0
|
|
43
|
+
let left = 0
|
|
44
|
+
if (position === 'left' || position === 'right') {
|
|
45
|
+
top = anchorRect.top + anchorRect.height / 2 - tooltipHeight / 2
|
|
46
|
+
if (position === 'left') left = anchorRect.left - tooltipWidth
|
|
47
|
+
else left = anchorRect.left + anchorRect.width
|
|
48
|
+
} else {
|
|
49
|
+
left = anchorRect.left + anchorRect.width / 2 - tooltipWidth / 2
|
|
50
|
+
if (position === 'top') top = anchorRect.top - tooltipHeight
|
|
51
|
+
else top = anchorRect.top + anchorRect.height
|
|
52
|
+
}
|
|
53
|
+
// takes the parent the tooltip is positioned relative to into consideration
|
|
54
|
+
this.computeRelativeTo()
|
|
55
|
+
const relativeRect = this.relativeTo?.getClientRects()[0] ?? { top: 0, left: 0 }
|
|
56
|
+
top -= relativeRect.top
|
|
57
|
+
left -= relativeRect.left
|
|
58
|
+
// adjusts positions in order to avoid overflowing the window and leaving a margin to the corners
|
|
59
|
+
if (top <= 0) top += MARGIN_TO_CORNERS_PX
|
|
60
|
+
else if (top + tooltipHeight >= document.body.clientHeight - MARGIN_TO_CORNERS_PX) {
|
|
61
|
+
top = document.body.clientHeight - MARGIN_TO_CORNERS_PX + tooltipHeight
|
|
62
|
+
}
|
|
63
|
+
if (left <= 0) left += MARGIN_TO_CORNERS_PX
|
|
64
|
+
else if (left + tooltipWidth >= document.body.clientWidth - MARGIN_TO_CORNERS_PX) {
|
|
65
|
+
left = document.body.clientWidth - MARGIN_TO_CORNERS_PX - tooltipWidth
|
|
66
|
+
}
|
|
67
|
+
this.tooltipRef.current.style.top = `${top}px`
|
|
68
|
+
this.tooltipRef.current.style.left = `${left}px`
|
|
69
|
+
if (hideOnClickOutside) {
|
|
70
|
+
this.clickListener = (e: MouseEvent) => {
|
|
71
|
+
if (this.tooltipRef.current?.contains(e.target as HTMLElement)) return
|
|
72
|
+
this.hide()
|
|
74
73
|
}
|
|
75
|
-
|
|
74
|
+
document.addEventListener('click', this.clickListener)
|
|
75
|
+
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
hide(): void {
|
package/src/layout.css
CHANGED
package/src/regex.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const quickCommandRegex = /^\/[\w\d-_]+$/
|
|
@@ -3,7 +3,9 @@ import { Times } from '@citric/icons'
|
|
|
3
3
|
import { IconButton } from '@citric/ui'
|
|
4
4
|
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
5
5
|
import { styled } from 'styled-components'
|
|
6
|
+
import { AutoFocus } from '../components/AutoFocus'
|
|
6
7
|
import { WithChildren } from '../types'
|
|
8
|
+
import { panelAnimationTime } from './constants'
|
|
7
9
|
|
|
8
10
|
interface Props extends WithChildren {
|
|
9
11
|
title: React.ReactNode,
|
|
@@ -42,17 +44,20 @@ const PanelBox = styled.div`
|
|
|
42
44
|
|
|
43
45
|
export const DefaultPanel = ({ description, onClose, title, children }: Props) => {
|
|
44
46
|
const t = useTranslate(dictionary)
|
|
47
|
+
|
|
45
48
|
return (
|
|
46
49
|
<PanelBox>
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
<AutoFocus delay={panelAnimationTime}>
|
|
51
|
+
<header>
|
|
52
|
+
<div className="title">
|
|
53
|
+
{typeof title === 'string' ? <Text appearance="h5">{title}</Text> : title}
|
|
54
|
+
{typeof description === 'string' ? <Text colorScheme="light.700">{description}</Text> : description}
|
|
55
|
+
</div>
|
|
56
|
+
<IconButton title={t.close} aria-label={t.close} onClick={onClose}>
|
|
57
|
+
<Times />
|
|
58
|
+
</IconButton>
|
|
59
|
+
</header>
|
|
60
|
+
</AutoFocus>
|
|
56
61
|
<article>{children}</article>
|
|
57
62
|
</PanelBox>
|
|
58
63
|
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const panelAnimationTime = 300
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useContext, useMemo } from 'react'
|
|
2
2
|
import { DefaultPanel } from './DefaultPanel'
|
|
3
3
|
import { RightPanelContext } from './RightPanelProvider'
|
|
4
|
+
import { panelAnimationTime } from './constants'
|
|
4
5
|
|
|
5
6
|
interface RightPanelOptions {
|
|
6
7
|
title: React.ReactNode,
|
|
@@ -35,7 +36,7 @@ export function useRightPanel() {
|
|
|
35
36
|
ctx.onCloseNext.current?.()
|
|
36
37
|
setTimeout(() => {
|
|
37
38
|
setContent(null)
|
|
38
|
-
},
|
|
39
|
+
}, panelAnimationTime)
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function isOpen() {
|
package/src/utils/chat.ts
CHANGED
package/src/utils/url.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const stkAIDomain = /^https:\/\/ai(\.\w+)?\.stackspot\.com$/
|
|
2
|
+
const localhostDomain = /^http:\/\/localhost:\d+$/
|
|
3
|
+
const aiPrd = 'https://ai.stackspot.com'
|
|
4
|
+
|
|
5
|
+
export function getUrlToStackSpotAI() {
|
|
6
|
+
const current = location.origin
|
|
7
|
+
return stkAIDomain.test(current) || localhostDomain.test(current) ? current : aiPrd
|
|
8
|
+
}
|
|
@@ -8,10 +8,31 @@ import { useCallback, useMemo, useRef, useState } from 'react'
|
|
|
8
8
|
import { Markdown } from '../../components/Markdown'
|
|
9
9
|
import { useChatEntry, useCurrentChat, useWidget } from '../../context/hooks'
|
|
10
10
|
import { ChatEntry, SerializableAction, TextChatEntry } from '../../state/ChatEntry'
|
|
11
|
+
import { ChatState } from '../../state/ChatState'
|
|
12
|
+
import { buildConversationContext } from '../../utils/chat'
|
|
11
13
|
import { useDateFormatter } from '../../utils/date'
|
|
14
|
+
import { getSizeOfString } from '../../utils/string'
|
|
12
15
|
import { AgentInfo } from './AgentInfo'
|
|
13
16
|
import { useChatScrollToBottomEffect } from './chat-scroll'
|
|
14
17
|
|
|
18
|
+
async function onCopyCode(code: string, messageId: string, chat: ChatState) {
|
|
19
|
+
try {
|
|
20
|
+
await aiClient.createEvent.mutate({
|
|
21
|
+
body: [{
|
|
22
|
+
type: 'code_copied',
|
|
23
|
+
code,
|
|
24
|
+
context: buildConversationContext(chat),
|
|
25
|
+
size: getSizeOfString(code),
|
|
26
|
+
generated_at: new Date().getTime(),
|
|
27
|
+
message_id: messageId,
|
|
28
|
+
}],
|
|
29
|
+
})
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.warn('Failed to register event: code copied.')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
export const ChatMessage = ({ message, username, isLast }: { message: ChatEntry, username: string, isLast: boolean }) => {
|
|
16
37
|
const t = useTranslate(dictionary)
|
|
17
38
|
const [liked, setLiked] = useState<boolean | undefined>()
|
|
@@ -19,7 +40,7 @@ export const ChatMessage = ({ message, username, isLast }: { message: ChatEntry,
|
|
|
19
40
|
const dateFormatter = useDateFormatter()
|
|
20
41
|
const userInfo = entry.agentType === 'user' ? <Avatar size="xs">{username}</Avatar> : <AgentInfo agent={entry.agent} />
|
|
21
42
|
const date = new Date(entry.updated ?? '')
|
|
22
|
-
const
|
|
43
|
+
const shouldShowFooter = entry.updated && !isNaN(date.getTime())
|
|
23
44
|
const ref = useRef<HTMLLIElement>(null)
|
|
24
45
|
const widget = useWidget()
|
|
25
46
|
const chat = useCurrentChat()
|
|
@@ -72,7 +93,10 @@ export const ChatMessage = ({ message, username, isLast }: { message: ChatEntry,
|
|
|
72
93
|
{entry.badges?.length && <div className="badges">
|
|
73
94
|
{entry.badges.map((b, index) => <Badge key={index} palette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
|
|
74
95
|
</div>}
|
|
75
|
-
{entry.type === 'md'
|
|
96
|
+
{entry.type === 'md'
|
|
97
|
+
? <Markdown onCopyCode={(code) => onCopyCode(code, entry.messageId ?? '', chat)}>{entry.content}</Markdown>
|
|
98
|
+
: <p className="plain-text">{entry.content}</p>
|
|
99
|
+
}
|
|
76
100
|
{entry.actions?.length && <div className="actions">
|
|
77
101
|
{entry.actions.map(
|
|
78
102
|
(a, index) => (
|
|
@@ -104,7 +128,7 @@ export const ChatMessage = ({ message, username, isLast }: { message: ChatEntry,
|
|
|
104
128
|
</li>
|
|
105
129
|
))}</ul>
|
|
106
130
|
</div>}
|
|
107
|
-
<div className="message-footer">
|
|
131
|
+
{shouldShowFooter && <div className="message-footer">
|
|
108
132
|
{entry.agentType === 'bot' && !entry.error && <div className="message-actions">
|
|
109
133
|
{entry.type === 'md' && (
|
|
110
134
|
<IconButton title={t.copy} aria-label={t.copy} onClick={() => navigator.clipboard.writeText(entry.content)}>
|
|
@@ -122,8 +146,8 @@ export const ChatMessage = ({ message, username, isLast }: { message: ChatEntry,
|
|
|
122
146
|
</>
|
|
123
147
|
)}
|
|
124
148
|
</div>}
|
|
125
|
-
|
|
126
|
-
</div>
|
|
149
|
+
<Text appearance="microtext1" className="chat-date">{dateFormatter.formatForChatMessage(date)}</Text>
|
|
150
|
+
</div>}
|
|
127
151
|
</li>
|
|
128
152
|
)
|
|
129
153
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { aiClient } from '@stack-spot/portal-network'
|
|
2
2
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
|
3
|
+
import { AutoFocus } from '../../components/AutoFocus'
|
|
3
4
|
import { HistoryList } from '../../components/HistoryList'
|
|
4
5
|
import { MessageInterceptor } from '../../state/ChatState'
|
|
5
6
|
import { HistoryItem } from './HistoryItem'
|
|
@@ -7,7 +8,7 @@ import { HistoryItem } from './HistoryItem'
|
|
|
7
8
|
export const ChatHistoryPanel = ({ interceptors }: { interceptors: MessageInterceptor[] }) => {
|
|
8
9
|
const [chats, { fetchNextPage, hasNextPage }] = aiClient.chats.useInfiniteQuery({ size: 40 })
|
|
9
10
|
return (
|
|
10
|
-
<
|
|
11
|
+
<AutoFocus id="chatHistoryList" style={{ height: '100%', overflow: 'auto' }}>
|
|
11
12
|
<InfiniteScroll
|
|
12
13
|
scrollableTarget="chatHistoryList"
|
|
13
14
|
dataLength={chats.length}
|
|
@@ -23,6 +24,6 @@ export const ChatHistoryPanel = ({ interceptors }: { interceptors: MessageInterc
|
|
|
23
24
|
style={{ marginRight: '6px' }}
|
|
24
25
|
/>
|
|
25
26
|
</InfiniteScroll>
|
|
26
|
-
</
|
|
27
|
+
</AutoFocus>
|
|
27
28
|
)
|
|
28
29
|
}
|