@stack-spot/ai-chat-widget 0.9.0 → 0.11.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 (96) hide show
  1. package/dist/AbortedError.d.ts +5 -0
  2. package/dist/AbortedError.d.ts.map +1 -0
  3. package/dist/AbortedError.js +7 -0
  4. package/dist/AbortedError.js.map +1 -0
  5. package/dist/StackspotAIWidget.d.ts.map +1 -1
  6. package/dist/StackspotAIWidget.js +11 -3
  7. package/dist/StackspotAIWidget.js.map +1 -1
  8. package/dist/chat-interceptors/CustomInputs.d.ts +19 -0
  9. package/dist/chat-interceptors/CustomInputs.d.ts.map +1 -0
  10. package/dist/chat-interceptors/CustomInputs.js +62 -0
  11. package/dist/chat-interceptors/CustomInputs.js.map +1 -0
  12. package/dist/chat-interceptors/quick-command-questions.d.ts +4 -0
  13. package/dist/chat-interceptors/quick-command-questions.d.ts.map +1 -0
  14. package/dist/chat-interceptors/quick-command-questions.js +18 -0
  15. package/dist/chat-interceptors/quick-command-questions.js.map +1 -0
  16. package/dist/chat-interceptors/quick-commands.d.ts +3 -1
  17. package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
  18. package/dist/chat-interceptors/quick-commands.js +249 -8
  19. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  20. package/dist/chat-interceptors/send-message.d.ts +1 -1
  21. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  22. package/dist/chat-interceptors/send-message.js +4 -4
  23. package/dist/chat-interceptors/send-message.js.map +1 -1
  24. package/dist/components/Code.d.ts.map +1 -1
  25. package/dist/components/Code.js +9 -3
  26. package/dist/components/Code.js.map +1 -1
  27. package/dist/state/ChatEntry.d.ts +8 -8
  28. package/dist/state/ChatEntry.d.ts.map +1 -1
  29. package/dist/state/ChatEntry.js +4 -16
  30. package/dist/state/ChatEntry.js.map +1 -1
  31. package/dist/state/ChatState.d.ts +13 -1
  32. package/dist/state/ChatState.d.ts.map +1 -1
  33. package/dist/state/ChatState.js +38 -3
  34. package/dist/state/ChatState.js.map +1 -1
  35. package/dist/state/ObservableState.d.ts +1 -1
  36. package/dist/state/ObservableState.d.ts.map +1 -1
  37. package/dist/state/ObservableState.js.map +1 -1
  38. package/dist/utils/chat.d.ts.map +1 -1
  39. package/dist/utils/chat.js +2 -1
  40. package/dist/utils/chat.js.map +1 -1
  41. package/dist/utils/knowledge-source.d.ts +2 -2
  42. package/dist/utils/knowledge-source.d.ts.map +1 -1
  43. package/dist/utils/knowledge-source.js +4 -6
  44. package/dist/utils/knowledge-source.js.map +1 -1
  45. package/dist/utils/programming-languages.d.ts +1 -0
  46. package/dist/utils/programming-languages.d.ts.map +1 -1
  47. package/dist/utils/programming-languages.js +1 -0
  48. package/dist/utils/programming-languages.js.map +1 -1
  49. package/dist/utils/string.d.ts +2 -0
  50. package/dist/utils/string.d.ts.map +1 -0
  51. package/dist/utils/string.js +7 -0
  52. package/dist/utils/string.js.map +1 -0
  53. package/dist/views/Chat/ChatMessage.d.ts +2 -1
  54. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  55. package/dist/views/Chat/ChatMessage.js +15 -4
  56. package/dist/views/Chat/ChatMessage.js.map +1 -1
  57. package/dist/views/Chat/ChatMessages.d.ts.map +1 -1
  58. package/dist/views/Chat/ChatMessages.js +1 -1
  59. package/dist/views/Chat/ChatMessages.js.map +1 -1
  60. package/dist/views/Chat/styled.d.ts.map +1 -1
  61. package/dist/views/Chat/styled.js +31 -0
  62. package/dist/views/Chat/styled.js.map +1 -1
  63. package/dist/views/Editor.d.ts.map +1 -1
  64. package/dist/views/Editor.js +3 -4
  65. package/dist/views/Editor.js.map +1 -1
  66. package/dist/views/MessageInput/ButtonGroup.d.ts +1 -1
  67. package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
  68. package/dist/views/MessageInput/index.d.ts.map +1 -1
  69. package/dist/views/MessageInput/index.js +8 -4
  70. package/dist/views/MessageInput/index.js.map +1 -1
  71. package/package.json +2 -2
  72. package/src/AbortedError.ts +7 -0
  73. package/src/StackspotAIWidget.tsx +13 -3
  74. package/src/chat-interceptors/CustomInputs.ts +70 -0
  75. package/src/chat-interceptors/quick-command-questions.ts +15 -0
  76. package/src/chat-interceptors/quick-commands.ts +269 -7
  77. package/src/chat-interceptors/send-message.ts +4 -4
  78. package/src/components/Code.tsx +16 -3
  79. package/src/state/ChatEntry.ts +7 -20
  80. package/src/state/ChatState.ts +41 -3
  81. package/src/state/ObservableState.ts +1 -1
  82. package/src/utils/chat.ts +2 -1
  83. package/src/utils/knowledge-source.ts +6 -8
  84. package/src/utils/programming-languages.ts +2 -0
  85. package/src/utils/string.ts +6 -0
  86. package/src/views/Chat/ChatMessage.tsx +38 -8
  87. package/src/views/Chat/ChatMessages.tsx +4 -1
  88. package/src/views/Chat/styled.ts +31 -0
  89. package/src/views/Editor.tsx +3 -4
  90. package/src/views/MessageInput/ButtonGroup.tsx +1 -1
  91. package/src/views/MessageInput/index.tsx +9 -5
  92. package/dist/components/Editor.d.ts +0 -9
  93. package/dist/components/Editor.d.ts.map +0 -1
  94. package/dist/components/Editor.js +0 -2
  95. package/dist/components/Editor.js.map +0 -1
  96. package/src/components/Editor.tsx +0 -12
