@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.
- 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 +249 -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 +4 -4
- package/dist/chat-interceptors/send-message.js.map +1 -1
- package/dist/components/Code.d.ts.map +1 -1
- package/dist/components/Code.js +9 -3
- package/dist/components/Code.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 +2 -1
- 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/views/Chat/ChatMessage.d.ts +2 -1
- package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
- package/dist/views/Chat/ChatMessage.js +15 -4
- 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/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/index.d.ts.map +1 -1
- package/dist/views/MessageInput/index.js +8 -4
- package/dist/views/MessageInput/index.js.map +1 -1
- package/package.json +2 -2
- 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 +269 -7
- package/src/chat-interceptors/send-message.ts +4 -4
- package/src/components/Code.tsx +16 -3
- 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 +2 -1
- 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/views/Chat/ChatMessage.tsx +38 -8
- package/src/views/Chat/ChatMessages.tsx +4 -1
- package/src/views/Chat/styled.ts +31 -0
- package/src/views/Editor.tsx +3 -4
- package/src/views/MessageInput/ButtonGroup.tsx +1 -1
- package/src/views/MessageInput/index.tsx +9 -5
- 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/components/Code.tsx
CHANGED
|
@@ -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={
|
|
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
|
package/src/state/ChatEntry.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
75
|
-
return new ChatEntry({ agentType: 'bot', type: 'md', content: '' }
|
|
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
|
-
|
|
83
|
+
this.listeners.push(listener)
|
|
97
84
|
return () => {
|
|
98
85
|
pull(this.listeners, listener)
|
|
99
86
|
}
|
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,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,
|
|
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
|
+
}
|
|
@@ -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=
|
|
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
|
-
|
|
82
|
-
<
|
|
83
|
-
|
|
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(
|
|
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
|
|
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) => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { listToClass } from '@stack-spot/portal-theme'
|
|
2
|
-
import {
|
|
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={() =>
|
|
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 +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
|