@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
@@ -113,4 +113,28 @@ export const ChatList = styled.ul`
113
113
  .plain-text {
114
114
  margin: 0
115
115
  }
116
+
117
+ .ks-box {
118
+ border-radius: 8px;
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 10px;
122
+ padding: 8px;
123
+ border: 1px solid ${theme.color.light[500]};
124
+
125
+ > ul {
126
+ display: flex;
127
+ flex-direction: row;
128
+ flex-wrap: wrap;
129
+ white-space: nowrap;
130
+ margin: 0;
131
+ padding: 0;
132
+ list-style: none;
133
+ gap: 6px;
134
+
135
+ > li button {
136
+ margin: 0;
137
+ }
138
+ }
139
+ }
116
140
  `
@@ -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
+ }
@@ -0,0 +1,127 @@
1
+ import { IconBox, Input } from '@citric/core'
2
+ import { Check, Download, EllipsisHorizontal, Pencil, Trash } from '@citric/icons'
3
+ import { IconButton, LoadingCircular } from '@citric/ui'
4
+ import { aiClient } from '@stack-spot/portal-network'
5
+ import { ConversationResponse } from '@stack-spot/portal-network/api/ai'
6
+ import { theme } from '@stack-spot/portal-theme'
7
+ import { useCallback, useEffect, useRef, useState } from 'react'
8
+ import { OverlayMenu } from '../../components/OverlayMenu'
9
+ import { useWidget } from '../../context/hooks'
10
+ import { ChatEntry } from '../../state/ChatEntry'
11
+ import { ChatState, MessageInterceptor } from '../../state/ChatState'
12
+ import { ButtonAction } from '../../types'
13
+ import { download } from '../../utils/download'
14
+ import { genericSourcesToKnowledgeSources } from '../../utils/knowledge-source'
15
+ import { useHistoryDictionary } from './dictionary'
16
+ import { HistoryItemBox } from './styled'
17
+ import { findStack, findWorkspace } from './utils'
18
+
19
+ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse, interceptors: MessageInterceptor[] }) => {
20
+ const t = useHistoryDictionary()
21
+ const [isLoading, setLoading] = useState(false)
22
+ const [isRenaming, setRenaming] = useState(false)
23
+ const [renamed, setRenamed] = useState(item.title)
24
+ const [title, setTitle] = useState(item.title)
25
+ const [isDeleted, setDeleted] = useState(false)
26
+ const renameInput = useRef<HTMLInputElement>(null)
27
+ const widget = useWidget()
28
+
29
+ useEffect(() => {
30
+ if (isRenaming) renameInput.current?.focus()
31
+ }, [isRenaming])
32
+
33
+ const onRename = useCallback(() => {
34
+ setRenaming(true)
35
+ }, [])
36
+
37
+ async function onSubmitRename() {
38
+ setRenaming(false)
39
+ if (!renamed || renamed === item.title) return
40
+ try {
41
+ await aiClient.renameChat.mutate({ conversationId: item.id, conversationUpdateTitleRequest: { title: renamed } })
42
+ setTitle(renamed)
43
+ aiClient.chats.invalidate()
44
+ } catch (error) {
45
+ // eslint-disable-next-line no-console
46
+ console.error(error)
47
+ setRenaming(true)
48
+ }
49
+ }
50
+
51
+ const onDownload = useCallback(async () => {
52
+ setLoading(true)
53
+ try {
54
+ const content = await aiClient.downloadChat.mutate({ conversationId: item.id })
55
+ download(`${title}.txt`, content)
56
+ } catch (error) {
57
+ // eslint-disable-next-line no-console
58
+ console.error(error)
59
+ }
60
+ setLoading(false)
61
+ }, [])
62
+
63
+ const onDelete = useCallback(async () => {
64
+ setDeleted(true)
65
+ try {
66
+ await aiClient.deleteChat.mutate({ conversationId: item.id })
67
+ aiClient.chats.invalidate()
68
+ } catch (error) {
69
+ // eslint-disable-next-line no-console
70
+ console.error(error)
71
+ setDeleted(false)
72
+ }
73
+ }, [])
74
+
75
+ const onSelect = useCallback(async () => {
76
+ const tab = widget.chatTabs.getAll().find(c => c.id === item.id)
77
+ if (tab) return widget.chatTabs.select(item.id)
78
+ setLoading(true)
79
+ try {
80
+ const chat = await aiClient.chat.query({ conversationId: item.id })
81
+ const [stack, workspace] = await Promise.all([findStack(chat.ai_stack_id), findWorkspace(chat.workspace_id)])
82
+ widget.chatTabs.add(new ChatState({
83
+ id: chat.id,
84
+ initial: { label: chat.title, stack, workspace },
85
+ interceptors,
86
+ entries: chat.history?.map(item => new ChatEntry({
87
+ agent: item.agent === 'USER' ? 'user' : 'bot',
88
+ content: item.content,
89
+ type: item.agent === 'USER' ? 'text' : 'md',
90
+ agentId: item.custom_agent?.id,
91
+ messageId: item.message_id,
92
+ knowledgeSources: genericSourcesToKnowledgeSources(item.sources),
93
+ updated: item.updated,
94
+ })),
95
+ }))
96
+ widget.chatTabs.select(chat.id)
97
+ } catch (error) {
98
+ // eslint-disable-next-line no-console
99
+ console.error(error)
100
+ }
101
+ setLoading(false)
102
+ }, [])
103
+
104
+ const actions: ButtonAction[] = [
105
+ { label: t.rename, onClick: onRename, icon: <Pencil /> },
106
+ { label: t.download, onClick: onDownload, icon: <Download /> },
107
+ { label: t.delete, onClick: onDelete, icon: <Trash />, color: theme.color.danger[500] },
108
+ ]
109
+
110
+ return isDeleted ? null : (
111
+ <HistoryItemBox className={isLoading ? 'loading' : ''}>
112
+ {isRenaming ? (
113
+ <>
114
+ <Input ref={renameInput} value={renamed} onChange={e => setRenamed(e.target.value)} />
115
+ <IconButton onClick={onSubmitRename}><Check /></IconButton>
116
+ </>
117
+ ) : (
118
+ <>
119
+ <button className="label" onClick={onSelect} disabled={isLoading}>{title}</button>
120
+ {isLoading ? <LoadingCircular size="xs" /> : <OverlayMenu actions={actions} position="left">
121
+ <IconBox><EllipsisHorizontal /></IconBox>
122
+ </OverlayMenu>}
123
+ </>
124
+ )}
125
+ </HistoryItemBox>
126
+ )
127
+ }
@@ -0,0 +1,20 @@
1
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
2
+
3
+ const dictionary = {
4
+ en: {
5
+ title: 'History',
6
+ description: 'Manage or continue previous conversations.',
7
+ rename: 'Rename',
8
+ download: 'Download',
9
+ delete: 'Delete',
10
+ },
11
+ pt: {
12
+ title: 'Histórico',
13
+ description: 'Gerencie ou retome conversas anteriores.',
14
+ rename: 'Renomear',
15
+ download: 'Download',
16
+ delete: 'Remover',
17
+ },
18
+ } satisfies Dictionary
19
+
20
+ export const useHistoryDictionary = () => useTranslate(dictionary)
@@ -0,0 +1,31 @@
1
+ import { useEffect } from 'react'
2
+ import { FallbackBoundary } from '../../components/FallbackBoundary'
3
+ import { useWidget, useWidgetState } from '../../context/hooks'
4
+ import { useRightPanel } from '../../right-panel/hooks'
5
+ import { MessageInterceptor } from '../../state/ChatState'
6
+ import { ChatHistoryPanel } from './ChatHistoryPanel'
7
+ import { useHistoryDictionary } from './dictionary'
8
+
9
+ export const ChatHistory = ({ interceptors }: { interceptors: MessageInterceptor[] }) => {
10
+ const t = useHistoryDictionary()
11
+ const panel = useWidgetState('panel')
12
+ const { open } = useRightPanel()
13
+ const widget = useWidget()
14
+
15
+ useEffect(() => {
16
+ if (panel === 'history') open(
17
+ <FallbackBoundary><ChatHistoryPanel interceptors={interceptors} /></FallbackBoundary>,
18
+ {
19
+ title: t.title,
20
+ description: t.description,
21
+ onClose: () => widget.set('panel', undefined),
22
+ },
23
+ )
24
+ }, [panel, t])
25
+
26
+ return null
27
+ }
28
+
29
+
30
+
31
+
@@ -0,0 +1,60 @@
1
+ import { IconBox } from '@citric/core'
2
+ import { IconButton } from '@citric/ui'
3
+ import { theme } from '@stack-spot/portal-theme'
4
+ import { styled } from 'styled-components'
5
+
6
+ export const HistoryItemBox = styled.div`
7
+ padding: 8px;
8
+ display: flex;
9
+ flex-direction: row;
10
+ align-items: center;
11
+ border-radius: 4px;
12
+ transition: background-color 0.2s;
13
+ cursor: pointer;
14
+ gap: 20px;
15
+
16
+ &:hover:not(.loading) {
17
+ background-color: ${theme.color.light[600]};
18
+ }
19
+
20
+ &.loading {
21
+ cursor: progress;
22
+
23
+ > svg {
24
+ width: 18px;
25
+ height: 18px;
26
+ }
27
+ }
28
+
29
+ button.label {
30
+ opacity: 0.8;
31
+ background-color: transparent;
32
+ border: none;
33
+ padding: 0;
34
+ color: ${theme.color.light.contrastText};
35
+ flex: 1;
36
+ display: flex;
37
+ cursor: pointer;
38
+ text-align: left;
39
+
40
+ &:disabled {
41
+ opacity: 0.4;
42
+ }
43
+ }
44
+
45
+ ${IconBox}, ${IconButton} {
46
+ padding: 0;
47
+ border: none;
48
+ background-color: transparent;
49
+ width: auto;
50
+ height: auto;
51
+
52
+ svg {
53
+ width: 12px;
54
+ }
55
+ }
56
+
57
+ ${IconButton} svg {
58
+ width: 16px;
59
+ }
60
+ `
@@ -0,0 +1,26 @@
1
+ import { aiClient, workspaceClient } from '@stack-spot/portal-network'
2
+ import { ChatProperties } from '../../state/ChatState'
3
+
4
+ export async function findStack(id: string | null): Promise<ChatProperties['stack'] | undefined> {
5
+ if (!id) return
6
+ try {
7
+ const stacks = await aiClient.aiStacks.query({})
8
+ return { id, label: stacks.find(s => s.id === id)?.name || id }
9
+ } catch (error) {
10
+ // eslint-disable-next-line no-console
11
+ console.error(error)
12
+ return { id, label: id }
13
+ }
14
+ }
15
+
16
+ export async function findWorkspace(id: string | null): Promise<ChatProperties['workspace'] | undefined> {
17
+ if (!id) return
18
+ try {
19
+ const ws = await workspaceClient.workspace.query({ workspaceId: id })
20
+ return { id, label: ws.name }
21
+ } catch (error) {
22
+ // eslint-disable-next-line no-console
23
+ console.error(error)
24
+ return { id, label: id }
25
+ }
26
+ }
@@ -35,7 +35,7 @@ export const ChatTabSelection = ({ history, interceptors }: Props) => {
35
35
  label: t.openHistory,
36
36
  className: 'test',
37
37
  style: { marginLeft: 'auto' },
38
- onClick: () => { /* todo */ },
38
+ onClick: () => widget.set('panel', 'history'),
39
39
  })