@@ -2,11 +2,11 @@
2
2
  * Copied from the extension's webview.
3
3
  */
4
4
 
5
- import { AddCode, ChevronDoubleDown, Copy } from '@citric/icons'
5
+ import { AddCode, ChevronDoubleDown, Collapse, Copy } from '@citric/icons'
6
6
  import { IconButton } from '@citric/ui'
7
7
  import { theme, useThemeKind } from '@stack-spot/portal-theme'
8
8
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
9
- import { CSSProperties } from 'react'
9
+ import { CSSProperties, useState } from 'react'
10
10
  import { ExtraProps } from 'react-markdown'
11
11
  import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
12
12
  import { materialDark, vs } from 'react-syntax-highlighter/dist/esm/styles/prism'
@@ -102,6 +102,7 @@ export const Code = ({
102
102
  }: Pick<CodeProps, 'className' | 'messageId'> & Props) => {
103
103
  const t = useTranslate(dictionary)
104
104
  const themeKind = useThemeKind()
105
+ const [showLines, setShowLines] = useState(true)
105
106
  const match = /language-(\w+)/.exec(className || '')
106
107
  const computedLanguage = language ?? (match ?? [])[1]?.toLowerCase() ?? 'txt'
107
108
  const content = String(children ?? '').replaceAll(/\n\t/g, '\n').trim()
@@ -135,6 +136,14 @@ export const Code = ({
135
136
  <CodeBox className={['code-box', themeKind].join(' ')}>
136
137
  {showActionBar && (
137
138
  <div className="action-bar" role="toolbar">
139
+ <IconButton
140
+ aria-label={showLines ? t.hideLines : t.showLines}
141
+ title={showLines ? t.hideLines : t.showLines}
142
+ onClick={() => setShowLines(v => !v)}
143
+ style={{ position: 'relative', transform: showLines ? undefined : 'rotate(180deg)', transition: 'transform 0.2s' }}
144
+ >
145
+ <Collapse />
146
+ </IconButton>
138
147
  <IconButton
139
148
  aria-label={t.copy}
140
149
  title={t.copy}
@@ -176,7 +185,7 @@ export const Code = ({
176
185
  style={themeKind === 'dark' ? materialDark : vs}
177
186
  language={computedLanguage}
178
187
  PreTag="div"
179
- showLineNumbers={true}
188
+ showLineNumbers={showLines}
180
189
  lineNumberContainerStyle={lineNumbersStyle}
181
190
  lineNumberStyle={lineNumbersStyle}
182
191
  >
@@ -192,10 +201,14 @@ const dictionary = {
192
201
  copy: 'Copy code to the clipboard',
193
202
  insert: 'Inject code into editor',
194
203
  newFile: 'Creates a new file with this code as its content',
204
+ hideLines: 'Hide line numbers',
205
+ showLines: 'Show line numbers',
195
206
  },
196
207
  pt: {
197
208
  copy: 'Copiar código para a área de transferência',
198
209
  insert: 'Inserir código no editor',
199
210
  newFile: 'Criar um novo arquivo com este código como conteúdo',
211
+ hideLines: 'Esconder números das linhas',
212
+ showLines: 'Mostrar números das linhas',
200
213
  },
201
214
  } satisfies Dictionary
@@ -1,3 +1,4 @@
1
+ import { ColorPaletteName } from '@stack-spot/portal-theme'
1
2
  import { pull } from 'lodash'
2
3
  import { LabeledWithImage } from './types'
3
4
 
@@ -27,16 +28,15 @@ export interface KnowledgeSource {
27
28
  export interface TextChatEntry {
28
29
  type: 'text' | 'md',
29
30
  agentType: 'bot' | 'user' | 'system',
30
- // image?: string,
31
31
  actions?: ChatAction[],
32
- subtitle?: string,
33
32
  content: string,
34
33
  knowledgeSources?: KnowledgeSource[],
35
34
  updated?: string,
36
35
  agent?: LabeledWithImage,
37
36
  messageId?: string,
38
37
  error?: string,
39
- // customInput?: CustomInputResponse,
38
+ badges?: { color?: ColorPaletteName, label: string }[],
39
+ card?: boolean,
40
40
  }
41
41
 
42
42
  type ChatEntryListener = (value: TextChatEntry) => void
@@ -46,20 +46,15 @@ let nextId = 0
46
46
  export class ChatEntry {
47
47
  readonly id: number
48
48
  private value: TextChatEntry
49
- private streamFinished: boolean
50
49
  private listeners: ChatEntryListener[] = []
51
- abort: () => void
52
50
 
53
51
  /**
54
52
  * @param value the value of the entry.
55
53
  * @param isStreamed whether or not this entry is streamed. Defaults to false.
56
- * @param abort an abort function to cancel the transmission of this chat entry. Specially useful for canceling streamings.
57
54
  */
58
- constructor(value: TextChatEntry, isStreamed = false, abort = () => {}) {
55
+ constructor(value: TextChatEntry) {
59
56
  this.id = nextId++
60
57
  this.value = value
61
- this.streamFinished = !isStreamed
62
- this.abort = abort
63
58
  }
64
59
 
65
60
  static createUserEntry(content: string, isMd = false) {
@@ -71,8 +66,8 @@ export class ChatEntry {
71
66
  })
72
67
  }
73
68
 
74
- static createStreamedBotEntry(abort: () => void) {
75
- return new ChatEntry({ agentType: 'bot', type: 'md', content: '' }, true, abort)
69
+ static createStreamedBotEntry() {
70
+ return new ChatEntry({ agentType: 'bot', type: 'md', content: '' })
76
71
  }
77
72
 
78
73
  setValue(value: TextChatEntry) {
@@ -84,16 +79,8 @@ export class ChatEntry {
84
79
  return this.value
85
80
  }
86
81
 
87
- finish() {
88
- this.streamFinished = true
89
- }
90
-
91
- hasFinished() {
92
- return this.streamFinished
93
- }
94
-
95
82
  onChange(listener: ChatEntryListener) {
96
- if (!this.streamFinished) this.listeners.push(listener)
83
+ this.listeners.push(listener)
97
84
  return () => {
98
85
  pull(this.listeners, listener)
99
86
  }
@@ -1,4 +1,6 @@
1
1
  import { dropRight, last, pull } from 'lodash'
2
+ import { ulid } from 'ulid'
3
+ import { AbortedError } from '../AbortedError'
2
4
  import { ChatEntry } from './ChatEntry'
3
5
  import { ObservableState } from './ObservableState'
4
6
  import { Labeled, LabeledWithImage } from './types'
@@ -27,7 +29,9 @@ export interface ChatProperties {
27
29
 
28
30
  type ChatMessagesListener = (chat: ChatEntry[]) => void
29
31
 
30
- export type MessageInterceptor = (entry: ChatEntry, chat: ChatState) => boolean | undefined | void | Promise<boolean | undefined | void>
32
+ export type MessageInterceptor = (
33
+ entry: ChatEntry, chat: ChatState, signal: AbortSignal,
34
+ ) => boolean | undefined | void | Promise<boolean | undefined | void>
31
35
 
32
36
  interface Options {
33
37
  id: string,
@@ -41,6 +45,8 @@ export class ChatState extends ObservableState<ChatProperties> {
41
45
  private entries: ChatEntry[]
42
46
  private messagesListeners: ChatMessagesListener[] = []
43
47
  private readonly interceptors: MessageInterceptor[]
48
+ interceptorMemory = new Map<string, any>()
49
+ private abortions: AbortController[] = []
44
50
 
45
51
  /**
46
52
  * @param id the id of the chat.
@@ -62,10 +68,18 @@ export class ChatState extends ObservableState<ChatProperties> {
62
68
  }
63
69
 
64
70
  private async runInterceptors(entry: ChatEntry) {
71
+ const abort = new AbortController()
72
+ this.abortions.push(abort)
65
73
  for (const interceptor of this.interceptors) {
66
- const result = await interceptor(entry, this)
67
- if (result === false) break
74
+ try {
75
+ const result = await interceptor(entry, this, abort.signal)
76
+ if (result === false) break
77
+ } catch (error) {
78
+ pull(this.abortions, abort)
79
+ throw error
80
+ }
68
81
  }
82
+ pull(this.abortions, abort)
69
83
  }
70
84
 
71
85
  pushMessage(...entries: ChatEntry[]) {
@@ -90,4 +104,28 @@ export class ChatState extends ObservableState<ChatProperties> {
90
104
  pull(this.messagesListeners, listener)
91
105
  }
92
106
  }
107
+
108
+ /**
109
+ * Aborts all currently running chat interceptors.
110
+ */
111
+ abort() {
112
+ this.abortions.forEach(a => a.abort(new AbortedError()))
113
+ this.abortions = []
114
+ }
115
+
116
+ /**
117
+ * Creates a chat that has the same state value and interceptors as this one.
118
+ *
119
+ * All abortions from this chat will be transferred to the new chat. The abortions of this chat will be cleared.
120
+ */
121
+ transferToNewChat(label: string) {
122
+ const newChat = new ChatState({
123
+ id: ulid(),
124
+ initial: { ...this.state, label },
125
+ interceptors: this.interceptors,
126
+ })
127
+ newChat.abortions = this.abortions
128
+ this.abortions = []
129
+ return newChat
130
+ }
93
131
  }
@@ -7,7 +7,7 @@ type Listeners<T> = {
7
7
  }
8
8
 
9
9
  export class ObservableState<T> {
10
- private state: T
10
+ protected state: T
11
11
  private listeners: Listeners<T> = {}
12
12
 
13
13
  constructor(initial: T) {
package/src/utils/chat.ts CHANGED
@@ -2,6 +2,7 @@ import { FixedChatRequest } from '@stack-spot/portal-network'
2
2
  import { ulid } from 'ulid'
3
3
  import { ChatState, MessageInterceptor } from '../state/ChatState'
4
4
  import { WidgetState } from '../state/WidgetState'
5
+ import { defaultLanguage } from './programming-languages'
5
6
 
6
7
  let next = 1
7
8
 
@@ -17,7 +18,7 @@ export function buildConversationContext(state: ChatState): FixedChatRequest['co
17
18
  workspace: state.get('workspace')?.id,
18
19
  conversation_id: state.id,
19
20
  stack_id: state.get('stack')?.id,
20
- language: state.get('codeLanguage'),
21
+ language: state.get('codeLanguage') || (state.get('codeSelection') ? defaultLanguage : undefined),
21
22
  knowledge_sources: state.get('knowledgeSources')?.map(ks => ks.id),
22
23
  agent_id: state.get('agent')?.id,
23
24
  agent_built_in: state.get('agent')?.builtIn,
@@ -1,4 +1,4 @@
1
- import { DocumentResponse, SourceKnowledgeSource4, SourceProjectFile4, SourceStackAi } from '@stack-spot/portal-network/api/ai'
1
+ import { DocumentResponse, SourceKnowledgeSource, SourceProjectFile4, SourceStackAi } from '@stack-spot/portal-network/api/ai'
2
2
  import { KnowledgeSource } from '../state/ChatEntry'
3
3
 
4
4
  function attemptToParseMalFormedJson(str: string) {
@@ -44,12 +44,10 @@ export function extractCodeFromKSDocument(document: DocumentResponse): { languag
44
44
  }
45
45
 
46
46
  export function genericSourcesToKnowledgeSources(
47
- sources: (SourceStackAi | SourceKnowledgeSource4 | SourceProjectFile4)[] | undefined,
47
+ sources: (SourceStackAi | SourceKnowledgeSource | SourceProjectFile4)[] | undefined,
48
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
- }))
49
+ return sources?.filter(s => s.type === 'knowledge_source').map(ks => {
50
+ const { document_id: documentId, document_score: documentScore, name, slug } = ks as SourceKnowledgeSource
51
+ return { documentId, documentScore, name, slug }
52
+ })
55
53
  }
@@ -1,3 +1,5 @@
1
+ export const defaultLanguage = 'python'
2
+
1
3
  export const languages = [
2
4
  {
3
5
  'value': 'abap',
@@ -0,0 +1,6 @@
1
+ // gets the size of a string removing control characters and spaces
2
+ export function getSizeOfString(str: string): number {
3
+ // eslint-disable-next-line no-control-regex
4
+ const withoutSpacesAndControls = str.replace(/[\u0000-\u001F\u007F-\u009F\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069\s]/g, '')
5
+ return withoutSpacesAndControls.length
6
+ }
@@ -1,17 +1,18 @@
1
1
  import { Button, IconBox, Text } from '@citric/core'
2
2
  import { Copy, Dislike, DislikeFill, Like, LikeFill, TimesCircle } from '@citric/icons'
3
- import { Avatar, IconButton } from '@citric/ui'
3
+ import { Avatar, Badge, IconButton } from '@citric/ui'
4
4
  import { aiClient } from '@stack-spot/portal-network'
5
+ import { listToClass } from '@stack-spot/portal-theme'
5
6
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
6
7
  import { useCallback, useMemo, useRef, useState } from 'react'
7
8
  import { Markdown } from '../../components/Markdown'
8
- import { useChatEntry, useWidget } from '../../context/hooks'
9
- import { ChatEntry, TextChatEntry } from '../../state/ChatEntry'
9
+ import { useChatEntry, useCurrentChat, useWidget } from '../../context/hooks'
10
+ import { ChatEntry, SerializableAction, TextChatEntry } from '../../state/ChatEntry'
10
11
  import { useDateFormatter } from '../../utils/date'
11
12
  import { AgentInfo } from './AgentInfo'
12
13
  import { useChatScrollToBottomEffect } from './chat-scroll'
13
14
 
14
- export const ChatMessage = ({ message, username }: { message: ChatEntry, username: string }) => {
15
+ export const ChatMessage = ({ message, username, isLast }: { message: ChatEntry, username: string, isLast: boolean }) => {
15
16
  const t = useTranslate(dictionary)
16
17
  const [liked, setLiked] = useState<boolean | undefined>()
17
18
  const entry = useChatEntry(message)
@@ -21,6 +22,7 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
21
22
  const shouldShowDate = entry.updated && !isNaN(date.getTime())
22
23
  const ref = useRef<HTMLLIElement>(null)
23
24
  const widget = useWidget()
25
+ const chat = useCurrentChat()
24
26
  useChatScrollToBottomEffect(ref, [entry])
25
27
 
26
28
  const detailKS = useCallback(({ name, slug, documentScore, documentId }: Required<TextChatEntry>['knowledgeSources'][number]) => {
@@ -28,6 +30,14 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
28
30
  widget.set('panel', 'ks-details')
29
31
  }, [])
30
32
 
33
+ const runAction = useCallback((action: SerializableAction) => {
34
+ if (action.type === 'link') {
35
+ window.open(action.exec, '_blank')
36
+ } else {
37
+ chat.pushMessage(ChatEntry.createUserEntry(action.exec))
38
+ }
39
+ }, [])
40
+
31
41
  const { like, dislike } = useMemo(() => {
32
42
  async function feedback(like: boolean) {
33
43
  if (!entry.messageId || like === liked) return
@@ -58,8 +68,26 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
58
68
  <li className={entry.agentType} ref={ref}>
59
69
  <div className="chat-message">
60
70
  <div className="user-info">{userInfo}</div>
61
- {entry.content && <div className="message-content">
71
+ {entry.content && <div className={listToClass(['message-content', entry.card && 'card'])}>
72
+ {entry.badges?.length && <div className="badges">
73
+ {entry.badges.map((b, index) => <Badge key={index} palette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
74
+ </div>}
62
75
  {entry.type === 'md' ? <Markdown>{entry.content}</Markdown> : <p className="plain-text">{entry.content}</p>}
76
+ {entry.actions?.length && <div className="actions">
77
+ {entry.actions.map(
78
+ (a, index) => (
79
+ <Button
80
+ key={index}
81
+ appearance={a.appearance === 'primary' ? 'contained' : 'outlined'}
82
+ colorScheme="inverse"
83
+ onClick={() => runAction(a)}
84
+ disabled={!isLast}
85
+ >
86
+ {a.title}
87
+ </Button>
88
+ ),
89
+ )}
90
+ </div>}
63
91
  </div>}
64
92
  </div>
65
93
  {entry.error && (
@@ -78,9 +106,11 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
78
106
  </div>}
79
107
  <div className="message-footer">
80
108
  {entry.agentType === 'bot' && !entry.error && <div className="message-actions">
81
- <IconButton title={t.copy} aria-label={t.copy} onClick={() => navigator.clipboard.writeText(entry.content)}>
82
- <Copy />
83
- </IconButton>
109
+ {entry.type === 'md' && (
110
+ <IconButton title={t.copy} aria-label={t.copy} onClick={() => navigator.clipboard.writeText(entry.content)}>
111
+ <Copy />
112
+ </IconButton>
113
+ )}
84
114
  {entry.messageId && (
85
115
  <>
86
116
  <IconButton title={t.like} aria-label={t.like} onClick={like}>
@@ -10,7 +10,10 @@ interface Props {
10
10
 
11
11
  export const ChatMessages = ({ chatId, username }: Props) => {
12
12
  const messages = useChatMessages(chatId)
13
- const items = useMemo(() => messages.map(m => <ChatMessage key={m.id} message={m} username={username} />), [messages])
13
+ const items = useMemo(
14
+ () => messages.map((m, index) => <ChatMessage key={m.id} message={m} username={username} isLast={index === messages.length - 1} />),
15
+ [messages],
16
+ )
14
17
  const ref = useRef<HTMLUListElement>(null)
15
18
  return <ChatList ref={ref}>{items}</ChatList>
16
19
  }
@@ -99,6 +99,37 @@ export const ChatList = styled.ul`
99
99
  }
100
100
  }
101
101
 
102
+ .message-content {
103
+ > .badges, > .actions {
104
+ display: flex;
105
+ flex-direction: row;
106
+ gap: 4px;
107
+ }
108
+ > .badges {
109
+ margin-bottom: 20px;
110
+ }
111
+ > .actions {
112
+ margin-top: 20px;
113
+ }
114
+ &.card {
115
+ margin-top: 5px;
116
+ position: relative;
117
+ padding: 16px;
118
+ border: 2px solid ${theme.color.light[500]};
119
+ border-radius: 4px;
120
+ overflow: hidden;
121
+ &:before {
122
+ content: '';
123
+ position: absolute;
124
+ top: 0;
125
+ left: 0;
126
+ bottom: 0;
127
+ width: 2px;
128
+ background: linear-gradient(180deg, ${theme.color.blue[500]} 0%, ${theme.color.indigo[500]} 100%);
129
+ }
130
+ }
131
+ }
132
+
102
133
  &.user {
103
134
  align-items: end;
104
135
 
@@ -10,9 +10,8 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'
10
10
  import { styled } from 'styled-components'
11
11
  import { useCurrentChat, useCurrentChatState, useWidget, useWidgetState } from '../context/hooks'
12
12
  import { useRightPanel } from '../right-panel/hooks'
13
- import { languages } from '../utils/programming-languages'
13
+ import { defaultLanguage, languages } from '../utils/programming-languages'
14
14
 
15
- const DEFAULT_LANGUAGE = 'python'
16
15
  const MIN_SELECTION_UPDATE_MS = 200
17
16
 
18
17
  const EditorBox = styled.div`
@@ -65,7 +64,7 @@ export const Editor = () => {
65
64
  }
66
65
 
67
66
  const Title = () => {
68
- const languageValue = useCurrentChatState('codeLanguage') || DEFAULT_LANGUAGE
67
+ const languageValue = useCurrentChatState('codeLanguage') || defaultLanguage
69
68
  const language = useMemo(() => languages.find(l => l.value === languageValue), [languageValue])
70
69
  const chat = useCurrentChat()
71
70
  return (
@@ -86,7 +85,7 @@ const Title = () => {
86
85
  const EditorPanel = () => {
87
86
  const themeKind = useThemeKind()
88
87
  const value = useCurrentChatState('code')
89
- const language = useCurrentChatState('codeLanguage') || DEFAULT_LANGUAGE
88
+ const language = useCurrentChatState('codeLanguage') || defaultLanguage
90
89
  const chat = useCurrentChat()
91
90
  const selectionObserver = useRef<IDisposable | undefined>()
92
91
 
@@ -13,7 +13,7 @@ interface ButtonGroupProps {
13
13
  setExpanded: React.Dispatch<React.SetStateAction<boolean>>,
14
14
  isLoading: boolean,
15
15
  onSend: () => void,
16
- onCancel?: () => void,
16
+ onCancel: () => void,
17
17
  }
18
18
 
19
19
  export const ButtonGroup = ({ features, onSend, onCancel, expanded, setExpanded, isLoading }: ButtonGroupProps) => {
@@ -1,6 +1,5 @@
1
1
  import { listToClass } from '@stack-spot/portal-theme'
2
- import { last } from 'lodash'
3
- import { useCallback, useRef, useState } from 'react'
2
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
3
  import { AdaptiveTextArea } from '../../components/AdaptiveTextArea'
5
4
  import { ProgressBar } from '../../components/ProgressBar'
6
5
  import { useCurrentChat, useCurrentChatState, useWidgetState } from '../../context/hooks'
@@ -24,13 +23,14 @@ export const MessageInput = ({ features }: Props) => {
24
23
  const isLoading = useCurrentChatState('isLoading') ?? false
25
24
  const value = useCurrentChatState('nextMessage') ?? ''
26
25
  const isMinimized = useWidgetState('isMinimized')
26
+ const elementRef = useRef<HTMLDivElement>(null)
27
27
 
28
28
  const onSend = useCallback(async () => {
29
29
  const message = chat.get('nextMessage')
30
30
  if (!message) return
31
31
  const code = chat.get('codeSelection')
32
32
  const language = chat.get('codeLanguage')
33
- const prompt = code ? `${message}\n\`\`\`${language}\n${code}\n\`\`\`` : message
33
+ const prompt = code && !message.startsWith('/') ? `${message}\n\`\`\`${language}\n${code}\n\`\`\`` : message
34
34
  chat.pushMessage(ChatEntry.createUserEntry(prompt, true))
35
35
  chat.set('nextMessage', '')
36
36
  setFocused(false)
@@ -43,8 +43,12 @@ export const MessageInput = ({ features }: Props) => {
43
43
  }
44
44
  }, [onSend])
45
45
 
46
+ useEffect(() => {
47
+ if (!isLoading) elementRef.current?.querySelector('textarea')?.focus()
48
+ }, [isLoading])
49
+
46
50
  return (
47
- <MessageInputBox aria-busy={isLoading} className="message-input">
51
+ <MessageInputBox ref={elementRef} aria-busy={isLoading} className="message-input">
48
52
  <ProgressBar visible={isLoading} shimmer />
49
53
  <InfoBar />
50
54
  <div className={listToClass(['action-box', focused && 'focused', isLoading && 'disabled'])}>
@@ -63,7 +67,7 @@ export const MessageInput = ({ features }: Props) => {
63
67
  <ButtonGroup
64
68
  features={features}
65
69
  onSend={onSend}
66
- onCancel={() => last(chat.getMessages())?.abort()}
70
+ onCancel={() => chat.abort()}
67
71
  expanded={expanded}
68
72
  isLoading={isLoading}
69
73
  setExpanded={(value) => {
@@ -1,9 +0,0 @@
1
- import { WithStyle } from '@stack-spot/portal-theme';
2
- interface Props extends WithStyle {
3
- value: string;
4
- onChange: (value: string) => void;
5
- language: string;
6
- }
7
- export declare const Editor: ({}: Props) => null;
8
- export {};
9
- //# sourceMappingURL=Editor.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Editor.d.ts","sourceRoot":"","sources":["../../src/components/Editor.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AAGpD,UAAU,KAAM,SAAQ,SAAS;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,MAAM,OAAQ,KAAK,SAAS,CAAA"}
@@ -1,2 +0,0 @@
1
- export const Editor = ({}) => null;
2
- //# sourceMappingURL=Editor.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"Editor.js","sourceRoot":"","sources":["../../src/components/Editor.tsx"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,EAAS,EAAE,EAAE,CAAC,IAAI,CAAA"}
@@ -1,12 +0,0 @@
1
- /* eslint-disable no-empty-pattern */
2
- /* Copiar do portal AI? */
3
- import { WithStyle } from '@stack-spot/portal-theme'
4
-
5
-
6
- interface Props extends WithStyle {
7
- value: string,
8
- onChange: (value: string) => void,
9
- language: string,
10
- }
11
-
12
- export const Editor = ({}: Props) => null