@stack-spot/ai-chat-widget 2.11.1 → 2.12.1

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 (51) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/StackspotAIWidget.d.ts +1 -0
  3. package/dist/StackspotAIWidget.d.ts.map +1 -1
  4. package/dist/StackspotAIWidget.js +1 -0
  5. package/dist/StackspotAIWidget.js.map +1 -1
  6. package/dist/app-metadata.json +15 -3
  7. package/dist/chat-interceptors/quick-commands.d.ts +15 -0
  8. package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
  9. package/dist/chat-interceptors/quick-commands.js +20 -2
  10. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  11. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  12. package/dist/chat-interceptors/send-message.js +39 -34
  13. package/dist/chat-interceptors/send-message.js.map +1 -1
  14. package/dist/components/Code.d.ts.map +1 -1
  15. package/dist/components/Code.js +19 -4
  16. package/dist/components/Code.js.map +1 -1
  17. package/dist/components/FileDescription.js +1 -1
  18. package/dist/components/FileDescription.js.map +1 -1
  19. package/dist/components/Markdown.d.ts.map +1 -1
  20. package/dist/components/Markdown.js +22 -6
  21. package/dist/components/Markdown.js.map +1 -1
  22. package/dist/components/TabManager.d.ts.map +1 -1
  23. package/dist/components/TabManager.js +20 -4
  24. package/dist/components/TabManager.js.map +1 -1
  25. package/dist/state/ChatState.d.ts +5 -0
  26. package/dist/state/ChatState.d.ts.map +1 -1
  27. package/dist/state/ChatState.js.map +1 -1
  28. package/dist/utils/chat.d.ts +4 -12
  29. package/dist/utils/chat.d.ts.map +1 -1
  30. package/dist/utils/chat.js +19 -19
  31. package/dist/utils/chat.js.map +1 -1
  32. package/dist/utils/knowledge-source.js +1 -1
  33. package/dist/utils/knowledge-source.js.map +1 -1
  34. package/dist/views/KnowledgeSources.js +1 -1
  35. package/dist/views/KnowledgeSources.js.map +1 -1
  36. package/dist/views/MessageInput/ContextBar.js +1 -1
  37. package/dist/views/MessageInput/ContextBar.js.map +1 -1
  38. package/package.json +6 -3
  39. package/src/StackspotAIWidget.tsx +1 -0
  40. package/src/app-metadata.json +15 -3
  41. package/src/chat-interceptors/quick-commands.ts +28 -7
  42. package/src/chat-interceptors/send-message.ts +39 -35
  43. package/src/components/Code.tsx +19 -4
  44. package/src/components/FileDescription.tsx +1 -1
  45. package/src/components/Markdown.tsx +42 -23
  46. package/src/components/TabManager.tsx +31 -8
  47. package/src/state/ChatState.ts +6 -0
  48. package/src/utils/chat.ts +22 -22
  49. package/src/utils/knowledge-source.ts +1 -1
  50. package/src/views/KnowledgeSources.tsx +1 -1
  51. package/src/views/MessageInput/ContextBar.tsx +1 -1
@@ -1,5 +1,4 @@
1
- import { AgentInfo, aiClient, ChatResponseWithPMResources, ChatResponseWithSteps, ChatStep, StackspotAPIError, StreamCanceledError } from '@stack-spot/portal-network'
2
- import { ChatResponse3 } from '@stack-spot/portal-network/api/ai'
1
+ import { AgentInfo, aiClient, ChatResponse, ChatResponseWithPMResources, ChatResponseWithSteps, ChatStep, genAiInferenceClient, StackspotAPIError, StreamCanceledError } from '@stack-spot/portal-network'
3
2
  import { findLast } from 'lodash'
4
3
  import { ChatEntry, KnowledgeSource, TextChatEntry } from '../state/ChatEntry'
5
4
  import { ChatState } from '../state/ChatState'
