@stack-spot/ai-chat-widget 0.10.0 → 1.0.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.
- package/dist/AbortedError.d.ts +5 -0
- package/dist/AbortedError.d.ts.map +1 -0
- package/dist/AbortedError.js +7 -0
- package/dist/AbortedError.js.map +1 -0
- package/dist/StackspotAIWidget.d.ts.map +1 -1
- package/dist/StackspotAIWidget.js +11 -3
- package/dist/StackspotAIWidget.js.map +1 -1
- package/dist/chat-interceptors/CustomInputs.d.ts +19 -0
- package/dist/chat-interceptors/CustomInputs.d.ts.map +1 -0
- package/dist/chat-interceptors/CustomInputs.js +62 -0
- package/dist/chat-interceptors/CustomInputs.js.map +1 -0
- package/dist/chat-interceptors/quick-command-questions.d.ts +4 -0
- package/dist/chat-interceptors/quick-command-questions.d.ts.map +1 -0
- package/dist/chat-interceptors/quick-command-questions.js +18 -0
- package/dist/chat-interceptors/quick-command-questions.js.map +1 -0
- package/dist/chat-interceptors/quick-commands.d.ts +3 -1
- package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
- package/dist/chat-interceptors/quick-commands.js +250 -8
- package/dist/chat-interceptors/quick-commands.js.map +1 -1
- package/dist/chat-interceptors/send-message.d.ts +1 -1
- package/dist/chat-interceptors/send-message.d.ts.map +1 -1
- package/dist/chat-interceptors/send-message.js +18 -14
- package/dist/chat-interceptors/send-message.js.map +1 -1
- package/dist/components/AdaptiveTextArea.d.ts +1 -1
- package/dist/components/AdaptiveTextArea.d.ts.map +1 -1
- package/dist/components/AdaptiveTextArea.js +6 -4
- package/dist/components/AdaptiveTextArea.js.map +1 -1
- package/dist/components/AutoFocus.d.ts +6 -0
- package/dist/components/AutoFocus.d.ts.map +1 -0
- package/dist/components/AutoFocus.js +15 -0
- package/dist/components/AutoFocus.js.map +1 -0
- package/dist/components/Fading.d.ts +15 -0
- package/dist/components/Fading.d.ts.map +1 -0
- package/dist/components/Fading.js +31 -0
- package/dist/components/Fading.js.map +1 -0
- package/dist/components/FallbackBoundary/ErrorBoundary.d.ts +3 -0
- package/dist/components/FallbackBoundary/ErrorBoundary.d.ts.map +1 -1
- package/dist/components/FallbackBoundary/ErrorBoundary.js +18 -4
- package/dist/components/FallbackBoundary/ErrorBoundary.js.map +1 -1
- package/dist/components/FallbackBoundary/Loading.js +1 -1
- package/dist/components/FallbackBoundary/Loading.js.map +1 -1
- package/dist/components/FallbackBoundary/index.d.ts +6 -1
- package/dist/components/FallbackBoundary/index.d.ts.map +1 -1
- package/dist/components/FallbackBoundary/index.js +1 -1
- package/dist/components/FallbackBoundary/index.js.map +1 -1
- package/dist/components/OverlayMenu.d.ts +1 -1
- package/dist/components/OverlayMenu.d.ts.map +1 -1
- package/dist/components/OverlayMenu.js +26 -9
- package/dist/components/OverlayMenu.js.map +1 -1
- package/dist/components/RightPanelForm.d.ts.map +1 -1
- package/dist/components/RightPanelForm.js +5 -4
- package/dist/components/RightPanelForm.js.map +1 -1
- package/dist/components/Tooltip/Tooltip.d.ts +3 -1
- package/dist/components/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/components/Tooltip/Tooltip.js +14 -5
- package/dist/components/Tooltip/Tooltip.js.map +1 -1
- package/dist/components/Tooltip/TooltipAPI.d.ts +2 -2
- package/dist/components/Tooltip/TooltipAPI.d.ts.map +1 -1
- package/dist/components/Tooltip/TooltipAPI.js +51 -51
- package/dist/components/Tooltip/TooltipAPI.js.map +1 -1
- package/dist/layout.css +5 -0
- package/dist/regex.d.ts +2 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +2 -0
- package/dist/regex.js.map +1 -0
- package/dist/right-panel/DefaultPanel.d.ts.map +1 -1
- package/dist/right-panel/DefaultPanel.js +3 -1
- package/dist/right-panel/DefaultPanel.js.map +1 -1
- package/dist/right-panel/constants.d.ts +2 -0
- package/dist/right-panel/constants.d.ts.map +1 -0
- package/dist/right-panel/constants.js +2 -0
- package/dist/right-panel/constants.js.map +1 -0
- package/dist/right-panel/hooks.d.ts.map +1 -1
- package/dist/right-panel/hooks.js +2 -1
- package/dist/right-panel/hooks.js.map +1 -1
- package/dist/state/ChatEntry.d.ts +8 -8
- package/dist/state/ChatEntry.d.ts.map +1 -1
- package/dist/state/ChatEntry.js +4 -16
- package/dist/state/ChatEntry.js.map +1 -1
- package/dist/state/ChatState.d.ts +13 -1
- package/dist/state/ChatState.d.ts.map +1 -1
- package/dist/state/ChatState.js +38 -3
- package/dist/state/ChatState.js.map +1 -1
- package/dist/state/ObservableState.d.ts +1 -1
- package/dist/state/ObservableState.d.ts.map +1 -1
- package/dist/state/ObservableState.js.map +1 -1
- package/dist/utils/chat.d.ts.map +1 -1
- package/dist/utils/chat.js +3 -2
- package/dist/utils/chat.js.map +1 -1
- package/dist/utils/knowledge-source.d.ts +2 -2
- package/dist/utils/knowledge-source.d.ts.map +1 -1
- package/dist/utils/knowledge-source.js +4 -6
- package/dist/utils/knowledge-source.js.map +1 -1
- package/dist/utils/programming-languages.d.ts +1 -0
- package/dist/utils/programming-languages.d.ts.map +1 -1
- package/dist/utils/programming-languages.js +1 -0
- package/dist/utils/programming-languages.js.map +1 -1
- package/dist/utils/string.d.ts +2 -0
- package/dist/utils/string.d.ts.map +1 -0
- package/dist/utils/string.js +7 -0
- package/dist/utils/string.js.map +1 -0
- package/dist/utils/url.d.ts +2 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +8 -0
- package/dist/utils/url.js.map +1 -0
- package/dist/views/Chat/ChatMessage.d.ts +2 -1
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +38 -5
- package/dist/views/Chat/ChatMessage.js.map +1 -1
- package/dist/views/Chat/ChatMessages.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessages.js +1 -1
- package/dist/views/Chat/ChatMessages.js.map +1 -1
- package/dist/views/Chat/styled.d.ts.map +1 -1
- package/dist/views/Chat/styled.js +31 -0
- package/dist/views/Chat/styled.js.map +1 -1
- package/dist/views/ChatHistory/ChatHistoryPanel.d.ts.map +1 -1
- package/dist/views/ChatHistory/ChatHistoryPanel.js +2 -1
- package/dist/views/ChatHistory/ChatHistoryPanel.js.map +1 -1
- package/dist/views/ChatHistory/HistoryItem.d.ts.map +1 -1
- package/dist/views/ChatHistory/HistoryItem.js +10 -1
- package/dist/views/ChatHistory/HistoryItem.js.map +1 -1
- package/dist/views/Editor.d.ts.map +1 -1
- package/dist/views/Editor.js +3 -4
- package/dist/views/Editor.js.map +1 -1
- package/dist/views/MessageInput/ButtonGroup.d.ts +1 -1
- package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
- package/dist/views/MessageInput/QuickCommandSelector.d.ts +6 -0
- package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -0
- package/dist/views/MessageInput/QuickCommandSelector.js +137 -0
- package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -0
- package/dist/views/MessageInput/index.d.ts.map +1 -1
- package/dist/views/MessageInput/index.js +10 -4
- package/dist/views/MessageInput/index.js.map +1 -1
- package/dist/views/MessageInput/styled.d.ts.map +1 -1
- package/dist/views/MessageInput/styled.js +137 -0
- package/dist/views/MessageInput/styled.js.map +1 -1
- package/package.json +3 -3
- package/src/AbortedError.ts +7 -0
- package/src/StackspotAIWidget.tsx +13 -3
- package/src/chat-interceptors/CustomInputs.ts +70 -0
- package/src/chat-interceptors/quick-command-questions.ts +15 -0
- package/src/chat-interceptors/quick-commands.ts +270 -7
- package/src/chat-interceptors/send-message.ts +27 -15
- package/src/components/AdaptiveTextArea.tsx +9 -4
- package/src/components/AutoFocus.tsx +20 -0
- package/src/components/Fading.tsx +46 -0
- package/src/components/FallbackBoundary/ErrorBoundary.tsx +26 -3
- package/src/components/FallbackBoundary/Loading.tsx +1 -1
- package/src/components/FallbackBoundary/index.tsx +7 -2
- package/src/components/OverlayMenu.tsx +59 -19
- package/src/components/RightPanelForm.tsx +12 -9
- package/src/components/Tooltip/Tooltip.tsx +24 -5
- package/src/components/Tooltip/TooltipAPI.ts +42 -42
- package/src/layout.css +5 -0
- package/src/regex.ts +1 -0
- package/src/right-panel/DefaultPanel.tsx +14 -9
- package/src/right-panel/constants.ts +1 -0
- package/src/right-panel/hooks.tsx +2 -1
- package/src/state/ChatEntry.ts +7 -20
- package/src/state/ChatState.ts +41 -3
- package/src/state/ObservableState.ts +1 -1
- package/src/utils/chat.ts +3 -2
- package/src/utils/knowledge-source.ts +6 -8
- package/src/utils/programming-languages.ts +2 -0
- package/src/utils/string.ts +6 -0
- package/src/utils/url.ts +8 -0
- package/src/views/Chat/ChatMessage.tsx +67 -13
- package/src/views/Chat/ChatMessages.tsx +4 -1
- package/src/views/Chat/styled.ts +31 -0
- package/src/views/ChatHistory/ChatHistoryPanel.tsx +3 -2
- package/src/views/ChatHistory/HistoryItem.tsx +11 -2
- package/src/views/Editor.tsx +3 -4
- package/src/views/MessageInput/ButtonGroup.tsx +1 -1
- package/src/views/MessageInput/QuickCommandSelector.tsx +210 -0
- package/src/views/MessageInput/index.tsx +12 -4
- package/src/views/MessageInput/styled.ts +137 -0
- package/dist/components/Editor.d.ts +0 -9
- package/dist/components/Editor.d.ts.map +0 -1
- package/dist/components/Editor.js +0 -2
- package/dist/components/Editor.js.map +0 -1
- package/src/components/Editor.tsx +0 -12
package/src/state/ChatState.ts
CHANGED
|
@@ -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 = (
|
|
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
|
-
|
|
67
|
-
|
|
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
|
}
|
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,13 +18,13 @@ 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,
|
|
24
25
|
os: navigator.userAgent,
|
|
25
26
|
platform: 'web-widget',
|
|
26
27
|
platform_version: navigator.userAgent,
|
|
27
|
-
stackspot_ai_version: '
|
|
28
|
+
stackspot_ai_version: '1.0.0',
|
|
28
29
|
}
|
|
29
30
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DocumentResponse,
|
|
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 |
|
|
47
|
+
sources: (SourceStackAi | SourceKnowledgeSource | SourceProjectFile4)[] | undefined,
|
|
48
48
|
): KnowledgeSource[] | undefined {
|
|
49
|
-
return sources?.filter(s => s.type === 'knowledge_source').map(ks =>
|
|
50
|
-
documentId: ks
|
|
51
|
-
documentScore
|
|
52
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|
package/src/utils/url.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const stkAIDomain = /^https:\/\/ai(\.\w+)?\.stackspot\.com$/
|
|
2
|
+
const localhostDomain = /^http:\/\/localhost:\d+$/
|
|
3
|
+
const aiPrd = 'https://ai.stackspot.com'
|
|
4
|
+
|
|
5
|
+
export function getUrlToStackSpotAI() {
|
|
6
|
+
const current = location.origin
|
|
7
|
+
return stkAIDomain.test(current) || localhostDomain.test(current) ? current : aiPrd
|
|
8
|
+
}
|
|
@@ -1,26 +1,49 @@
|
|
|
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'
|
|
11
|
+
import { ChatState } from '../../state/ChatState'
|
|
12
|
+
import { buildConversationContext } from '../../utils/chat'
|
|
10
13
|
import { useDateFormatter } from '../../utils/date'
|
|
14
|
+
import { getSizeOfString } from '../../utils/string'
|
|
11
15
|
import { AgentInfo } from './AgentInfo'
|
|
12
16
|
import { useChatScrollToBottomEffect } from './chat-scroll'
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
async function onCopyCode(code: string, messageId: string, chat: ChatState) {
|
|
19
|
+
try {
|
|
20
|
+
await aiClient.createEvent.mutate({
|
|
21
|
+
body: [{
|
|
22
|
+
type: 'code_copied',
|
|
23
|
+
code,
|
|
24
|
+
context: buildConversationContext(chat),
|
|
25
|
+
size: getSizeOfString(code),
|
|
26
|
+
generated_at: new Date().getTime(),
|
|
27
|
+
message_id: messageId,
|
|
28
|
+
}],
|
|
29
|
+
})
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.warn('Failed to register event: code copied.')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const ChatMessage = ({ message, username, isLast }: { message: ChatEntry, username: string, isLast: boolean }) => {
|
|
15
37
|
const t = useTranslate(dictionary)
|
|
16
38
|
const [liked, setLiked] = useState<boolean | undefined>()
|
|
17
39
|
const entry = useChatEntry(message)
|
|
18
40
|
const dateFormatter = useDateFormatter()
|
|
19
41
|
const userInfo = entry.agentType === 'user' ? <Avatar size="xs">{username}</Avatar> : <AgentInfo agent={entry.agent} />
|
|
20
42
|
const date = new Date(entry.updated ?? '')
|
|
21
|
-
const
|
|
43
|
+
const shouldShowFooter = entry.updated && !isNaN(date.getTime())
|
|
22
44
|
const ref = useRef<HTMLLIElement>(null)
|
|
23
45
|
const widget = useWidget()
|
|
46
|
+
const chat = useCurrentChat()
|
|
24
47
|
useChatScrollToBottomEffect(ref, [entry])
|
|
25
48
|
|
|
26
49
|
const detailKS = useCallback(({ name, slug, documentScore, documentId }: Required<TextChatEntry>['knowledgeSources'][number]) => {
|
|
@@ -28,6 +51,14 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
|
|
|
28
51
|
widget.set('panel', 'ks-details')
|
|
29
52
|
}, [])
|
|
30
53
|
|
|
54
|
+
const runAction = useCallback((action: SerializableAction) => {
|
|
55
|
+
if (action.type === 'link') {
|
|
56
|
+
window.open(action.exec, '_blank')
|
|
57
|
+
} else {
|
|
58
|
+
chat.pushMessage(ChatEntry.createUserEntry(action.exec))
|
|
59
|
+
}
|
|
60
|
+
}, [])
|
|
61
|
+
|
|
31
62
|
const { like, dislike } = useMemo(() => {
|
|
32
63
|
async function feedback(like: boolean) {
|
|
33
64
|
if (!entry.messageId || like === liked) return
|
|
@@ -58,8 +89,29 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
|
|
|
58
89
|
<li className={entry.agentType} ref={ref}>
|
|
59
90
|
<div className="chat-message">
|
|
60
91
|
<div className="user-info">{userInfo}</div>
|
|
61
|
-
{entry.content && <div className=
|
|
62
|
-
{entry.
|
|
92
|
+
{entry.content && <div className={listToClass(['message-content', entry.card && 'card'])}>
|
|
93
|
+
{entry.badges?.length && <div className="badges">
|
|
94
|
+
{entry.badges.map((b, index) => <Badge key={index} palette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
|
|
95
|
+
</div>}
|
|
96
|
+
{entry.type === 'md'
|
|
97
|
+
? <Markdown onCopyCode={(code) => onCopyCode(code, entry.messageId ?? '', chat)}>{entry.content}</Markdown>
|
|
98
|
+
: <p className="plain-text">{entry.content}</p>
|
|
99
|
+
}
|
|
100
|
+
{entry.actions?.length && <div className="actions">
|
|
101
|
+
{entry.actions.map(
|
|
102
|
+
(a, index) => (
|
|
103
|
+
<Button
|
|
104
|
+
key={index}
|
|
105
|
+
appearance={a.appearance === 'primary' ? 'contained' : 'outlined'}
|
|
106
|
+
colorScheme="inverse"
|
|
107
|
+
onClick={() => runAction(a)}
|
|
108
|
+
disabled={!isLast}
|
|
109
|
+
>
|
|
110
|
+
{a.title}
|
|
111
|
+
</Button>
|
|
112
|
+
),
|
|
113
|
+
)}
|
|
114
|
+
</div>}
|
|
63
115
|
</div>}
|
|
64
116
|
</div>
|
|
65
117
|
{entry.error && (
|
|
@@ -76,11 +128,13 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
|
|
|
76
128
|
</li>
|
|
77
129
|
))}</ul>
|
|
78
130
|
</div>}
|
|
79
|
-
<div className="message-footer">
|
|
131
|
+
{shouldShowFooter && <div className="message-footer">
|
|
80
132
|
{entry.agentType === 'bot' && !entry.error && <div className="message-actions">
|
|
81
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
133
|
+
{entry.type === 'md' && (
|
|
134
|
+
<IconButton title={t.copy} aria-label={t.copy} onClick={() => navigator.clipboard.writeText(entry.content)}>
|
|
135
|
+
<Copy />
|
|
136
|
+
</IconButton>
|
|
137
|
+
)}
|
|
84
138
|
{entry.messageId && (
|
|
85
139
|
<>
|
|
86
140
|
<IconButton title={t.like} aria-label={t.like} onClick={like}>
|
|
@@ -92,8 +146,8 @@ export const ChatMessage = ({ message, username }: { message: ChatEntry, usernam
|
|
|
92
146
|
</>
|
|
93
147
|
)}
|
|
94
148
|
</div>}
|
|
95
|
-
|
|
96
|
-
</div>
|
|
149
|
+
<Text appearance="microtext1" className="chat-date">{dateFormatter.formatForChatMessage(date)}</Text>
|
|
150
|
+
</div>}
|
|
97
151
|
</li>
|
|
98
152
|
)
|
|
99
153
|
}
|
|
@@ -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(
|
|
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
|
}
|
package/src/views/Chat/styled.ts
CHANGED
|
@@ -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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { aiClient } from '@stack-spot/portal-network'
|
|
2
2
|
import InfiniteScroll from 'react-infinite-scroll-component'
|
|
3
|
+
import { AutoFocus } from '../../components/AutoFocus'
|
|
3
4
|
import { HistoryList } from '../../components/HistoryList'
|
|
4
5
|
import { MessageInterceptor } from '../../state/ChatState'
|
|
5
6
|
import { HistoryItem } from './HistoryItem'
|
|
@@ -7,7 +8,7 @@ import { HistoryItem } from './HistoryItem'
|
|
|
7
8
|
export const ChatHistoryPanel = ({ interceptors }: { interceptors: MessageInterceptor[] }) => {
|
|
8
9
|
const [chats, { fetchNextPage, hasNextPage }] = aiClient.chats.useInfiniteQuery({ size: 40 })
|
|
9
10
|
return (
|
|
10
|
-
<
|
|
11
|
+
<AutoFocus id="chatHistoryList" style={{ height: '100%', overflow: 'auto' }}>
|
|
11
12
|
<InfiniteScroll
|
|
12
13
|
scrollableTarget="chatHistoryList"
|
|
13
14
|
dataLength={chats.length}
|
|
@@ -23,6 +24,6 @@ export const ChatHistoryPanel = ({ interceptors }: { interceptors: MessageInterc
|
|
|
23
24
|
style={{ marginRight: '6px' }}
|
|
24
25
|
/>
|
|
25
26
|
</InfiniteScroll>
|
|
26
|
-
</
|
|
27
|
+
</AutoFocus>
|
|
27
28
|
)
|
|
28
29
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { IconBox, Input } from '@citric/core'
|
|
2
2
|
import { Check, Download, EllipsisHorizontal, Pencil, Trash } from '@citric/icons'
|
|
3
3
|
import { IconButton, LoadingCircular } from '@citric/ui'
|
|
4
|
+
import { focusNextIgnoringChildren } from '@stack-spot/portal-components'
|
|
4
5
|
import { aiClient } from '@stack-spot/portal-network'
|
|
5
6
|
import { ConversationResponse } from '@stack-spot/portal-network/api/ai'
|
|
6
7
|
import { theme } from '@stack-spot/portal-theme'
|
|
@@ -26,6 +27,7 @@ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse
|
|
|
26
27
|
const [isDeleted, setDeleted] = useState(false)
|
|
27
28
|
const renameInput = useRef<HTMLInputElement>(null)
|
|
28
29
|
const widget = useWidget()
|
|
30
|
+
const overlayRef = useRef<HTMLDivElement>(null)
|
|
29
31
|
|
|
30
32
|
useEffect(() => {
|
|
31
33
|
if (isRenaming) renameInput.current?.focus()
|
|
@@ -52,6 +54,7 @@ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
const onDownload = useCallback(async () => {
|
|
57
|
+
setTimeout(() => focusNextIgnoringChildren(overlayRef.current), 10)
|
|
55
58
|
setLoading(true)
|
|
56
59
|
try {
|
|
57
60
|
const content = await aiClient.downloadChat.mutate({ conversationId: item.id })
|
|
@@ -65,6 +68,7 @@ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse
|
|
|
65
68
|
|
|
66
69
|
const onDelete = useCallback(async () => {
|
|
67
70
|
setDeleted(true)
|
|
71
|
+
setTimeout(() => overlayRef.current?.focus(), 10)
|
|
68
72
|
try {
|
|
69
73
|
await aiClient.deleteChat.mutate({ conversationId: item.id })
|
|
70
74
|
aiClient.chats.invalidate()
|
|
@@ -124,14 +128,19 @@ export const HistoryItem = ({ item, interceptors }: { item: ConversationResponse
|
|
|
124
128
|
ref={renameInput}
|
|
125
129
|
value={renamed}
|
|
126
130
|
onChange={e => setRenamed(e.target.value)}
|
|
127
|
-
onKeyDown={e =>
|
|
131
|
+
onKeyDown={(e) => {
|
|
132
|
+
if (['Enter', 'Escape'].includes(e.key)) {
|
|
133
|
+
e.key === 'Enter' ? onSubmitRename() : setRenaming(false)
|
|
134
|
+
setTimeout(() => overlayRef.current?.focus(), 10)
|
|
135
|
+
}
|
|
136
|
+
}}
|
|
128
137
|
/>
|
|
129
138
|
<IconButton onClick={onSubmitRename}><Check /></IconButton>
|
|
130
139
|
</>
|
|
131
140
|
) : (
|
|
132
141
|
<>
|
|
133
142
|
<button className="label" onClick={onSelect} disabled={isLoading}>{title}</button>
|
|
134
|
-
{isLoading ? <LoadingCircular size="xs" /> : <OverlayMenu actions={actions} position="left">
|
|
143
|
+
{isLoading ? <LoadingCircular size="xs" /> : <OverlayMenu actions={actions} position="left" ref={overlayRef}>
|
|
135
144
|
<IconBox><EllipsisHorizontal /></IconBox>
|
|
136
145
|
</OverlayMenu>}
|
|
137
146
|
</>
|
package/src/views/Editor.tsx
CHANGED
|
@@ -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') ||
|
|
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') ||
|
|
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
|
|
16
|
+
onCancel: () => void,
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export const ButtonGroup = ({ features, onSend, onCancel, expanded, setExpanded, isLoading }: ButtonGroupProps) => {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { IconBox, Text } from '@citric/core'
|
|
2
|
+
import { ExternalLink, QuickCommand } from '@citric/icons'
|
|
3
|
+
import { IconButton } from '@citric/ui'
|
|
4
|
+
import { useKeyboardControls } from '@stack-spot/portal-components'
|
|
5
|
+
import { aiClient } from '@stack-spot/portal-network'
|
|
6
|
+
import { QuickCommandListResponse, VisibilityLevelEnum } from '@stack-spot/portal-network/api/ai'
|
|
7
|
+
import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
|
|
8
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
9
|
+
import { Fading } from '../../components/Fading'
|
|
10
|
+
import { FallbackBoundary } from '../../components/FallbackBoundary'
|
|
11
|
+
import { useCurrentChat, useCurrentChatState } from '../../context/hooks'
|
|
12
|
+
import { quickCommandRegex } from '../../regex'
|
|
13
|
+
import { getUrlToStackSpotAI } from '../../utils/url'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ContentProps extends Props {
|
|
20
|
+
filter?: string,
|
|
21
|
+
onClose: () => void,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface ListProps {
|
|
25
|
+
filter?: string,
|
|
26
|
+
visibility?: VisibilityLevelEnum,
|
|
27
|
+
onSelect: (slug: string) => void,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ItemProps {
|
|
31
|
+
qc: QuickCommandListResponse,
|
|
32
|
+
onSelect: (slug: string) => void,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sections = [undefined, 'personal', 'workspace', 'account', 'shared'] as const
|
|
36
|
+
|
|
37
|
+
const CommandListItem = ({ qc, onSelect }: ItemProps) => {
|
|
38
|
+
const t = useTranslate(dictionary)
|
|
39
|
+
return (
|
|
40
|
+
<li>
|
|
41
|
+
<button
|
|
42
|
+
className="qc"
|
|
43
|
+
onClick={() => onSelect(qc.slug)}
|
|
44
|
+
// the following line prevents a new line character in the message when the user presses enter to select a qc.
|
|
45
|
+
onKeyDown={e => e.key === 'Enter' && e.preventDefault()}
|
|
46
|
+
onFocus={e => e.target.closest('li')?.classList.add('focus')}
|
|
47
|
+
onBlur={e => e.target.closest('li')?.classList.remove('focus')}
|
|
48
|
+
>
|
|
49
|
+
<p className="qc-title">/{qc.slug}</p>
|
|
50
|
+
<p className="qc-description">{qc.description}</p>
|
|
51
|
+
</button>
|
|
52
|
+
<IconButton as="a" title={t.openQC} aria-label={t.openQC} href={`${getUrlToStackSpotAI()}/quick-command/${qc.slug}`} target="_blank">
|
|
53
|
+
<ExternalLink />
|
|
54
|
+
</IconButton>
|
|
55
|
+
</li>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const CommandList = ({ filter, visibility, onSelect }: ListProps) => {
|
|
60
|
+
const t = useTranslate(dictionary)
|
|
61
|
+
const quickCommands = aiClient.quickCommands.useQuery({ order: 'a-to-z' })
|
|
62
|
+
let filtered = quickCommands
|
|
63
|
+
|
|
64
|
+
if (visibility || filter) {
|
|
65
|
+
const lowerFilter = filter?.toLocaleLowerCase()
|
|
66
|
+
filtered = quickCommands.filter(
|
|
67
|
+
qc => (!lowerFilter || qc.slug.toLocaleLowerCase().startsWith(lowerFilter)) && (!visibility || qc.visibility_level === visibility),
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
if (!quickCommands.length) return <Text className="empty" colorScheme="light.700">{t.noData}</Text>
|
|
71
|
+
if (!filtered.length) return <Text className="empty" colorScheme="light.700">{t.noResults}</Text>
|
|
72
|
+
return (
|
|
73
|
+
<ul className="command-list">
|
|
74
|
+
{filtered.map(qc => <CommandListItem key={qc.id} qc={qc} onSelect={onSelect} />)}
|
|
75
|
+
</ul>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const SelectorContent = ({ filter, onClose, inputRef }: ContentProps) => {
|
|
80
|
+
const t = useTranslate(dictionary)
|
|
81
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
82
|
+
const chat = useCurrentChat()
|
|
83
|
+
const [visibility, setVisibility] = useState<VisibilityLevelEnum | undefined>()
|
|
84
|
+
|
|
85
|
+
const onSelectQC = useCallback((slug: string) => {
|
|
86
|
+
const newValue = `/${slug}`
|
|
87
|
+
chat.set('nextMessage', newValue)
|
|
88
|
+
onClose()
|
|
89
|
+
if (!inputRef.current) return
|
|
90
|
+
// the following line prevents bugs by setting the text area value before react gets the chance to.
|
|
91
|
+
inputRef.current.value = newValue
|
|
92
|
+
inputRef.current.focus()
|
|
93
|
+
}, [])
|
|
94
|
+
|
|
95
|
+
useKeyboardControls({
|
|
96
|
+
querySelectors: '.tabs button, button.qc',
|
|
97
|
+
disableTabBehavior: true,
|
|
98
|
+
onPressEscape: onClose,
|
|
99
|
+
onPressArrowLeft: () => (ref.current?.querySelector('.tabs button.active') as HTMLElement)?.focus(),
|
|
100
|
+
onPressArrowRight: () => (ref.current?.querySelector('button.qc') as HTMLElement)?.focus(),
|
|
101
|
+
ref,
|
|
102
|
+
}, [])
|
|
103
|
+
|
|
104
|
+
function createSectionItem(action: VisibilityLevelEnum | undefined) {
|
|
105
|
+
return (
|
|
106
|
+
<li key={action ?? 'all'}>
|
|
107
|
+
<button className={visibility === action ? 'active' : ''} onFocus={() => setVisibility(action)}>
|
|
108
|
+
{t[action || 'all']}
|
|
109
|
+
</button>
|
|
110
|
+
</li>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<div ref={ref}>
|
|
116
|
+
<header>
|
|
117
|
+
<IconBox><QuickCommand /></IconBox>
|
|
118
|
+
<Text as="h3">QUICK COMMANDS</Text>
|
|
119
|
+
</header>
|
|
120
|
+
<div className="body">
|
|
121
|
+
<ul className="tabs">{sections.map(createSectionItem)}</ul>
|
|
122
|
+
<FallbackBoundary message={t.error} mini>
|
|
123
|
+
<CommandList onSelect={onSelectQC} filter={filter} visibility={visibility} />
|
|
124
|
+
</FallbackBoundary>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const QuickCommandSelector = ({ inputRef }: Props) => {
|
|
131
|
+
const value = useCurrentChatState('nextMessage') ?? ''
|
|
132
|
+
const filter = useMemo(() => value === '/' || quickCommandRegex.test(value) ? value.substring(1) : undefined, [value])
|
|
133
|
+
const [isClosed, setClosed] = useState(false)
|
|
134
|
+
const selectorRef = useRef<HTMLDivElement>(null)
|
|
135
|
+
const shouldRender = filter !== undefined && !isClosed
|
|
136
|
+
|
|
137
|
+
// Resets the closed state whenever the message input is cleared
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!value) setClosed(false)
|
|
140
|
+
}, [value])
|
|
141
|
+
|
|
142
|
+
// Creates the following behavior while the user types in the message input:
|
|
143
|
+
// auto-complete on tab; move focus to the qc panel on press up or down; and close the qc panel on esc.
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
function getFirst() {
|
|
146
|
+
return selectorRef.current?.querySelector('.qc') as HTMLElement | null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function onKeyDown(event: Event) {
|
|
150
|
+
const key = (event as KeyboardEvent).key
|
|
151
|
+
if (!selectorRef.current) return
|
|
152
|
+
if (key === 'Tab') {
|
|
153
|
+
getFirst()?.click()
|
|
154
|
+
event.preventDefault()
|
|
155
|
+
}
|
|
156
|
+
else if (key === 'ArrowDown' || key === 'ArrowUp') {
|
|
157
|
+
getFirst()?.focus()
|
|
158
|
+
event.preventDefault()
|
|
159
|
+
}
|
|
160
|
+
if (key === 'Escape') {
|
|
161
|
+
setClosed(true)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
inputRef.current?.addEventListener('keydown', onKeyDown)
|
|
166
|
+
return () => inputRef.current?.removeEventListener('keydown', onKeyDown)
|
|
167
|
+
}, [])
|
|
168
|
+
|
|
169
|
+
// Closes the panel when the user clicks outside the qc panel or the message input.
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (!shouldRender) return
|
|
172
|
+
function onClickOut(e: Event) {
|
|
173
|
+
const target = e.target as HTMLElement | null
|
|
174
|
+
if (!selectorRef.current?.contains(target) && !inputRef.current?.contains(target)) setClosed(true)
|
|
175
|
+
}
|
|
176
|
+
document.addEventListener('click', onClickOut)
|
|
177
|
+
return () => document.removeEventListener('click', onClickOut)
|
|
178
|
+
}, [shouldRender])
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<Fading visible={shouldRender} ref={selectorRef} className="quick-command-selector">
|
|
182
|
+
<SelectorContent filter={filter} onClose={() => setClosed(true)} inputRef={inputRef} />
|
|
183
|
+
</Fading>
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const dictionary = {
|
|
188
|
+
en: {
|
|
189
|
+
all: 'All',
|
|
190
|
+
personal: 'Personal',
|
|
191
|
+
account: 'Account',
|
|
192
|
+
shared: 'Shared',
|
|
193
|
+
workspace: 'Workspace',
|
|
194
|
+
error: 'Could not load the quick commands.',
|
|
195
|
+
noData: 'You don\'t have any quick command yet.',
|
|
196
|
+
noResults: 'There are no quick commands to show here.',
|
|
197
|
+
openQC: 'Open this quick command\'s settings in a new tab.',
|
|
198
|
+
},
|
|
199
|
+
pt: {
|
|
200
|
+
all: 'Todos',
|
|
201
|
+
personal: 'Pessoal',
|
|
202
|
+
account: 'Conta',
|
|
203
|
+
shared: 'Compartilhado',
|
|
204
|
+
workspace: 'Workspace',
|
|
205
|
+
error: 'Não foi possível carregar os quick commands.',
|
|
206
|
+
noData: 'Você ainda não possui quick commands.',
|
|
207
|
+
noResults: 'Não há quick commands para mostrar aqui.',
|
|
208
|
+
openQC: 'Abra as configurações deste quick command em uma nova aba.',
|
|
209
|
+
},
|
|
210
|
+
} satisfies Dictionary
|