@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.
Files changed (148) hide show
  1. package/dist/StackspotAIWidget.d.ts.map +1 -1
  2. package/dist/StackspotAIWidget.js +2 -1
  3. package/dist/StackspotAIWidget.js.map +1 -1
  4. package/dist/chat-interceptors/quick-commands.js +2 -2
  5. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  6. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  7. package/dist/chat-interceptors/send-message.js +6 -9
  8. package/dist/chat-interceptors/send-message.js.map +1 -1
  9. package/dist/components/HistoryList.d.ts +2 -5
  10. package/dist/components/HistoryList.d.ts.map +1 -1
  11. package/dist/components/HistoryList.js +70 -2
  12. package/dist/components/HistoryList.js.map +1 -1
  13. package/dist/components/OverlayMenu.d.ts +3 -2
  14. package/dist/components/OverlayMenu.d.ts.map +1 -1
  15. package/dist/components/OverlayMenu.js +57 -1
  16. package/dist/components/OverlayMenu.js.map +1 -1
  17. package/dist/components/Tooltip/Tooltip.d.ts +2 -1
  18. package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
  19. package/dist/components/Tooltip/Tooltip.js +10 -2
  20. package/dist/components/Tooltip/Tooltip.js.map +1 -1
  21. package/dist/components/Tooltip/TooltipAPI.d.ts +5 -2
  22. package/dist/components/Tooltip/TooltipAPI.d.ts.map +1 -1
  23. package/dist/components/Tooltip/TooltipAPI.js +50 -8
  24. package/dist/components/Tooltip/TooltipAPI.js.map +1 -1
  25. package/dist/components/Tooltip/context.js +1 -1
  26. package/dist/components/Tooltip/context.js.map +1 -1
  27. package/dist/components/Tooltip/style.d.ts.map +1 -1
  28. package/dist/components/Tooltip/style.js +0 -1
  29. package/dist/components/Tooltip/style.js.map +1 -1
  30. package/dist/components/Tooltip/types.d.ts +6 -0
  31. package/dist/components/Tooltip/types.d.ts.map +1 -1
  32. package/dist/features.d.ts.map +1 -1
  33. package/dist/features.js +1 -0
  34. package/dist/features.js.map +1 -1
  35. package/dist/layout.css +6 -0
  36. package/dist/state/ChatEntry.d.ts +3 -2
  37. package/dist/state/ChatEntry.d.ts.map +1 -1
  38. package/dist/state/ChatEntry.js +2 -2
  39. package/dist/state/ChatEntry.js.map +1 -1
  40. package/dist/state/ChatState.d.ts +1 -7
  41. package/dist/state/ChatState.d.ts.map +1 -1
  42. package/dist/state/ChatState.js.map +1 -1
  43. package/dist/state/types.d.ts +8 -0
  44. package/dist/state/types.d.ts.map +1 -0
  45. package/dist/state/types.js +2 -0
  46. package/dist/state/types.js.map +1 -0
  47. package/dist/types.d.ts +1 -1
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/utils/date.d.ts +1 -0
  50. package/dist/utils/date.d.ts.map +1 -1
  51. package/dist/utils/date.js +3 -0
  52. package/dist/utils/date.js.map +1 -1
  53. package/dist/utils/download.d.ts +2 -0
  54. package/dist/utils/download.d.ts.map +1 -0
  55. package/dist/utils/download.js +10 -0
  56. package/dist/utils/download.js.map +1 -0
  57. package/dist/utils/knowledge-source.d.ts +3 -1
  58. package/dist/utils/knowledge-source.d.ts.map +1 -1
  59. package/dist/utils/knowledge-source.js +8 -0
  60. package/dist/utils/knowledge-source.js.map +1 -1
  61. package/dist/views/Agents.js +2 -1
  62. package/dist/views/Agents.js.map +1 -1
  63. package/dist/views/Chat/AgentInfo.d.ts +3 -2
  64. package/dist/views/Chat/AgentInfo.d.ts.map +1 -1
  65. package/dist/views/Chat/AgentInfo.js +3 -3
  66. package/dist/views/Chat/AgentInfo.js.map +1 -1
  67. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  68. package/dist/views/Chat/ChatMessage.js +3 -3
  69. package/dist/views/Chat/ChatMessage.js.map +1 -1
  70. package/dist/views/Chat/chat-scroll.d.ts.map +1 -0
  71. package/dist/{hooks → views/Chat}/chat-scroll.js +5 -3
  72. package/dist/views/Chat/chat-scroll.js.map +1 -0
  73. package/dist/views/Chat/styled.d.ts.map +1 -1
  74. package/dist/views/Chat/styled.js +8 -1
  75. package/dist/views/Chat/styled.js.map +1 -1
  76. package/dist/views/ChatHistory/ChatHistoryPanel.d.ts +5 -0
  77. package/dist/views/ChatHistory/ChatHistoryPanel.d.ts.map +1 -0
  78. package/dist/views/ChatHistory/ChatHistoryPanel.js +10 -0
  79. package/dist/views/ChatHistory/ChatHistoryPanel.js.map +1 -0
  80. package/dist/views/ChatHistory/HistoryItem.d.ts +7 -0
  81. package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -0
  82. package/dist/views/ChatHistory/HistoryItem.js +116 -0
  83. package/dist/views/ChatHistory/HistoryItem.js.map +1 -0
  84. package/dist/views/ChatHistory/dictionary.d.ts +2 -0
  85. package/dist/views/ChatHistory/dictionary.d.ts.map +1 -0
  86. package/dist/views/ChatHistory/dictionary.js +19 -0
  87. package/dist/views/ChatHistory/dictionary.js.map +1 -0
  88. package/dist/views/ChatHistory/index.d.ts +5 -0
  89. package/dist/views/ChatHistory/index.d.ts.map +1 -0
  90. package/dist/views/ChatHistory/index.js +23 -0
  91. package/dist/views/ChatHistory/index.js.map +1 -0
  92. package/dist/views/ChatHistory/styled.d.ts +2 -0
  93. package/dist/views/ChatHistory/styled.d.ts.map +1 -0
  94. package/dist/views/ChatHistory/styled.js +60 -0
  95. package/dist/views/ChatHistory/styled.js.map +1 -0
  96. package/dist/views/ChatHistory/utils.d.ts +5 -0
  97. package/dist/views/ChatHistory/utils.d.ts.map +1 -0
  98. package/dist/views/ChatHistory/utils.js +39 -0
  99. package/dist/views/ChatHistory/utils.js.map +1 -0
  100. package/dist/views/ChatTabSelection.js +1 -1
  101. package/dist/views/ChatTabSelection.js.map +1 -1
  102. package/dist/views/KnowledgeSources.d.ts.map +1 -1
  103. package/dist/views/KnowledgeSources.js +30 -21
  104. package/dist/views/KnowledgeSources.js.map +1 -1
  105. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  106. package/dist/views/Stacks.js +2 -1
  107. package/dist/views/Stacks.js.map +1 -1
  108. package/dist/views/Workspaces.d.ts.map +1 -1
  109. package/dist/views/Workspaces.js +3 -2
  110. package/dist/views/Workspaces.js.map +1 -1
  111. package/package.json +3 -2
  112. package/src/StackspotAIWidget.tsx +3 -1
  113. package/src/chat-interceptors/quick-commands.ts +2 -2
  114. package/src/chat-interceptors/send-message.ts +6 -9
  115. package/src/components/HistoryList.tsx +80 -7
  116. package/src/components/OverlayMenu.tsx +70 -3
  117. package/src/components/Tooltip/Tooltip.tsx +13 -7
  118. package/src/components/Tooltip/TooltipAPI.ts +47 -9
  119. package/src/components/Tooltip/context.tsx +1 -1
  120. package/src/components/Tooltip/style.tsx +0 -1
  121. package/src/components/Tooltip/types.ts +7 -0
  122. package/src/features.ts +1 -0
  123. package/src/layout.css +6 -0
  124. package/src/state/ChatEntry.ts +5 -4
  125. package/src/state/ChatState.ts +1 -9
  126. package/src/state/types.ts +8 -0
  127. package/src/types.ts +1 -1
  128. package/src/utils/date.ts +4 -0
  129. package/src/utils/download.ts +12 -0
  130. package/src/utils/knowledge-source.ts +13 -1
  131. package/src/views/Agents.tsx +2 -1
  132. package/src/views/Chat/AgentInfo.tsx +8 -8
  133. package/src/views/Chat/ChatMessage.tsx +4 -4
  134. package/src/{hooks → views/Chat}/chat-scroll.ts +6 -3
  135. package/src/views/Chat/styled.ts +8 -1
  136. package/src/views/ChatHistory/ChatHistoryPanel.tsx +28 -0
  137. package/src/views/ChatHistory/HistoryItem.tsx +139 -0
  138. package/src/views/ChatHistory/dictionary.ts +20 -0
  139. package/src/views/ChatHistory/index.tsx +31 -0
  140. package/src/views/ChatHistory/styled.ts +60 -0
  141. package/src/views/ChatHistory/utils.ts +37 -0
  142. package/src/views/ChatTabSelection.tsx +1 -1
  143. package/src/views/KnowledgeSources.tsx +39 -20
  144. package/src/views/Stacks.tsx +2 -1
  145. package/src/views/Workspaces.tsx +3 -2
  146. package/dist/hooks/chat-scroll.d.ts.map +0 -1
  147. package/dist/hooks/chat-scroll.js.map +0 -1
  148. /package/dist/{hooks → views/Chat}/chat-scroll.d.ts +0 -0