40
40
  }
41
41
  return actions
@@ -0,0 +1,58 @@
1
+ import { Flex, IconBox, Text } from '@citric/core'
2
+ import { Score } from '@citric/icons'
3
+ import { aiClient } from '@stack-spot/portal-network'
4
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
5
+ import { useEffect } from 'react'
6
+ import { Code } from '../components/Code'
7
+ import { useWidget, useWidgetState } from '../context/hooks'
8
+ import { useRightPanel } from '../right-panel/hooks'
9
+ import { extractCodeFromKSDocument } from '../utils/knowledge-source'
10
+
11
+ export const KSDocument = () => {
12
+ const t = useTranslate(dictionary)
13
+ const panel = useWidgetState('panel')
14
+ const ks = useWidgetState('currentKSInPanel')
15
+ const { open } = useRightPanel()
16
+ const widget = useWidget()
17
+
18
+ useEffect(() => {
19
+ if (panel === 'ks-details' && ks) open(
20
+ <KSDocumentPanel documentId={ks.documentId} slug={ks.slug} />,
21
+ {
22
+ title: (
23
+ <Flex flexDirection="row" alignItems="center" flex="1">
24
+ <Text appearance="h5" style={{ flex: 1 }}>{ks.name}</Text>
25
+ <Flex flexDirection="row" alignItems="center" style={{ gap: '5px' }} title="Score" aria-label="Score">
26
+ <IconBox><Score /></IconBox>
27
+ <Text>{ks.score.toFixed(2)}</Text>
28
+ </Flex>
29
+ </Flex>
30
+ ),
31
+ description: t.description,
32
+ onClose: () => widget.set('panel', undefined),
33
+ },
34
+ )
35
+ }, [panel, ks, t])
36
+
37
+ return null
38
+ }
39
+
40
+ const KSDocumentPanel = ({ slug, documentId }: { slug: string, documentId: string }) => {
41
+ const document = aiClient.knowledgeSourceDocument.useQuery({ slug, customId: documentId })
42
+ const { snippet, language, text } = extractCodeFromKSDocument(document)
43
+ return (
44
+ <>
45
+ {text && <Text>{text}</Text>}
46
+ <Code language={language}>{snippet}</Code>
47
+ </>
48
+ )
49
+ }
50
+
51
+ const dictionary = {
52
+ en: {
53
+ description: 'Knowledge Source details',
54
+ },
55
+ pt: {
56
+ description: 'Detalhes do Knowledge Source',
57
+ },
58
+ } satisfies Dictionary
@@ -4,49 +4,66 @@ import { Placeholder } from '@stack-spot/portal-components/Placeholder'
4
4
  import { aiClient } from '@stack-spot/portal-network'
