@stack-spot/ai-chat-widget 1.2.0 → 1.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 (178) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/StackspotAIWidget.d.ts +1 -6
  3. package/dist/StackspotAIWidget.d.ts.map +1 -1
  4. package/dist/StackspotAIWidget.js +6 -11
  5. package/dist/StackspotAIWidget.js.map +1 -1
  6. package/dist/chat-interceptors/quick-commands.js +1 -1
  7. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  8. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  9. package/dist/chat-interceptors/send-message.js +21 -2
  10. package/dist/chat-interceptors/send-message.js.map +1 -1
  11. package/dist/components/AutoFocus.d.ts.map +1 -1
  12. package/dist/components/AutoFocus.js +8 -1
  13. package/dist/components/AutoFocus.js.map +1 -1
  14. package/dist/components/FadingOverflow.js +2 -2
  15. package/dist/components/FadingOverflow.js.map +1 -1
  16. package/dist/components/QuickStartButton.d.ts +6 -1
  17. package/dist/components/QuickStartButton.d.ts.map +1 -1
  18. package/dist/components/QuickStartButton.js +6 -2
  19. package/dist/components/QuickStartButton.js.map +1 -1
  20. package/dist/components/RightPanelForm.d.ts.map +1 -1
  21. package/dist/components/RightPanelForm.js +2 -1
  22. package/dist/components/RightPanelForm.js.map +1 -1
  23. package/dist/context/hooks.d.ts +1 -1
  24. package/dist/context/hooks.d.ts.map +1 -1
  25. package/dist/context/hooks.js +4 -5
  26. package/dist/context/hooks.js.map +1 -1
  27. package/dist/features.d.ts +16 -17
  28. package/dist/features.d.ts.map +1 -1
  29. package/dist/features.js +17 -9
  30. package/dist/features.js.map +1 -1
  31. package/dist/index.d.ts +4 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/layout.css +7 -0
  36. package/dist/state/ChatState.d.ts +23 -3
  37. package/dist/state/ChatState.d.ts.map +1 -1
  38. package/dist/state/ChatState.js +5 -2
  39. package/dist/state/ChatState.js.map +1 -1
  40. package/dist/state/ChatTabsController.d.ts +21 -3
  41. package/dist/state/ChatTabsController.d.ts.map +1 -1
  42. package/dist/state/ChatTabsController.js +49 -11
  43. package/dist/state/ChatTabsController.js.map +1 -1
  44. package/dist/state/WidgetState.d.ts +22 -10
  45. package/dist/state/WidgetState.d.ts.map +1 -1
  46. package/dist/state/WidgetState.js +24 -9
  47. package/dist/state/WidgetState.js.map +1 -1
  48. package/dist/views/Agents/AgentDescription.d.ts +9 -0
  49. package/dist/views/Agents/AgentDescription.d.ts.map +1 -0
  50. package/dist/views/Agents/AgentDescription.js +21 -0
  51. package/dist/views/Agents/AgentDescription.js.map +1 -0
  52. package/dist/views/Agents/AgentsPanel.d.ts +5 -0
  53. package/dist/views/Agents/AgentsPanel.d.ts.map +1 -0
  54. package/dist/views/Agents/AgentsPanel.js +19 -0
  55. package/dist/views/Agents/AgentsPanel.js.map +1 -0
  56. package/dist/views/Agents/AgentsTab.d.ts +5 -0
  57. package/dist/views/Agents/AgentsTab.d.ts.map +1 -0
  58. package/dist/views/Agents/AgentsTab.js +43 -0
  59. package/dist/views/Agents/AgentsTab.js.map +1 -0
  60. package/dist/views/Agents/dictionary.d.ts +2 -0
  61. package/dist/views/Agents/dictionary.d.ts.map +1 -0
  62. package/dist/views/Agents/dictionary.js +35 -0
  63. package/dist/views/Agents/dictionary.js.map +1 -0
  64. package/dist/views/Agents/index.d.ts +5 -0
  65. package/dist/views/Agents/index.d.ts.map +1 -0
  66. package/dist/views/Agents/index.js +21 -0
  67. package/dist/views/Agents/index.js.map +1 -0
  68. package/dist/views/Agents/styled.d.ts +3 -0
  69. package/dist/views/Agents/styled.d.ts.map +1 -0
  70. package/dist/views/Agents/styled.js +58 -0
  71. package/dist/views/Agents/styled.js.map +1 -0
  72. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  73. package/dist/views/Chat/ChatMessage.js +1 -1
  74. package/dist/views/Chat/ChatMessage.js.map +1 -1
  75. package/dist/views/Chat/index.js +1 -1
  76. package/dist/views/Chat/index.js.map +1 -1
  77. package/dist/views/Chat/styled.js +1 -1
  78. package/dist/views/ChatTabSelection.d.ts +1 -5
  79. package/dist/views/ChatTabSelection.d.ts.map +1 -1
  80. package/dist/views/ChatTabSelection.js +6 -5
  81. package/dist/views/ChatTabSelection.js.map +1 -1
  82. package/dist/views/Editor.d.ts.map +1 -1
  83. package/dist/views/Editor.js +4 -1
  84. package/dist/views/Editor.js.map +1 -1
  85. package/dist/views/Home/BuiltInAgent.d.ts +6 -0
  86. package/dist/views/Home/BuiltInAgent.d.ts.map +1 -0
  87. package/dist/views/{Home.js → Home/BuiltInAgent.js} +7 -41
  88. package/dist/views/Home/BuiltInAgent.js.map +1 -0
  89. package/dist/views/Home/CustomAgent.d.ts +5 -0
  90. package/dist/views/Home/CustomAgent.d.ts.map +1 -0
  91. package/dist/views/Home/CustomAgent.js +24 -0
  92. package/dist/views/Home/CustomAgent.js.map +1 -0
  93. package/dist/views/Home/index.d.ts +8 -0
  94. package/dist/views/Home/index.d.ts.map +1 -0
  95. package/dist/views/Home/index.js +15 -0
  96. package/dist/views/Home/index.js.map +1 -0
  97. package/dist/views/Home/styled.d.ts +2 -0
  98. package/dist/views/Home/styled.d.ts.map +1 -0
  99. package/dist/views/Home/styled.js +59 -0
  100. package/dist/views/Home/styled.js.map +1 -0
  101. package/dist/views/Home/types.d.ts +7 -0
  102. package/dist/views/Home/types.d.ts.map +1 -0
  103. package/dist/views/Home/types.js +2 -0
  104. package/dist/views/Home/types.js.map +1 -0
  105. package/dist/views/KnowledgeSources.js +1 -1
  106. package/dist/views/KnowledgeSources.js.map +1 -1
  107. package/dist/views/MessageInput/ButtonGroup.d.ts +1 -6
  108. package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
  109. package/dist/views/MessageInput/ButtonGroup.js +12 -4
  110. package/dist/views/MessageInput/ButtonGroup.js.map +1 -1
  111. package/dist/views/MessageInput/InfoBar.d.ts.map +1 -1
  112. package/dist/views/MessageInput/InfoBar.js +16 -6
  113. package/dist/views/MessageInput/InfoBar.js.map +1 -1
  114. package/dist/views/MessageInput/QuickCommandSelector.js +3 -3
  115. package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -1
  116. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  117. package/dist/views/MessageInput/index.d.ts +1 -9
  118. package/dist/views/MessageInput/index.d.ts.map +1 -1
  119. package/dist/views/MessageInput/index.js +2 -2
  120. package/dist/views/MessageInput/index.js.map +1 -1
  121. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  122. package/dist/views/MessageInput/styled.js +6 -2
  123. package/dist/views/MessageInput/styled.js.map +1 -1
  124. package/dist/views/MinimizedHeader.d.ts.map +1 -1
  125. package/dist/views/MinimizedHeader.js +2 -3
  126. package/dist/views/MinimizedHeader.js.map +1 -1
  127. package/dist/views/Stacks.js +2 -1
  128. package/dist/views/Stacks.js.map +1 -1
  129. package/dist/views/Workspaces.js +2 -1
  130. package/dist/views/Workspaces.js.map +1 -1
  131. package/package.json +2 -2
  132. package/src/StackspotAIWidget.tsx +6 -16
  133. package/src/chat-interceptors/quick-commands.ts +1 -1
  134. package/src/chat-interceptors/send-message.ts +22 -2
  135. package/src/components/AutoFocus.tsx +9 -1
  136. package/src/components/FadingOverflow.tsx +2 -2
  137. package/src/components/QuickStartButton.tsx +17 -5
  138. package/src/components/RightPanelForm.tsx +2 -1
  139. package/src/context/hooks.ts +7 -8
  140. package/src/features.ts +27 -24
  141. package/src/index.ts +6 -0
  142. package/src/layout.css +7 -0
  143. package/src/state/ChatState.ts +26 -4
  144. package/src/state/ChatTabsController.ts +50 -11
  145. package/src/state/WidgetState.ts +39 -13
  146. package/src/views/Agents/AgentDescription.tsx +48 -0
  147. package/src/views/Agents/AgentsPanel.tsx +19 -0
  148. package/src/views/Agents/AgentsTab.tsx +80 -0
  149. package/src/views/Agents/dictionary.ts +36 -0
  150. package/src/views/Agents/index.tsx +26 -0
  151. package/src/views/Agents/styled.ts +59 -0
  152. package/src/views/Chat/ChatMessage.tsx +19 -17
  153. package/src/views/Chat/index.tsx +1 -1
  154. package/src/views/Chat/styled.ts +1 -1
  155. package/src/views/ChatTabSelection.tsx +7 -9
  156. package/src/views/Editor.tsx +4 -1
  157. package/src/views/{Home.tsx → Home/BuiltInAgent.tsx} +7 -48
  158. package/src/views/Home/CustomAgent.tsx +39 -0
  159. package/src/views/Home/index.tsx +20 -0
  160. package/src/views/Home/styled.ts +59 -0
  161. package/src/views/Home/types.ts +6 -0
  162. package/src/views/KnowledgeSources.tsx +2 -2
  163. package/src/views/MessageInput/ButtonGroup.tsx +15 -12
  164. package/src/views/MessageInput/InfoBar.tsx +25 -9
  165. package/src/views/MessageInput/QuickCommandSelector.tsx +3 -3
  166. package/src/views/MessageInput/index.tsx +1 -10
  167. package/src/views/MessageInput/styled.ts +6 -2
  168. package/src/views/MinimizedHeader.tsx +2 -3
  169. package/src/views/Stacks.tsx +3 -2
  170. package/src/views/Workspaces.tsx +3 -2
  171. package/dist/views/Agents.d.ts +0 -2
  172. package/dist/views/Agents.d.ts.map +0 -1
  173. package/dist/views/Agents.js +0 -146
  174. package/dist/views/Agents.js.map +0 -1
  175. package/dist/views/Home.d.ts +0 -14
  176. package/dist/views/Home.d.ts.map +0 -1
  177. package/dist/views/Home.js.map +0 -1
  178. package/src/views/Agents.tsx +0 -203