@@ -1,16 +1,89 @@
1
1
  /* eslint-disable no-empty-pattern */
2
- import { WithStyle } from '@stack-spot/portal-theme'
3
- import { ButtonAction } from '../types'
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
- renderLabel: (item: T) => React.ReactElement,
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
- export function HistoryList<T>({}: Props<T>) {
15
- return null
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 { WithStyle } from '@stack-spot/portal-theme'
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?: 'left' | 'top' | 'bottom' | 'right',
12
+ position?: TooltipPosition,
7
13
  actions: ButtonAction[],
8
14
  }
9
15
 
10
- export const OverlayMenu = ({}: Props) => null
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 = 'bottom', children, className, style }: Props) => {
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={e => api.show(
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 { TooltipPosition } from './types'
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
- show(content: React.ReactNode, anchor: HTMLElement, position: TooltipPosition): void {
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 rect = anchor.getClientRects()[0]
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 = rect.top + rect.height / 2 - tooltipHeight / 2
29
- if (position === 'left') left = rect.left - tooltipWidth
30
- else left = rect.left + rect.width
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 = rect.left + rect.width / 2 - tooltipWidth / 2
33
- if (position === 'top') top = rect.top - tooltipHeight
34
- else top = rect.top + rect.height
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} aria-hidden>{content}</TooltipBox>
15
+ <TooltipBox ref={ref}>{content}</TooltipBox>
16
16
  </Context.Provider>
17
17
  )
18
18
  }
@@ -5,7 +5,6 @@ export const animationTimeMS = 300
5
5
 
6
6
  export const TooltipBox = styled.div`
7
7
  position: absolute;
8
- pointer-events: none;
9
8
  opacity: 0;
10
9
  transition: opacity ${animationTimeMS/ 1000}s;
11
10
  top: 0;
@@ -6,3 +6,10 @@ export interface BoxPosition {
6
6
  y1: number,
7
7
  y2: number,
8
8
  }
9
+
10
+ export interface ShowOptions {
11
+ content: React.ReactNode,
12
+ anchor: HTMLElement,
13
+ position?: TooltipPosition,
14
+ hideOnClickOutside?: boolean,
15
+ }
package/src/features.ts CHANGED
@@ -16,4 +16,5 @@ export const defaultFeatures: AIWidgetFeatures = {
16
16
  workspace: true,
17
17
  knowledgeSource: true,
18
18
  agent: true,
19
+ chatHistory: true,
19
20
  }
package/src/layout.css CHANGED
@@ -44,6 +44,12 @@
44
44
  padding: 0 10px;
45
45
  width: calc(100% - 20px);
46
46
  }
47
+ .chat-window.narrow {
48
+ width: auto;
49
+ }
50
+ .chat-right-panel {
51
+ display: none;
52
+ }
47
53
  .home-page {
48
54
  .title {
49
55
  margin-top: 40px;
@@ -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
- agent: 'bot' | 'user' | 'system',
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
- agentId?: string,
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
- agent: 'user',
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({ agent: 'bot', type: 'md', content: '' }, true, abort)
75
+ return new ChatEntry({ agentType: 'bot', type: 'md', content: '' }, true, abort)
75
76
  }
76
77
 
77
78
  setValue(value: TextChatEntry) {
@@ -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,
@@ -0,0 +1,8 @@
1
+ export interface Labeled {
2
+ id: string,
3
+ label: string,
4
+ }
5
+
6
+ export interface LabeledWithImage extends Labeled {
7
+ image?: string,
8
+ }
package/src/types.ts CHANGED
@@ -10,7 +10,7 @@ export interface ButtonAction extends WithStyle {
10
10
  icon?: React.ReactElement,
11
11
  color?: string,
12
12
  label: string,
13
- onClick: () => void,
13
+ onClick: () => any,
14
14
  }
15
15
 
16
16
  export interface MinimizedActions {
package/src/utils/date.ts CHANGED
@@ -28,6 +28,10 @@ export function useDateFormatter() {
28
28
  }
29
29
  }
30
30
 
31
+ export function subtractDays(date: Date, numberOfDays: number) {
32
+ return new Date(date.getTime() - 24 * numberOfDays * 60 * 60000)
33
+ }
34
+
31
35
  const dictionary = {
32
36
  en: {
33
37
  today: 'Today',
@@ -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
+ }
@@ -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
- agentId?: string,
6
+ agent?: LabeledWithImage,
6
7
  }
7
8
 
8
- // todo: retrieve agent data and render accordingly
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
- <div className="agent-image-wrapper">
13
- <MiniLogo className="agent-image" />
14
- </div>
15
- <Text appearance="body2">Stackspot AI</Text>
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.agent === 'user' ? <Avatar size="xs">{username}</Avatar> : <AgentInfo agentId={entry.agentId} />
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.agent} ref={ref}>
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.agent === 'bot' && entry.messageId && !entry.error && <div className="message-actions">
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
  }
@@ -76,7 +76,7 @@ export const ChatList = styled.ul`
76
76
  flex-direction: column;
77
77
  gap: 4px;
78
78
 
79
- .agent-image-wrapper {
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
+ }