@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.
Files changed (108) hide show
  1. package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
  2. package/dist/chat-interceptors/quick-commands.js +23 -22
  3. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  4. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  5. package/dist/chat-interceptors/send-message.js +14 -10
  6. package/dist/chat-interceptors/send-message.js.map +1 -1
  7. package/dist/components/AdaptiveTextArea.d.ts +1 -1
  8. package/dist/components/AdaptiveTextArea.d.ts.map +1 -1
  9. package/dist/components/AdaptiveTextArea.js +6 -4
  10. package/dist/components/AdaptiveTextArea.js.map +1 -1
  11. package/dist/components/AutoFocus.d.ts +6 -0
  12. package/dist/components/AutoFocus.d.ts.map +1 -0
  13. package/dist/components/AutoFocus.js +15 -0
  14. package/dist/components/AutoFocus.js.map +1 -0
  15. package/dist/components/Fading.d.ts +15 -0
  16. package/dist/components/Fading.d.ts.map +1 -0
  17. package/dist/components/Fading.js +31 -0
  18. package/dist/components/Fading.js.map +1 -0
  19. package/dist/components/FallbackBoundary/ErrorBoundary.d.ts +3 -0
  20. package/dist/components/FallbackBoundary/ErrorBoundary.d.ts.map +1 -1
  21. package/dist/components/FallbackBoundary/ErrorBoundary.js +18 -4
  22. package/dist/components/FallbackBoundary/ErrorBoundary.js.map +1 -1
  23. package/dist/components/FallbackBoundary/Loading.js +1 -1
  24. package/dist/components/FallbackBoundary/Loading.js.map +1 -1
  25. package/dist/components/FallbackBoundary/index.d.ts +6 -1
  26. package/dist/components/FallbackBoundary/index.d.ts.map +1 -1
  27. package/dist/components/FallbackBoundary/index.js +1 -1
  28. package/dist/components/FallbackBoundary/index.js.map +1 -1
  29. package/dist/components/OverlayMenu.d.ts +1 -1
  30. package/dist/components/OverlayMenu.d.ts.map +1 -1
  31. package/dist/components/OverlayMenu.js +26 -9
  32. package/dist/components/OverlayMenu.js.map +1 -1
  33. package/dist/components/RightPanelForm.d.ts.map +1 -1
  34. package/dist/components/RightPanelForm.js +5 -4
  35. package/dist/components/RightPanelForm.js.map +1 -1
  36. package/dist/components/Tooltip/Tooltip.d.ts +3 -1
  37. package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
  38. package/dist/components/Tooltip/Tooltip.js +14 -5
  39. package/dist/components/Tooltip/Tooltip.js.map +1 -1
  40. package/dist/components/Tooltip/TooltipAPI.d.ts +2 -2
  41. package/dist/components/Tooltip/TooltipAPI.d.ts.map +1 -1
  42. package/dist/components/Tooltip/TooltipAPI.js +51 -51
  43. package/dist/components/Tooltip/TooltipAPI.js.map +1 -1
  44. package/dist/layout.css +5 -0
  45. package/dist/regex.d.ts +2 -0
  46. package/dist/regex.d.ts.map +1 -0
  47. package/dist/regex.js +2 -0
  48. package/dist/regex.js.map +1 -0
  49. package/dist/right-panel/DefaultPanel.d.ts.map +1 -1
  50. package/dist/right-panel/DefaultPanel.js +3 -1
  51. package/dist/right-panel/DefaultPanel.js.map +1 -1
  52. package/dist/right-panel/constants.d.ts +2 -0
  53. package/dist/right-panel/constants.d.ts.map +1 -0
  54. package/dist/right-panel/constants.js +2 -0
  55. package/dist/right-panel/constants.js.map +1 -0
  56. package/dist/right-panel/hooks.d.ts.map +1 -1
  57. package/dist/right-panel/hooks.js +2 -1
  58. package/dist/right-panel/hooks.js.map +1 -1
  59. package/dist/utils/chat.js +1 -1
  60. package/dist/utils/url.d.ts +2 -0
  61. package/dist/utils/url.d.ts.map +1 -0
  62. package/dist/utils/url.js +8 -0
  63. package/dist/utils/url.js.map +1 -0
  64. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  65. package/dist/views/Chat/ChatMessage.js +24 -2
  66. package/dist/views/Chat/ChatMessage.js.map +1 -1
  67. package/dist/views/ChatHistory/ChatHistoryPanel.d.ts.map +1 -1
  68. package/dist/views/ChatHistory/ChatHistoryPanel.js +2 -1
  69. package/dist/views/ChatHistory/ChatHistoryPanel.js.map +1 -1
  70. package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -1
  71. package/dist/views/ChatHistory/HistoryItem.js +10 -1
  72. package/dist/views/ChatHistory/HistoryItem.js.map +1 -1
  73. package/dist/views/MessageInput/QuickCommandSelector.d.ts +6 -0
  74. package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -0
  75. package/dist/views/MessageInput/QuickCommandSelector.js +137 -0
  76. package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -0
  77. package/dist/views/MessageInput/index.d.ts.map +1 -1
  78. package/dist/views/MessageInput/index.js +6 -4
  79. package/dist/views/MessageInput/index.js.map +1 -1
  80. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  81. package/dist/views/MessageInput/styled.js +137 -0
  82. package/dist/views/MessageInput/styled.js.map +1 -1
  83. package/package.json +2 -2
  84. package/src/chat-interceptors/quick-commands.ts +24 -23
  85. package/src/chat-interceptors/send-message.ts +23 -11
  86. package/src/components/AdaptiveTextArea.tsx +9 -4
  87. package/src/components/AutoFocus.tsx +20 -0
  88. package/src/components/Fading.tsx +46 -0
  89. package/src/components/FallbackBoundary/ErrorBoundary.tsx +26 -3
  90. package/src/components/FallbackBoundary/Loading.tsx +1 -1
  91. package/src/components/FallbackBoundary/index.tsx +7 -2
  92. package/src/components/OverlayMenu.tsx +59 -19
  93. package/src/components/RightPanelForm.tsx +12 -9
  94. package/src/components/Tooltip/Tooltip.tsx +24 -5
  95. package/src/components/Tooltip/TooltipAPI.ts +42 -42
  96. package/src/layout.css +5 -0
  97. package/src/regex.ts +1 -0
  98. package/src/right-panel/DefaultPanel.tsx +14 -9
  99. package/src/right-panel/constants.ts +1 -0
  100. package/src/right-panel/hooks.tsx +2 -1
  101. package/src/utils/chat.ts +1 -1
  102. package/src/utils/url.ts +8 -0
  103. package/src/views/Chat/ChatMessage.tsx +29 -5
  104. package/src/views/ChatHistory/ChatHistoryPanel.tsx +3 -2
  105. package/src/views/ChatHistory/HistoryItem.tsx +11 -2
  106. package/src/views/MessageInput/QuickCommandSelector.tsx +210 -0
  107. package/src/views/MessageInput/index.tsx +8 -4
  108. 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
  }
