@stack-spot/ai-chat-widget 0.1.0 → 0.3.0

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