5
5
  import { KnowledgeSourceItemResponse, VisibilityLevelEnum } from '@stack-spot/portal-network/api/ai'
6
6
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
7
- import { useEffect, useMemo, useState } from 'react'
7
+ import { difference, uniqBy } from 'lodash'
8
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
8
9
  import { DescribedCheckboxGroup } from '../components/form/DescribedCheckboxGroup'
9
10
  import { IconInput } from '../components/IconInput'
10
11
  import { RightPanelTabs } from '../components/RightPanelTabs'
11
12
  import { useCurrentChat, useWidget, useWidgetState } from '../context/hooks'
12
13
  import { useRightPanel } from '../right-panel/hooks'
14
+ import { ChatProperties } from '../state/ChatState'
15
+
16
+ interface TabProps {
17
+ visibility: VisibilityLevelEnum,
18
+ onSubmit: () => void,
19
+ allKS: React.MutableRefObject<ChatProperties['knowledgeSources']>,
20
+ }
13
21
 
14
22
  export const KnowledgeSources = () => {
15
23
  const t = useTranslate(dictionary)
16
- const isKnowledgeSourceSelectionOpen = useWidgetState('isKnowledgeSourceSelectionOpen')
24
+ const panel = useWidgetState('panel')
17
25
  const { open } = useRightPanel()
18
26
  const widget = useWidget()
19
27
 
20
28
  useEffect(() => {
21
- if (isKnowledgeSourceSelectionOpen) open(
29
+ if (panel === 'ks') open(
22
30
  <KnowledgeSourcesPanel />,
23
- { title: t.title, description: t.description, onClose: () => widget.set('isKnowledgeSourceSelectionOpen', false) },
31
+ { title: t.title, description: t.description, onClose: () => widget.set('panel', undefined) },
24
32
  )
25
- }, [isKnowledgeSourceSelectionOpen, t])
33
+ }, [panel, t])
26
34
 