@@ -10,5 +10,5 @@ const LoadingBox = styled.div`
10
10
  `
11
11
 
12
12
  export const Loading = () => (
13
- <LoadingBox><LoadingCircular /></LoadingBox>
13
+ <LoadingBox className="loading"><LoadingCircular /></LoadingBox>
14
14
  )
@@ -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 }: WithChildren) => (
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 { useMemo } from 'react'
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
- export const OverlayMenu = ({ actions, children, className, position, style }: Props) => {
64
+ const Menu = ({ actions, trigger }: MenuProps) => {
57
65
  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])
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
- <Tooltip content={menu} custom position={position} className={className} style={style} triggeredBy="click">
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
- <Form
46
- {...props}
47
- onSubmit={(e) => {
48
- e.preventDefault()
49
- onSubmit?.(e)
50
- }}
51
- >
52
- {children}
53
- </Form>
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 = ({ content, custom, position, triggeredBy = 'hover', children, className, style }: Props) => {
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.MouseEvent<HTMLDivElement, MouseEvent>, hideOnClickOutside?: boolean) {
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' ? { onMouseEnter: show, onMouseLeave: () => api.hide() } : { onClick: (e) => show(e, true) })}
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
- private tooltipRef: React.RefObject<HTMLDivElement>
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): void {
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
- setTimeout(() => {
36
- if (!this.tooltipRef.current) return
37
- const anchorRect = anchor.getClientRects()[0]
38
- this.tooltipRef.current.classList.add('visible')
39
- const tooltipWidth = this.tooltipRef.current.clientWidth
40
- const tooltipHeight = this.tooltipRef.current.clientHeight
41
- let top = 0
42
- let left = 0
43
- if (position === 'left' || position === 'right') {
44
- top = anchorRect.top + anchorRect.height / 2 - tooltipHeight / 2
45
- if (position === 'left') left = anchorRect.left - tooltipWidth
46
- else left = anchorRect.left + anchorRect.width
47
- } else {
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
65
- }
66
- this.tooltipRef.current.style.top = `${top}px`
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)
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
- }, 10)
74
+ document.addEventListener('click', this.clickListener)
75
+ }
76
76
  }
77
77
 
78
78
  hide(): void {
package/src/layout.css CHANGED
@@ -73,6 +73,11 @@
73
73
  display: none;
74
74
  }
75
75
  }
76
+ .quick-command-selector {
77
+ width: auto;
78
+ left: 10px;
79
+ right: 10px;
80
+ }
76
81
  }
77
82
  }
78
83
 
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
- <header>
48
- <div className="title">
49
- {typeof title === 'string' ? <Text appearance="h5">{title}</Text> : title}
50
- {typeof description === 'string' ? <Text colorScheme="light.700">{description}</Text> : description}
51
- </div>
52
- <IconButton title={t.close} aria-label={t.close} onClick={onClose}>
53
- <Times />
54
- </IconButton>
55
- </header>
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
- }, 300)
39
+ }, panelAnimationTime)
39
40
  }
40
41
 
41
42
  function isOpen() {
package/src/utils/chat.ts CHANGED
@@ -25,6 +25,6 @@ export function buildConversationContext(state: ChatState): FixedChatRequest['co
25
25
  os: navigator.userAgent,
26
26
  platform: 'web-widget',
27
27
  platform_version: navigator.userAgent,
28
- stackspot_ai_version: 'alpha',
28
+ stackspot_ai_version: '1.0.0',
29
29
  }
30
30
  }
@@ -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 shouldShowDate = entry.updated && !isNaN(date.getTime())
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' ? <Markdown>{entry.content}</Markdown> : <p className="plain-text">{entry.content}</p>}
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
- {shouldShowDate && <Text appearance="microtext1" className="chat-date">{dateFormatter.formatForChatMessage(date)}</Text>}
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
- <div id="chatHistoryList" style={{ height: '100%', overflow: 'auto' }}>
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
- </div>
27
+ </AutoFocus>
27
28
  )
28
29
  }