@@ -46,8 +46,12 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
46
46
  if (agentType !== 'user') return
47
47
  const context = buildConversationContext(chat)
48
48
  chat.set('isLoading', true)
49
+ const untitled = chat.untitled
49
50
  const isFirstMessage = chat.getMessages().length === 1
50
- if (isFirstMessage) chat.set('label', content)
51
+ if (untitled) {
52
+ chat.set('label', content)
53
+ chat.untitled = false
54
+ }
51
55
  const stream = aiClient.sendChatMessage({ context, user_prompt: content.replace(/^\s*\\(\\|\/)/, '$1') })
52
56
  signal.addEventListener('abort', () => stream.cancel())
53
57
  const botEntry = ChatEntry.createStreamedBotEntry()
@@ -63,7 +67,23 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
63
67
  const finalValue = await stream.getValue()
64
68
  botEntry.setValue(createEntryValueFromChatResponse(finalValue, knowledgeSources, chat.get('agent'), true))
65
69
  aiClient.chat.invalidate({ conversationId: chat.id })
66
- if (isFirstMessage) aiClient.chats.invalidate()
70
+ if (isFirstMessage) {
71
+ // if the chat has a title and this was its first message, we need to rename it according to the title, otherwise, the backend will
72
+ // keep the name generated by default.
73
+ if (!untitled) {
74
+ try {
75
+ await aiClient.renameChat.mutate({
76
+ conversationId: chat.id,
77
+ conversationUpdateTitleRequest: { title: chat.get('label') },
78
+ })
79
+ } catch (error) {
80
+ // eslint-disable-next-line no-console
81
+ console.warn('Failed to rename chat:', chat.get('label'), error)
82
+ }
83
+ }
84
+ // this makes sure to update the chat history
85
+ aiClient.chats.invalidate()
86
+ }
67
87
  } catch (error: any) {
68
88
  botEntry.setValue({
69
89
  ...botEntry.getValue(),
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable react/display-name */
2
2
  import { focusFirstChild, TagPriorityElement } from '@stack-spot/portal-components'
3
3
  import { forwardRef, RefObject, useEffect, useRef } from 'react'
4
+ import { styled } from 'styled-components'
4
5
 
5
6
  interface Props extends React.HTMLAttributes<HTMLDivElement> {
6
7
  /**
@@ -19,6 +20,13 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
19
20
  ignore?: string,
20
21
  }
21
22
 
23
+ const FlexibleDiv = styled.div`
24
+ overflow: hidden;
25
+ flex: 1;
26
+ display: flex;
27
+ flex-direction: column;
28
+ `
29
+
22
30
  /**
23
31
  * Focus the first focusable child as soon as the component mounts.
24
32
  */
@@ -30,5 +38,5 @@ export const AutoFocus = forwardRef<HTMLDivElement, Props>(({ children, delay =
30
38
  setTimeout(() => focusFirstChild(ref.current, { priority, ignore }), delay)
31
39
  }, [])
32
40
 
33
- return <div ref={ref} {...props}>{children}</div>
41
+ return <FlexibleDiv ref={ref} {...props}>{children}</FlexibleDiv>
34
42
  })
@@ -45,8 +45,8 @@ const SCROLL_PX = 4
45
45
  const masks = {
46
46
  right: 'linear-gradient(to left, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
47
47
  left: 'linear-gradient(to right, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
48
- top: 'linear-gradient(to bottom, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
49
- bottom: 'linear-gradient(to top, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
48
+ top: 'linear-gradient(to top, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
49
+ bottom: 'linear-gradient(to bottom, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgba(0, 0, 0) 100%)',
50
50
  horizontal: 'linear-gradient(to left, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgb(0, 0, 0) max(70%, calc(100% - 100px)), rgba(0, 0, 0, 0) 100%)',
51
51
  vertical: 'linear-gradient(to top, rgb(0, 0, 0, 0) 0%, rgb(0, 0, 0) min(30%, 100px), rgb(0, 0, 0) max(70%, calc(100% - 100px)), rgba(0, 0, 0, 0) 100%)',
52
52
  }
@@ -2,9 +2,15 @@ import { IconBox, Text } from '@citric/core'
2
2
  import { theme, WithStyle } from '@stack-spot/portal-theme'
3
3
  import { styled } from 'styled-components'
4
4
  import { ButtonAction } from '../types'
5
+ import { FadingOverflow } from './FadingOverflow'
5
6
 
6
7
  interface Props extends ButtonAction, WithStyle {
7
8
  background?: string,
9
+ /**
10
+ * Whether or not the vertical overflow of this button should be managed by the component {@link FadingOverflow}.
11
+ * @default false
12
+ */
13
+ manageOverflow?: boolean,
8
14
  }
9
15
 
10
16
  /**
@@ -46,9 +52,15 @@ const QuickButton = styled.button<{ $color?: string, $bg?: string }>`
46
52
  }
47
53
  `
48
54
 
49
- export const QuickStartButton = ({ label, onClick, background, className, color, icon, style }: Props) => (
50
- <QuickButton className={className} style={style} onClick={onClick} $color={color} $bg={background}>
51
- <IconBox aria-hidden>{icon}</IconBox>
55
+ export const QuickStartButton = ({ label, onClick, background, className, color, icon, style, manageOverflow }: Props) => {
56
+ const content = <>
57
+ {icon && <IconBox aria-hidden>{icon}</IconBox>}
52
58
  <Text>{label}</Text>
53
- </QuickButton>
54
- )
59
+ </>
60
+
61
+ return (
62
+ <QuickButton className={className} style={style} onClick={onClick} $color={color} $bg={background}>
63
+ {manageOverflow ? <FadingOverflow sides={['top', 'bottom']} scroll="wheel">{content}</FadingOverflow> : content}
64
+ </QuickButton>
65
+ )
66
+ }
@@ -1,5 +1,6 @@
1
1
  import { Card } from '@citric/ui'
2
2
  import { styled } from 'styled-components'
3
+ import { panelAnimationTime } from '../right-panel/constants'
3
4
  import { PropsOf } from '../types'
4
5
  import { AutoFocus } from './AutoFocus'
5
6
  import { FallbackBoundary } from './FallbackBoundary'
@@ -46,7 +47,7 @@ const Form = styled.form`
46
47
  */
47
48
  export const RightPanelForm = ({ children, onSubmit, ...props }: PropsOf<typeof Form>) => (
48
49
  <FallbackBoundary>
49
- <AutoFocus>
50
+ <AutoFocus delay={panelAnimationTime}>
50
51
  <Form
51
52
  {...props}
52
53
  onSubmit={(e) => {
@@ -39,11 +39,11 @@ export function useWidgetState<K extends keyof WidgetProperties>(key: K): Widget
39
39
  /**
40
40
  * Watches the tabs of a ChatState. The value is updated whenever a tab is added, removed or selected.
41
41
  */
42
- export function useChatTabs(): { chats: ChatState[], active: string } {
42
+ export function useChatTabs(): { chats: ChatState[], active: ChatState } {
43
43
  const widget = useWidget()
44
- const [tabs, setTabs] = useState<{ chats: ChatState[], active: string }>({
45
- chats: widget.chatTabs.getAll(),
46
- active: widget.chatTabs.getActiveChatId(),
44
+ const [tabs, setTabs] = useState<{ chats: ChatState[], active: ChatState }>({
45
+ chats: widget.chatTabs.getAll(),
46
+ active: widget.chatTabs.getActiveChat(),
47
47
  })
48
48
  useEffect(() => widget.chatTabs.onChange((chats, active) => setTabs({ chats, active })), [])
49
49
  return tabs
@@ -59,7 +59,7 @@ export function useChatTabs(): { chats: ChatState[], active: string } {
59
59
  export function useChat(chatId: string): ChatState {
60
60
  const widget = useWidget()
61
61
  const chat = widget.chatTabs.get(chatId)
62
- if (!chat) throw new Error(`No chat with id ${chatId} was found. Maybe there are no chats opened, try to call useFirstChat first.`)
62
+ if (!chat) throw new Error(`No chat with id ${chatId} was found. Maybe there are no chats opened.`)
63
63
  return chat
64
64
  }
65
65
 
@@ -71,8 +71,7 @@ export function useChat(chatId: string): ChatState {
71
71
  * @returns the currently active chat.
72
72
  */
73
73
  export function useCurrentChat(): ChatState {
74
- const { active } = useChatTabs()
75
- return useChat(active)
74
+ return useChatTabs().active
76
75
  }
77
76
 
78
77
  /**
@@ -118,7 +117,7 @@ export function useChatMessages(chatId: string): ChatEntry[] {
118
117
  */
119
118
  export function useCurrentChatMessages(): ChatEntry[] {
120
119
  const { active } = useChatTabs()
121
- return useChatMessages(active)
120
+ return useChatMessages(active.id)
122
121
  }
123
122
 
124
123
  /**
package/src/features.ts CHANGED
@@ -1,50 +1,53 @@
1
- export interface MessageInputFeatures {
1
+ export interface ChatFeatures {
2
2
  /**
3
3
  * Enables stack selection.
4
- * @default true
5
4
  */
6
- stack?: boolean,
5
+ stack: boolean,
7
6
  /**
8
7
  * Enables workspace selection.
9
- * @default true
10
8
  */
11
- workspace?: boolean,
9
+ workspace: boolean,
12
10
  /**
13
11
  * Enables knowledge source selection.
14
- * @default true
15
12
  */
16
- knowledgeSource?: boolean,
13
+ knowledgeSource: boolean,
17
14
  /**
18
15
  * Enables agent selection.
19
- * @default true
20
16
  */
21
- agent?: boolean,
17
+ agent: boolean,
22
18
  /**
23
19
  * Enables quick commands.
24
- * @default true
25
20
  */
26
- quickCommands?: boolean,
21
+ quickCommands: boolean,
27
22
  /**
28
23
  * Enables the editor.
29
- * @default true
30
24
  */
31
- editor?: boolean,
25
+ editor: boolean,
32
26
  }
33
27
 
34
- export interface AIWidgetFeatures extends MessageInputFeatures {
28
+ export interface GlobalFeatures {
35
29
  /**
36
30
  * Enables the chat history.
37
- * @default true
38
31
  */
39
- chatHistory?: boolean,
32
+ chatHistory: boolean,
40
33
  }
41
34
 
42
- export const defaultFeatures: AIWidgetFeatures = {
43
- stack: true,
44
- workspace: true,
45
- knowledgeSource: true,
46
- agent: true,
47
- chatHistory: true,
48
- editor: true,
49
- quickCommands: true,
35
+ export type AIWidgetFeatures = ChatFeatures & GlobalFeatures
36
+
37
+ /**
38
+ * Receives a partial feature object and returns a full feature object, setting to true any missing property.
39
+ * @param features the partial features object.
40
+ * @returns a full features object.
41
+ */
42
+ export function getFeaturesWithDefaults(features?: Partial<AIWidgetFeatures>): AIWidgetFeatures {
43
+ return {
44
+ agent: true,
45
+ editor: true,
46
+ knowledgeSource: true,
47
+ quickCommands: true,
48
+ stack: true,
49
+ workspace: true,
50
+ chatHistory: true,
51
+ ...features,
52
+ }
50
53
  }
package/src/index.ts CHANGED
@@ -1,8 +1,14 @@
1
+ /* Attention: in order for the package "page" to work without linking the lib, we must export types separately, using the "type" keyword */
2
+
3
+ export { QuickStartButton } from './components/QuickStartButton'
1
4
  export { AIWidgetProvider } from './context/AIWidgetProvider'
2
5
  export * from './context/hooks'
6
+ export { getFeaturesWithDefaults } from './features'
7
+ export type { AIWidgetFeatures, ChatFeatures } from './features'
3
8
  export { StackspotAIWidget } from './StackspotAIWidget'
4
9
  export { ChatEntry } from './state/ChatEntry'
5
10
  export { ChatState } from './state/ChatState'
6
11
  export { ChatTabsController } from './state/ChatTabsController'
7
12
  export { ObservableState } from './state/ObservableState'
13
+ export type { Labeled, LabeledWithImage } from './state/types'
8
14
  export { WidgetState } from './state/WidgetState'
package/src/layout.css CHANGED
@@ -128,3 +128,10 @@
128
128
  left: 50%;
129
129
  }
130
130
  }
131
+
132
+ /* fixes placeholder description */
133
+ .no-data-placeholder {
134
+ > div > p {
135
+ text-align: center;
136
+ }
137
+ }
@@ -1,11 +1,12 @@
1
1
  import { dropRight, last, pull } from 'lodash'
2
2
  import { ulid } from 'ulid'
3
3
  import { AbortedError } from '../AbortedError'
4
+ import { ChatFeatures, getFeaturesWithDefaults } from '../features'
4
5
  import { ChatEntry } from './ChatEntry'
5
6
  import { ObservableState } from './ObservableState'
6
7
  import { Labeled, LabeledWithImage } from './types'
7
8
 
8
- export interface ChatProperties {
9
+ export interface ChatPropertiesWithOptionalFeatures {
9
10
  /**
10
11
  * The name of the chat.
11
12
  */
@@ -46,6 +47,19 @@ export interface ChatProperties {
46
47
  * The current selection in the editor.
47
48
  */
48
49
  codeSelection?: string,
50
+ /**
51
+ * The features enabled for this chat.
52
+ *
53
+ * If a feature is marked as false, it's disabled, otherwise it's enabled.
54
+ */
55
+ features?: Partial<ChatFeatures>,
56
+ }
57
+
58
+ export interface ChatProperties extends ChatPropertiesWithOptionalFeatures {
59
+ /**
60
+ * The features enabled for this chat.
61
+ */
62
+ features: ChatFeatures,
49
63
  }
50
64
 
51
65
  type ChatMessagesListener = (chat: ChatEntry[]) => void
@@ -62,7 +76,7 @@ interface Options {
62
76
  /**
63
77
  * The initial value for the state of this chat.
64
78
  */
65
- initial: ChatProperties,
79
+ initial: ChatPropertiesWithOptionalFeatures,
66
80
  /**
67
81
  * The interceptors to use for the messages pushed to this chat.
68
82
  *
@@ -86,6 +100,12 @@ interface Options {
86
100
  * The content of this chat, i.e. its messages.
87
101
  */
88
102
  entries?: ChatEntry[],
103
+ /**
104
+ * Whether or not the label for this chat is real (persisted in the backend) or just a mock.
105
+ *
106
+ * This should be true if the label is just a mock.
107
+ */
108
+ untitled?: boolean,
89
109
  }
90
110
 
91
111
  /**
@@ -104,12 +124,14 @@ export class ChatState extends ObservableState<ChatProperties> {
104
124
  * Abort signals currently active.
105
125
  */
106
126
  private abortions: AbortController[] = []
127
+ untitled: boolean
107
128
 
108
- constructor({ id, initial, entries = [], interceptors = [] }: Options) {
109
- super(initial)
129
+ constructor({ id, initial, entries = [], interceptors = [], untitled = false }: Options) {
130
+ super({ ...initial, features: getFeaturesWithDefaults(initial.features) })
110
131
  this.id = id
111
132
  this.interceptors = interceptors
112
133
  this.entries = entries
134
+ this.untitled = untitled
113
135
  }
114
136
 
115
137
  private runMessagesListeners() {
@@ -1,7 +1,7 @@
1
- import { last, pull } from 'lodash'
1
+ import { groupBy, last, pull } from 'lodash'
2
2
  import { ChatState } from './ChatState'
3
3
 
4
- type TabChangeListener = (chats: ChatState[], activeId: string) => void
4
+ type TabChangeListener = (chats: ChatState[], active: ChatState) => void
5
5
 
6
6
  /**
7
7
  * Controls the chat tabs.
@@ -12,7 +12,7 @@ export class ChatTabsController {
12
12
  private listeners: TabChangeListener[] = []
13
13
 
14
14
  private runListeners() {
15
- this.listeners.forEach(l => l(this.chats, this.activeChatId))
15
+ this.listeners.forEach(l => l(this.chats, this.getActiveChat()))
16
16
  }
17
17
 
18
18
  /**
@@ -26,20 +26,50 @@ export class ChatTabsController {
26
26
  }
27
27
 
28
28
  /**
29
- * Removes chats from the tab view.
30
- * @param ids the ids of the chats to remove.
29
+ * If the active chat is deleted, this selects a new chat based on the position of the chat that has been removed.
30
+ *
31
+ * @param previous the previous tabs.
32
+ * @param deleted the ids of the chats that have been deleted.
31
33
  */
32
- remove(...ids: string[]) {
33
- if (this.chats.length <= 1 || !ids.length) return
34
- const currentActiveIndex = this.chats.findIndex(c => c.id === this.activeChatId)
35
- this.chats = this.chats.filter(c => !ids.includes(c.id))
36
- if (ids.includes(this.activeChatId)) {
34
+ private reselect(previous: ChatState[], deleted: string[]) {
35
+ if (deleted.includes(this.activeChatId)) {
36
+ const currentActiveIndex = previous.findIndex(c => c.id === this.activeChatId)
37
37
  if (currentActiveIndex === -1) this.activeChatId = this.chats[0]?.id
38
38
  this.activeChatId = currentActiveIndex < this.chats.length ? this.chats[currentActiveIndex].id : last(this.chats)?.id ?? ''
39
39
  }
40
+ }
41
+
42
+ /**
43
+ * Removes chats from the tab view. This will never remove a chat if this chat is the last one.
44
+ *
45
+ * @param ids the ids of the chats to remove.
46
+ */
47
+ remove(...ids: string[]) {
48
+ ids.splice(this.chats.length - 1, ids.length - this.chats.length)
49
+ if (!ids.length) return
50
+ const previous = this.chats
51
+ this.chats = this.chats.filter(c => !ids.includes(c.id))
52
+ this.reselect(previous, ids)
40
53
  this.runListeners()
41
54
  }
42
55
 
56
+ /**
57
+ * Removes all empty chats. The last chat will be kept if it's empty, but the only chat remaining.
58
+ *
59
+ * @param exceptions ids of chats that shouldn't be removed even if empty.
60
+ */
61
+ removeEmptyChats(...exceptions: string[]) {
62
+ const lastChat = last(this.chats)
63
+ const previous = this.chats
64
+ const { true: removed, false: kept = [] } = groupBy(this.chats, c => c.getMessages().length === 0 && !exceptions.includes(c.id))
65
+ if (removed) {
66
+ this.chats = kept
67
+ if (lastChat && this.chats.length === 0) this.chats.push(lastChat)
68
+ this.reselect(previous, removed.map(c => c.id))
69
+ this.runListeners()
70
+ }
71
+ }
72
+
43
73
  /**
44
74
  * @param id the id of the chat to retrieve.
45
75
  * @returns a ChatState corresponding to the id.
@@ -49,12 +79,21 @@ export class ChatTabsController {
49
79
  }
50
80
 
51
81
  /**
52
- * @returns the if of the chat corresponding to the tab that is currently active.
82
+ * @returns the id of the chat corresponding to the tab that is currently active.
53
83
  */
54
84
  getActiveChatId() {
55
85
  return this.activeChatId
56
86
  }
57
87
 
88
+ /**
89
+ * @returns the chat corresponding to the tab that is currently active.
90
+ */
91
+ getActiveChat() {
92
+ const chat = this.chats.find(c => c.id === this.activeChatId)
93
+ if (!chat) throw new Error('Chat state error, the active chat id corresponds to none chat in the tabs.')
94
+ return chat
95
+ }
96
+
58
97
  /**
59
98
  * @returns all chats in tab view.
60
99
  */
@@ -3,8 +3,9 @@ import { ulid } from 'ulid'
3
3
  import { quickCommandQuestionsInterceptor } from '../chat-interceptors/quick-command-questions'
4
4
  import { createQuickCommandInterceptor } from '../chat-interceptors/quick-commands'
5
5
  import { sendMessageInterceptor } from '../chat-interceptors/send-message'
6
+ import { AIWidgetFeatures, ChatFeatures, getFeaturesWithDefaults, GlobalFeatures } from '../features'
6
7
  import { ChatEntry } from './ChatEntry'
7
- import { ChatProperties, ChatState, MessageInterceptor } from './ChatState'
8
+ import { ChatPropertiesWithOptionalFeatures, ChatState, MessageInterceptor } from './ChatState'
8
9
  import { ChatTabsController } from './ChatTabsController'
9
10
  import { ObservableState } from './ObservableState'
10
11
 
@@ -22,27 +23,39 @@ export interface WidgetProperties {
22
23
  */
23
24
  isMinimized?: boolean,
24
25
  /**
25
- * Whether or not the execution of quick commands is enabled.
26
+ * Global features of the chat widget. These features don't depend on the chat currently selected.
26
27
  */
27
- areQuickCommandsEnabled?: boolean,
28
+ features: GlobalFeatures,
28
29
  }
29
30
 
30
31
  interface Options {
31
32
  /**
32
33
  * Chat interceptors allows you to intercept chat messages, interpret them, modify them and even change the chat flow. See
33
34
  * {@link ChatState} for more details.
35
+ *
36
+ * These interceptors, in addition to the default interceptors, will be used when creating a new chat via `WidgetState#createChat`.
34
37
  */
35
38
  interceptors?: MessageInterceptor[],
39
+ /**
40
+ * Features to enable in the widget. If a feature is not set explicitly to false, it's interpreted as enabled.
41
+ *
42
+ * These features will be used when creating a new chat via `WidgetState#createChat`. Updating chat features only influence new chats.
43
+ */
44
+ features?: Partial<AIWidgetFeatures>,
36
45
  /**
37
46
  * The initial values for the widget state.
47
+ *
48
+ * The default value for each feature under `initial.features` is true.
38
49
  */
39
- initial?: WidgetProperties,
50
+ initial?: Omit<WidgetProperties, 'features'>,
40
51
  /**
41
52
  * The initial tabs for the chat window.
42
53
  */
43
54
  chatTabs?: ChatTabsController,
44
55
  }
45
56
 
57
+ const untitledChatPrefix = 'Chat '
58
+
46
59
  /**
47
60
  * Holds the full state of the AI Chat Widget.
48
61
  *
@@ -55,12 +68,14 @@ export class WidgetState extends ObservableState<WidgetProperties> {
55
68
  */
56
69
  interceptors: MessageInterceptor[] = []
57
70
  /**
58
- * helps with naming new chats
71
+ * Chat features to be used by default when creating a new chat.
59
72
  */
60
- private next = 1
73
+ chatFeatures: ChatFeatures
61
74
 
62
- constructor({ chatTabs, initial = {}, interceptors = [] }: Options = {}) {
63
- super(initial)
75
+ constructor({ chatTabs, initial, interceptors = [], features }: Options = {}) {
76
+ const featuresWithDefaults = getFeaturesWithDefaults(features)
77
+ super({ ...initial, features: featuresWithDefaults })
78
+ this.chatFeatures = featuresWithDefaults
64
79
  this.chatTabs = chatTabs ?? new ChatTabsController()
65
80
  this.interceptors = [
66
81
  ...interceptors,
@@ -71,21 +86,32 @@ export class WidgetState extends ObservableState<WidgetProperties> {
71
86
  this.createChat()
72
87
  }
73
88
 
89
+ private getNextUntitledChatIndex() {
90
+ let max = 0
91
+ for (const chat of this.chatTabs.getAll()) {
92
+ const [, match] = chat.get('label').match(`${untitledChatPrefix}(\\d+)`) ?? []
93
+ const index = parseInt(match)
94
+ if (index > max) max = index
95
+ }
96
+ return max + 1
97
+ }
98
+
74
99
  /**
75
- * Utility function for adding a new chat to the chat tabs.
100
+ * Utility function for adding a new chat to the chat tabs. This also selects the new tab.
76
101
  * @param properties the chat properties (initial state).
77
102
  * @param entries the entries for the chat to start with.
78
- * @returns a function that, when called, removes the chat tab.
103
+ * @returns the chat created.
79
104
  */
80
- createChat(properties?: ChatProperties, entries: ChatEntry[] = []) {
105
+ createChat({ label, ...properties }: Partial<ChatPropertiesWithOptionalFeatures> = {}, entries: ChatEntry[] = []) {
81
106
  const chat = new ChatState({
82
107
  id: ulid(),
83
- initial: { label: `Chat ${this.next++}`, ...properties },
108
+ initial: { label: label || `${untitledChatPrefix}${this.getNextUntitledChatIndex()}`, features: this.chatFeatures, ...properties },
84
109
  entries,
85
110
  interceptors: this.interceptors,
111
+ untitled: !label,
86
112
  })
87
113
  this.chatTabs.add(chat)
88
114
  this.chatTabs.select(chat.id)
89
- return () => this.chatTabs.remove(chat.id)
115
+ return chat
90
116
  }
91
117
  }
@@ -0,0 +1,48 @@
1
+ import { Text } from '@citric/core'
2
+ import { Badge, Skeleton } from '@citric/ui'
3
+ import { agentClient } from '@stack-spot/portal-network'
4
+ import { useMemo } from 'react'
5
+ import { useAgentsDictionary } from './dictionary'
6
+ import { AgentDescriptionBox } from './styled'
7
+
8
+ interface Props {
9
+ agentId?: string,
10
+ llm?: string,
11
+ description?: string,
12
+ numberOfKnowledgeSources: number,
13
+ }
14
+
15
+ export const AgentDescription = ({ agentId, llm, description, numberOfKnowledgeSources }: Props) => {
16
+ const t = useAgentsDictionary()
17
+ const [agent,,, { isLoading }] = agentClient.agent.useStatefulQuery({ agentId: agentId! }, { enabled: !!agentId })
18
+ const knowledgeSources = useMemo(
19
+ () => agent?.knowledge_sources_config?.knowledge_sources_details?.map((ks, index) => (
20
+ <li key={index}><Badge palette="teal" appearance="square">{ks.name}</Badge></li>
21
+ )),
22
+ [agent],
23
+ )
24
+ const skeleton = useMemo(() => {
25
+ const loadingKS: React.ReactElement[] = []
26
+ for (let i = 0; i < numberOfKnowledgeSources; i++) {
27
+ loadingKS.push(<li key={i}><Badge palette="teal" appearance="square"><Skeleton className="ks-skeleton" /></Badge></li>)
28
+ }
29
+ return loadingKS
30
+ }, [numberOfKnowledgeSources])
31
+
32
+ return (
33
+ <AgentDescriptionBox>
34
+ {description && <section>
35
+ <Text appearance="microtext1" className="title">{t.description}</Text>
36
+ <Text>{description}</Text>
37
+ </section>}
38
+ {(!!numberOfKnowledgeSources || !!knowledgeSources?.length) && <section>
39
+ <Text appearance="microtext1" className="title">Knowledge sources</Text>
40
+ <ul>{isLoading ? skeleton : knowledgeSources}</ul>
41
+ </section>}
42
+ {llm && <section>
43
+ <Text appearance="microtext1" className="title">LLM</Text>
44
+ <Badge palette="orange" appearance="square">{llm}</Badge>
45
+ </section>}
46
+ </AgentDescriptionBox>
47
+ )
48
+ }
@@ -0,0 +1,19 @@
1
+ import { RightPanelTabs } from '../../components/RightPanelTabs'
2
+ import { useCurrentChat } from '../../context/hooks'
3
+ import { AgentsTab } from './AgentsTab'
4
+ import { useAgentsDictionary } from './dictionary'
5
+
6
+ /**
7
+ * Renders the Agent selection form in the Right Panel if this is the panel that is currently opened.
8
+ */
9
+ export const AgentsPanel = () => {
10
+ const t = useAgentsDictionary()
11
+ const chat = useCurrentChat()
12
+
13
+ return <RightPanelTabs key={chat.id} tabs={[
14
+ { title: t.builtin, content: <AgentsTab key="builtin" visibility="BUILT-IN" /> },
15
+ { title: t.personal, content: <AgentsTab key="personal" visibility="PERSONAL" /> },
16
+ { title: t.shared, content: <AgentsTab key="shared" visibility="SHARED" /> },
17
+ { title: t.account, content: <AgentsTab key="account" visibility="ACCOUNT" /> },
18
+ ]} />
19
+ }