27
35
  return null
28
36
  }
29
37
 
30
38
  const KnowledgeSourcesPanel = () => {
31
39
  const t = useTranslate(dictionary)
40
+ const chat = useCurrentChat()
41
+ const allKS = useRef(chat.get('knowledgeSources') ?? [])
42
+ const { close } = useRightPanel()
43
+
44
+ const onSubmit = useCallback(() => {
45
+ chat.set('knowledgeSources', allKS.current)
46
+ close()
47
+ }, [chat])
48
+
49
+ useEffect(() => {
50
+ allKS.current = chat.get('knowledgeSources') ?? []
51
+ }, [chat])
32
52
 
33
- return <RightPanelTabs tabs={[
34
- { title: t.personal, content: <KnowledgeSourcesTab key="personal" visibility="personal" /> },
35
- { title: t.shared, content: <KnowledgeSourcesTab key="shared" visibility="shared" /> },
36
- { title: t.account, content: <KnowledgeSourcesTab key="account" visibility="account" /> },
53
+ return <RightPanelTabs key={chat.id} tabs={[
54
+ { title: t.personal, content: <KnowledgeSourcesTab key="personal" visibility="personal" allKS={allKS} onSubmit={onSubmit} /> },
55
+ { title: t.shared, content: <KnowledgeSourcesTab key="shared" visibility="shared" allKS={allKS} onSubmit={onSubmit} /> },
56
+ { title: t.account, content: <KnowledgeSourcesTab key="account" visibility="account" allKS={allKS} onSubmit={onSubmit} /> },
37
57
  ]} />
38
58
  }
39
59
 
40
- const KnowledgeSourcesTab = ({ visibility }: { visibility: VisibilityLevelEnum }) => {
60
+ const KnowledgeSourcesTab = ({ visibility, allKS, onSubmit }: TabProps) => {
41
61
  const t = useTranslate(dictionary)
42
- const { close } = useRightPanel()
43
- const chat = useCurrentChat()
44
62
  const [filter, setFilter] = useState('')
45
63
  const knowledgeSources = aiClient.knowledgeSources.useQuery({ visibility, order: 'a-to-z' })
46
- const [hasChanged, setChanged] = useState(false)
47
64
  const [value, setValue] = useState<KnowledgeSourceItemResponse[]>((() => {
48
- const currentlySelected = chat.get('knowledgeSources')?.map(ks => ks.id)
49
- return knowledgeSources.filter(ks => currentlySelected?.includes(ks.id))
65
+ const currentlySelected = allKS.current?.map(ks => ks.id)
66
+ return knowledgeSources.filter(ks => currentlySelected?.includes(ks.slug))
50
67
  })())
51
68
  const filtered = useMemo(
52
69
  () => filter
@@ -55,10 +72,15 @@ const KnowledgeSourcesTab = ({ visibility }: { visibility: VisibilityLevelEnum }
55
72
  [knowledgeSources, filter, value],
56
73
  )
57
74
 
58
- function submit() {
59
- if (value) chat.set('knowledgeSources', value.map(({ id, name }) => ({ id, label: name })))
60
- close()
61
- }
75
+ const onChange = useCallback((newValue: KnowledgeSourceItemResponse[]) => {
76
+ setValue((current) => {
77
+ const added = difference(newValue, current)
78
+ const removed = difference(current, newValue)
79
+ allKS.current = allKS.current?.filter(ks => !removed.some(r => r.slug === ks.id)) ?? []
80
+ allKS.current = uniqBy([...allKS.current, ...added.map(ks => ({ id: ks.slug, label: ks.name }))], 'id')
81
+ return newValue
82
+ })
83
+ }, [])
62
84
 
63
85
  return (
64
86
  <>
@@ -68,13 +90,13 @@ const KnowledgeSourcesTab = ({ visibility }: { visibility: VisibilityLevelEnum }
68
90
  options={filtered}
69
91
  keygen={ks => ks.id}
70
92
  value={value}
71
- onChange={(value) => {
72
- setValue(value)
73
- setChanged(true)
74
- }}
93
+ onChange={onChange}
75
94
  renderLabel={ks => ks.name}
76
95
  renderDescription={ks => ks.description}
77
- optionClassName={ks => (filter && !ks.name.includes(filter) && value.includes(ks)) ? 'filtered-out' : ''}
96
+ optionClassName={ks => (filter && !ks.name.toLocaleLowerCase().includes(filter.toLocaleLowerCase()) && value.includes(ks))
97
+ ? 'filtered-out'
98
+ : ''
99
+ }
78
100
  className="option-list"
79
101
  />}
80
102
  {!!knowledgeSources.length && !filtered.length && (
@@ -82,7 +104,7 @@ const KnowledgeSourcesTab = ({ visibility }: { visibility: VisibilityLevelEnum }
82
104
  )}
83
105
  {!knowledgeSources.length && <Placeholder title={t.noData} description={t.noDataDescription} />}
84
106
  </div>
85
- <Button onClick={submit} disabled={!hasChanged}>{t.apply}</Button>
107
+ <Button onClick={onSubmit}>{t.apply}</Button>
86
108
  </>
87
109
  )
88
110
  }
@@ -1,8 +1,9 @@
1
- import { ChevronRight, FaceSmile, KnowledgeSource, Send, Stack, Times, Workspace } from '@citric/icons'
1
+ import { ChevronRight, KnowledgeSource, Send, Stack, Times, Workspace } from '@citric/icons'
2
2
  import { IconButton } from '@citric/ui'
3
+ import { MiniLogo } from '@stack-spot/portal-components/svg'
3
4
  import { listToClass } from '@stack-spot/portal-theme'
4
5
  import { useEffect, useRef } from 'react'
5
- import { useWidget } from '../../context/hooks'
6
+ import { useCurrentChatState, useWidget } from '../../context/hooks'
6
7
  import { MessageInputFeatures } from '../../features'
7
8
  import { useMessageInputDictionary } from './dictionary'
8
9
 
@@ -20,6 +21,7 @@ export const ButtonGroup = ({ features, onSend, onCancel, expanded, setExpanded,
20
21
  const widget = useWidget()
21
22
  const featureButtonsWidth = useRef<number | undefined>()
22
23
  const featureButtons = useRef<HTMLDivElement>(null)
24
+ const agent = useCurrentChatState('agent')
23
25
  const hasFeatureButtons = features.agent || features.workspace || features.knowledgeSource || features.stack
24
26
 
25
27
  useEffect(() => {
@@ -37,12 +39,12 @@ export const ButtonGroup = ({ features, onSend, onCancel, expanded, setExpanded,
37
39
  style={{ width: expanded ? featureButtonsWidth.current : 0 }}
38
40
  >
39
41
  {features.agent && (
40
- <IconButton aria-label={t.agent} title={t.agent} onClick={() => widget.set('isAgentSelectionOpen', true)}>
41
- <FaceSmile />
42
+ <IconButton aria-label={t.agent} title={t.agent} className="agent" onClick={() => widget.set('panel', 'agent')}>
43
+ {agent?.image ? <img src={agent.image} /> : <MiniLogo />}
42
44
  </IconButton>
43
45
  )}
44
46
  {features.workspace && (
45
- <IconButton aria-label={t.workspace} title={t.workspace} onClick={() => widget.set('isWorkspaceSelectionOpen', true)}>
47
+ <IconButton aria-label={t.workspace} title={t.workspace} onClick={() => widget.set('panel', 'workspace')}>
46
48
  <Workspace />
47
49
  </IconButton>
48
50
  )}
@@ -50,13 +52,13 @@ export const ButtonGroup = ({ features, onSend, onCancel, expanded, setExpanded,
50
52
  <IconButton
51
53
  aria-label={t.knowledgeSource}
52
54
  title={t.knowledgeSource}
53
- onClick={() => widget.set('isKnowledgeSourceSelectionOpen', true)}
55
+ onClick={() => widget.set('panel', 'ks')}
54
56
  >
55
57
  <KnowledgeSource />
56
58
  </IconButton>
57
59
  )}
58
60
  {features.stack && (
59
- <IconButton aria-label={t.stack} title={t.stack} onClick={() => widget.set('isStackSelectionOpen', true)}>
61
+ <IconButton aria-label={t.stack} title={t.stack} onClick={() => widget.set('panel', 'stack')}>
60
62
  <Stack />
61
63
  </IconButton>
62
64
  )}
@@ -9,15 +9,12 @@ import { ChatEntry } from '../../state/ChatEntry'
9
9
  import { ButtonGroup } from './ButtonGroup'
10
10
  import { useMessageInputDictionary } from './dictionary'
11
11
  import { InfoBar } from './InfoBar'
12
- import { MessageInputBox } from './styled'
12
+ import { MAX_INPUT_HEIGHT, MessageInputBox, MIN_INPUT_HEIGHT } from './styled'
13
13
 
14
14
  interface Props {
15
15
  features: MessageInputFeatures,
16
16
  }
17
17
 
18
- const MAX_INPUT_HEIGHT = 300
19
- const MAX_INPUT_HEIGHT_MINIMIZED = 30
20
-
21
18
  export const MessageInput = ({ features }: Props) => {
22
19
  const t = useMessageInputDictionary()
23
20
  const [focused, setFocused] = useState(false)
@@ -58,7 +55,7 @@ export const MessageInput = ({ features }: Props) => {
58
55
  onKeyDown={onKeyDown}
59
56
  onIncreaseSize={() => setExpanded(false)}
60
57
  onResetSize={() => !expansionLocked.current && setExpanded(true)}
61
- maxHeight={isMinimized ? MAX_INPUT_HEIGHT_MINIMIZED : MAX_INPUT_HEIGHT}
58
+ maxHeight={isMinimized ? MIN_INPUT_HEIGHT : MAX_INPUT_HEIGHT}
62
59
  />
63
60
  <ButtonGroup
64
61
  features={features}
@@ -4,6 +4,8 @@ import { styled } from 'styled-components'
4
4
 
5
5
  const INFO_BAR_HEIGHT = 42
6
6
  const INFO_BAR_DISPLACEMENT = 4
7
+ export const MAX_INPUT_HEIGHT = 300
8
+ export const MIN_INPUT_HEIGHT = 24
7
9
 
8
10
  export const MessageInputBox = styled.div`
9
11
  display: flex;
@@ -103,7 +105,7 @@ export const MessageInputBox = styled.div`
103
105
  flex-direction: row;
104
106
  align-items: center;
105
107
  gap: 4px;
106
- margin-bottom: 3px;
108
+ margin-bottom: 1px;
107
109
 
108
110
  button {
109
111
  border: none;
@@ -144,6 +146,12 @@ export const MessageInputBox = styled.div`
144
146
  transform: rotate(180deg);
145
147
  }
146
148
  }
149
+
150
+ &.agent img {
151
+ width: 80%;
152
+ height: 80%;
153
+ border-radius: 50%;
154
+ }
147
155
  }
148
156
 
149
157
  .feature-buttons {
@@ -185,8 +193,8 @@ export const MessageInputBox = styled.div`
185
193
  border: none;
186
194
  flex: 1;
187
195
  padding: 0;
188
- height: 30px;
189
- padding: 2px 0;
196
+ height: ${MIN_INPUT_HEIGHT}px;
197
+ padding: 0;
190
198
  transition: height 0.3s;
191
199
  background-color: transparent;
192
200
  &:focus {