@@ -27,13 +26,13 @@ export function createEntryValueFromChatResponse(
27
26
  const entry = {
28
27
  agentType: 'bot',
29
28
  type: 'md',
30
- content: response.answer ?? '',
29
+ content: response.message ?? '',
31
30
  messageId: response.message_id ?? undefined,
32
31
  knowledgeSources,
33
32
  agent: agent,
34
33
  updated: includeDate ? new Date().toISOString() : undefined,
35
34
  steps: response.steps,
36
- tools: response.tools,
35
+ tools: response.tools_id,
37
36
  opportunities: response.opportunities,
38
37
  hypothesis: response.hypothesis,
39
38
  prfaq: response.prfaq,
@@ -112,39 +111,44 @@ const updatePlanningMessage = (messages: ChatEntry[]) => {
112
111
 
113
112
  export function helperSendMessage(messages: ChatEntry[], value: Partial<ChatResponseWithSteps> & { opportunities?: any },
114
113
  chat: ChatState, botEntry: ChatEntry, knowledgeSources: KnowledgeSource[] | undefined) {
115
- if (value.agent_info?.type === 'planning') {
116
- if (value.agent_info.action === 'start') {
117
- chat.set('isPlaning', true)
118
- } else {
119
- chat.set('isPlaning', false)
114
+ for (const agent_info of value.agent_info ?? []) {
115
+ if (agent_info?.type === 'planning') {
116
+ if (agent_info.action === 'start') {
117
+ chat.set('isPlaning', true)
118
+ } else {
119
+ chat.set('isPlaning', false)
120
+ }
120
121
  }
121
- }
122
122
 
123
- if (value.agent_info?.type === 'chat' && value.agent_info?.action === 'end') {
124
- //When an error happens, the step can still be running, so we enforce the error
125
- const stepRunning = findLast(value.steps, (item) => item.status === 'running')
126
- if (stepRunning?.status) {
127
- stepRunning.status = 'error'
123
+ if (agent_info?.type === 'chat' && agent_info?.action === 'end') {
124
+ //When an error happens, the step can still be running, so we enforce the error
125
+ const stepRunning = findLast(value.steps, (item) => item.status === 'running')
126
+ if (stepRunning?.status) {
127
+ stepRunning.status = 'error'
128
+ }
128
129
  }
129
- }
130
130
 
131
- if (value.sources?.length !== knowledgeSources?.length && chat.get('features').showSourcesInResponse) {
132
- knowledgeSources = genericSourcesToKnowledgeSources(value.sources)
133
- }
131
+ const lastPlanningAwaiting = getLastPlanningAwaiting(messages)
132
+ if (lastPlanningAwaiting && value.steps) {
133
+ value.steps?.map(step => {
134
+ if (step.type === 'planning') {
135
+ updatePlanningMessage(messages)
136
+ } else if (step.type === 'step') {
137
+ updateStepMessage(step, messages)
138
+ }
139
+ })
140
+ }
134
141
 
135
- const lastPlanningAwaiting = getLastPlanningAwaiting(messages)
136
- if (lastPlanningAwaiting && value.steps) {
137
- value.steps?.map(step => {
138
- if (step.type === 'planning') {
139
- updatePlanningMessage(messages)
140
- } else if (step.type === 'step') {
141
- updateStepMessage(step, messages)
142
- }
143
- })
142
+ if (agent_info?.type === 'tool' && agent_info?.action !== 'awaiting_approval') {
143
+ updateToolStatus(agent_info, messages)
144
+ }
144
145
  }
145
146
 
146
- if (value.agent_info?.type === 'tool' && value.agent_info?.action !== 'awaiting_approval') {
147
- updateToolStatus(value.agent_info, messages)
147
+ if (chat.get('features').showSourcesInResponse) {
148
+ const mergedSources = [...(value.source ?? []), ...(value.cross_account_source ?? [])]
149
+ if (mergedSources.length !== (knowledgeSources?.length ?? 0)) {
150
+ knowledgeSources = genericSourcesToKnowledgeSources(mergedSources)
151
+ }
148
152
  }
149
153
 
150
154
  if (value.steps) {
@@ -193,7 +197,7 @@ export function helperSendMessage(messages: ChatEntry[], value: Partial<ChatResp
193
197
  export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState, signal: AbortSignal) {
194
198
  const { agentType, content, data } = entry.getValue()
195
199
  if (agentType !== 'user') return
196
- const context = buildConversationContext(chat, entry)
200
+ const context = buildConversationContext(chat, entry, buildPrompt(content, data))
197
201
  chat.set('isLoading', true)
198
202
  const untitled = chat.untitled
199
203
  const messages = chat.getMessages()
@@ -203,7 +207,7 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
203
207
  chat.untitled = false
204
208
  }
205
209
 
206
- const stream = aiClient.sendChatMessage({ context, user_prompt: buildPrompt(content, data) })
210
+ const stream = genAiInferenceClient.sendChatMessage(context)
207
211
  signal.addEventListener('abort', () => stream.cancel())
208
212
  const botEntry = ChatEntry.createStreamedBotEntry()
209
213
  // we add the chat entry and show the streaming if the streaming feature is enabled
@@ -216,14 +220,14 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
216
220
  helperSendMessage(messages, value, chat, botEntry, knowledgeSources)
217
221
  })
218
222
 
219
- let finalValue: Partial<ChatResponse3> | undefined
223
+ let finalValue: Partial<ChatResponse> | undefined
220
224
  try {
221
225
  finalValue = await stream.getValue()
222
226
 
223
227
  const lastPlanningAwaiting = getLastPlanningAwaiting(messages)
224
228
  if (lastPlanningAwaiting) {
225
229
  const value = lastPlanningAwaiting.getValue()
226
- value.content = finalValue.answer || value.content
230
+ value.content = finalValue.message || value.content
227
231
  lastPlanningAwaiting.setValue(value)
228
232
  }
229
233
  // if the streaming feature is not enabled, we only add the chat entry once the streaming has finished
@@ -243,7 +247,7 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
243
247
  })
244
248
  }
245
249
  }
246
- if (finalValue?.answer) {
250
+ if (finalValue?.message) {
247
251
  botEntry.setValue({
248
252
  ...createEntryValueFromChatResponse(finalValue, botEntry.getValue().knowledgeSources, chat.get('agent'), true),
249
253
  hasPlanning: botEntry.getValue().hasPlanning,
@@ -36,7 +36,9 @@ export interface Props extends WithChildren {
36
36
  const CodeBox = styled.code`
37
37
  background: ${theme.color.light[500]};
38
38
  border-radius: 6px;
39
- overflow: hidden;
39
+ overflow: visible;
40
+ display: block;
41
+ position: relative;
40
42
 
41
43
  .highlighter {
42
44
  margin: 0 !important;
@@ -55,13 +57,26 @@ const CodeBox = styled.code`
55
57
  }
56
58
 
57
59
  .header-code {
60
+ position: sticky;
61
+ top: 0;
62
+ z-index: 10;
58
63
  display: flex;
59
64
  flex-direction: row;
60
65
  justify-content: space-between;
61
- background: ${theme.color.light[600]};
66
+ background: ${theme.color.light[400]};
62
67
  padding: 4px 6px;
63
- border-top-left-radius: 6px;
64
- border-top-right-radius: 6px;
68
+ &:after {
69
+ content: '';
70
+ position: absolute;
71
+ top: 0;
72
+ left: 0;
73
+ right: 0;
74
+ bottom: 0;
75
+ border-top-left-radius: 6px;
76
+ border-top-right-radius: 6px;
77
+ background: ${theme.color.light[600]};
78
+ z-index: -1;
79
+ }
65
80
 
66
81
  .action-bar {
67
82
  display: flex;
@@ -94,7 +94,7 @@ export const FileDescription = ({ fileName, icon, status, onRemove, onRetry }: F
94
94
  icon="TimesMini"
95
95
  onClick={onRemove}
96
96
  title={t.remove}
97
- arial-label={`${t.remove} ${name}`}
97
+ aria-label={name}
98
98
  style={{ alignSelf: 'start' }}
99
99
  size="xs"
100
100
  />}
@@ -4,7 +4,9 @@
4
4
 
5
5
  import { Children, Fragment } from 'react'
6
6
  import ReactMarkdown from 'react-markdown'
7
+ import rehypeKatex from 'rehype-katex'
7
8
  import remarkGfm from 'remark-gfm'
9
+ import remarkMath from 'remark-math'
8
10
  import { WithChildren } from '../types'
9
11
  import { Code, Props as CodeProps } from './Code'
10
12
 
@@ -29,26 +31,43 @@ export const Markdown = (
29
31
  onCopyCode,
30
32
  children,
31
33
  }: Props,
32
- ) => (
33
- <>
34
- <ReactMarkdown
35
- className="markdown apply-citric"
36
- remarkPlugins={[[remarkGfm]]}
37
- components={{
38
- a: props => <a target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'underline' }} {...props} />,
39
- code: props =>
40
- <Code
41
- {...props}
42
- onInsertCode={onInsertCode}
43
- onNewFile={onNewFile}
44
- onCopyCode={onCopyCode}
45
- showActionBar
46
- />,
47
- pre: ({ children }) => <>{children}</>,
48
- p: ({ children }) => <p>{Children.map(children, renderP)}</p>,
49
- }}
50
- >
51
- {children}
52
- </ReactMarkdown>
53
- </>
54
- )
34
+ ) => {
35
+
36
+ // fix parsing LaTex: https://github.com/remarkjs/react-markdown/issues/785
37
+ const content = typeof children === 'string'
38
+ ? children
39
+ .replace(/\\\\\[/g, '$$$$') // Replace '\\[' with '$$'
40
+ .replace(/\\\\\]/g, '$$$$') // Replace '\\]' with '$$'
41
+ .replace(/\\\\\(/g, '$$$$') // Replace '\\(' with '$$'
42
+ .replace(/\\\\\)/g, '$$$$') // Replace '\\)' with '$$'
43
+ .replace(/\\\[/g, '$$$$') // Replace '\[' with '$$'
44
+ .replace(/\\\]/g, '$$$$') // Replace '\]' with '$$'
45
+ .replace(/\\\(/g, '$$$$') // Replace '\(' with '$$'
46
+ .replace(/\\\)/g, '$$$$') // Replace '\)' with '$$';
47
+ : children
48
+
49
+ return (
50
+ <>
51
+ <ReactMarkdown
52
+ className="markdown apply-citric"
53
+ remarkPlugins={[[remarkMath, { singleDollarTextMath: true }], remarkGfm]}
54
+ rehypePlugins={[rehypeKatex]}
55
+ components={{
56
+ a: props => <a target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'underline' }} {...props} />,
57
+ code: props =>
58
+ <Code
59
+ {...props}
60
+ onInsertCode={onInsertCode}
61
+ onNewFile={onNewFile}
62
+ onCopyCode={onCopyCode}
63
+ showActionBar
64
+ />,
65
+ pre: ({ children }) => <>{children}</>,
66
+ p: ({ children }) => <p>{Children.map(children, renderP)}</p>,
67
+ }}
68
+ >
69
+ {content}
70
+ </ReactMarkdown>
71
+ </>
72
+ )
73
+ }
@@ -5,7 +5,7 @@ import { Button, IconButton } from '@stack-spot/citric-react'
5
5
  import { listToClass, theme } from '@stack-spot/portal-theme'
6
6
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
7
7
  import { last } from 'lodash'
8
- import { useCallback, useEffect, useMemo, useRef } from 'react'
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
9
9
  import { styled } from 'styled-components'
10
10
  import { ButtonAction } from '../types'
11
11
  import { FadingOverflow } from './FadingOverflow'
@@ -152,19 +152,31 @@ export function TabManager<T, Key extends React.Key>(
152
152
  const t = useTranslate(dictionary)
153
153
  const tabList = useRef<HTMLUListElement>(null)
154
154
  const lastNumberOfTabs = useRef(tabs.length)
155
+ const [ariaMessage, setAriaMessage] = useState('')
155
156
 
156
157
  const onClickTab = useCallback((event: React.MouseEvent<HTMLElement, MouseEvent>) => {
157
158
  const target = event.target as HTMLElement
158
159
  if (target.tagName === 'LI') target.querySelector('button')?.click()
159
160
  }, [])
160
161
 
161
- const tabItems = useMemo(() => tabs.map((tab) => (
162
- <li key={keygen(tab)} className={keygen(tab) === active ? 'active' : undefined} onClick={onClickTab}>
163
- <button className="label" onClick={() => onSelect(tab)}><FadingOverflow>{renderLabel(tab)}</FadingOverflow></button>
164
- {tabs.length > 1 &&
165
- <IconButton appearance="text" icon="TimesMini" aria-label={t.close} title={t.close} onClick={() => onRemove(tab)} />}
166
- </li>
167
- )), [tabs, active])
162
+ const tabItems = useMemo(() => tabs.map((tab, idx) => {
163
+ const tabLabel = typeof renderLabel(tab) === 'string'
164
+ ? renderLabel(tab)
165
+ : `${t.tab} ${idx + 1}`
166
+
167
+ return (
168
+ <li key={keygen(tab)} className={keygen(tab) === active ? 'active' : undefined} onClick={onClickTab}>
169
+ <button className="label" aria-label={`${t.chat} ${tabLabel}`} onClick={() => onSelect(tab)}>
170
+ <FadingOverflow>{renderLabel(tab)}</FadingOverflow>
171
+ </button>
172
+ {tabs.length > 1 &&
173
+ <IconButton appearance="text" icon="TimesMini" aria-label={`${tabLabel}`}
174
+ title={t.close} onClick={() => { onRemove(tab), setAriaMessage(`${t.chat} ${tabLabel} ${t.closed}`)
175
+
176
+ // Clears the message after a short time to prevent it from repeating
177
+ setTimeout(() => setAriaMessage(''), 1000)}} />}
178
+ </li>
179
+ )}), [tabs, active])
168
180
 
169
181
  const extras = useMemo(() => buttons.map(({
170
182
  ariaLabel, title, label, onClick, group, icon, appearance, size, className, style, disabled }) => label
@@ -204,6 +216,11 @@ export function TabManager<T, Key extends React.Key>(
204
216
 
205
217
  return (
206
218
  <Tabs $numberOfExtraButtons={buttons.length} className="tabs">
219
+ <div
220
+ aria-live="polite" aria-atomic="true"
221
+ style={{ position: 'absolute', left: '-9999px', width: '1px', height: '1px', overflow: 'hidden' }}>
222
+ {ariaMessage}
223
+ </div>
207
224
  <FadingOverflow className="list-overflow" scroll="arrows" enableHorizontalScrollWithVerticalWheel>
208
225
  <ul ref={tabList}>{tabItems}</ul>
209
226
  </FadingOverflow>
@@ -215,9 +232,15 @@ export function TabManager<T, Key extends React.Key>(
215
232
  const dictionary = {
216
233
  en: {
217
234
  close: 'Close',
235
+ tab: 'Tab',
236
+ chat: 'Chat',
237
+ closed: 'Closed',
218
238
  },
219
239
  pt: {
220
240
  close: 'Fechar',
241
+ tab: 'Aba',
242
+ chat: 'Chat',
243
+ closed: 'Fechado',
221
244
  },
222
245
  } satisfies Dictionary
223
246
 
@@ -1,3 +1,4 @@
1
+ import { ChatRequest } from '@stack-spot/portal-network/api/genAiInference'
1
2
  import { dropRight, last, pull } from 'lodash'
2
3
  import { ulid } from 'ulid'
3
4
  import { AbortedError } from '../AbortedError'
@@ -63,6 +64,11 @@ export interface ChatPropertiesWithOptionalFeatures {
63
64
  * The current LLM (Large Language Model) being used for this chat.
64
65
  */
65
66
  selected_model_id?: string,
67
+
68
+ /**
69
+ * Extra options to be merged into the chat request payload sent to the backend.
70
+ */
71
+ requestOptions?: Pick<ChatRequest, 'deep_search_ks' | 'return_ks_in_response' | 'use_conversation'>,
66
72
  }
67
73
 
68
74
  export interface ChatProperties extends ChatPropertiesWithOptionalFeatures {
package/src/utils/chat.ts CHANGED
@@ -1,31 +1,31 @@
1
- import { FixedChatRequest } from '@stack-spot/portal-network'
1
+ import type { FixedChatRequest } from '@stack-spot/portal-network'
2
2
  import appData from '../app-metadata.json'
3
- import { ChatEntry } from '../state/ChatEntry'
4
- import { ChatState } from '../state/ChatState'
5
- import { defaultLanguage } from './programming-languages'
3
+ import type { ChatEntry } from '../state/ChatEntry'
4
+ import type { ChatState } from '../state/ChatState'
6
5
 
7
- /**
8
- * Builds a conversation context from a ChatState.
9
- *
10
- * The conversation context is needed by most backend services.
11
- *
12
- * @param state the ChatState to build the context from.
13
- * @returns the conversation context ready to be sent to the backend.
14
- */
15
- export function buildConversationContext(state: ChatState, message?: ChatEntry): FixedChatRequest['context'] {
6
+ export function buildConversationContext(state: ChatState, message?: ChatEntry | undefined, userPrompt?: string): FixedChatRequest {
7
+ const requestOptions = state.get('requestOptions')
16
8
  return {
17
- workspace: state.get('workspace')?.id,
9
+ streaming: true,
10
+ show_chat_processing_state: true,
11
+ return_ks_in_response: state.get('features')?.showSourcesInResponse,
12
+ deep_search_ks: false,
13
+ stackspot_knowledge: false,
14
+ user_prompt: userPrompt ?? '',
15
+ use_conversation: true,
18
16
  conversation_id: state.id,
17
+ workspace_id: state.get('workspace')?.id,
19
18
  stack_id: state.get('stack')?.id,
20
- language: state.get('codeLanguage') || (state.get('codeSelection') ? defaultLanguage : undefined),
21
19
  knowledge_sources: state.get('knowledgeSources')?.map(ks => ks.id),
22
20
  upload_ids: message?.getValue().upload?.map(f => f.id),
23
- agent_id: state.get('agent')?.id,
24
- agent_built_in: state.get('agent')?.builtIn,
25
- os: navigator.userAgent,
26
- platform: 'web-widget',
27
- platform_version: navigator.userAgent,
28
- stackspot_ai_version: appData.version,
29
- selected_model_id: state.get('selected_model_id'),
21
+ selected_model: state.get('selected_model_id') ?? null,
22
+ context: {
23
+ os: navigator.userAgent,
24
+ platform: 'web-widget',
25
+ platform_version: navigator.userAgent,
26
+ stackspot_ai_version: appData.version,
27
+ agent_id: state.get('agent')?.id,
28
+ },
29
+ ...requestOptions,
30
30
  }
31
31
  }
@@ -62,7 +62,7 @@ export function extractCodeFromKSDocument(document: DocumentResponse): { languag
62
62
  export function genericSourcesToKnowledgeSources(
63
63
  sources: (SourceStackAi | SourceKnowledgeSource | SourceProjectFile3)[] | undefined,
64
64
  ): KnowledgeSource[] | undefined {
65
- return sources?.filter(s => s.type === 'knowledge_source').map(ks => {
65
+ return sources?.filter(s => s.type === 'knowledge_source' || s.type === 'cross_account').map(ks => {
66
66
  const { document_id: documentId, document_score: documentScore, name, slug } = ks as SourceKnowledgeSource
67
67
  return { documentId, documentScore, name, slug }
68
68
  })
@@ -189,7 +189,7 @@ export const KnowledgeSourcesTab = ({ visibility, allKS, onSubmit, workspaceId,
189
189
  hasNextPage={hasNextPage && !workspaceId}
190
190
  fetchNextPage={fetchNextPage}
191
191
  data={(ks: KnowledgeSourceItemResponse) => ({
192
- idOrSlug: ks.slug, description: ks.description, name: ks.name, listFavorites,
192
+ idOrSlug: ks.id, description: ks.description, name: ks.name, listFavorites,
193
193
  onAddFavorite, onRemoveFavorite,
194
194
  })}
195
195
  emptyResults={<Placeholder title={t.noSearchResults} description={t.noSearchResultsDescription} />}
@@ -21,7 +21,7 @@ const ContextBadge = ({ label, color, dismiss, onDismiss }: ContextBadgeProps) =
21
21
  appearance="none"
22
22
  onClick={onDismiss}
23
23
  title={dismiss}
24
- arial-label={dismiss}
24
+ aria-label={dismiss}
25
25
  style={{ padding: '2px 0 2px 2px', color: 'inherit' }}
26
26
  />}
27
27
  </Badge>