Skip to content

Commit 28d462a

Browse files
committed
feat: ai agent
1 parent 3b554c8 commit 28d462a

File tree

34 files changed

+3249
-101
lines changed

34 files changed

+3249
-101
lines changed

package-lock.json

Lines changed: 706 additions & 98 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/containers/App/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {Helmet} from 'react-helmet-async';
66
import {connect} from 'react-redux';
77

88
import {ErrorBoundary} from '../../components/ErrorBoundary/ErrorBoundary';
9+
import {ChatPanel} from '../../features/chat';
910
import type {RootState} from '../../store';
1011
import {Navigation} from '../AsideNavigation/Navigation';
1112
import ReduxTooltip from '../ReduxTooltip/ReduxTooltip';
@@ -43,6 +44,7 @@ function App({
4344
</ErrorBoundary>
4445
</Navigation>
4546
</ContentWrapper>
47+
<ChatPanel />
4648
<ReduxTooltip />
4749
</Providers>
4850
);

src/containers/Header/Header.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import {useLocation} from 'react-router-dom';
66

77
import {getConnectToDBDialog} from '../../components/ConnectToDB/ConnectToDBDialog';
88
import {InternalLink} from '../../components/InternalLink';
9+
import {AIAssistantButton} from '../../features/chat/components/AIAssistantButton/AIAssistantButton';
910
import {useAddClusterFeatureAvailable} from '../../store/reducers/capabilities/hooks';
1011
import {useClusterBaseInfo} from '../../store/reducers/cluster/cluster';
1112
import {uiFactory} from '../../uiFactory/uiFactory';
1213
import {cn} from '../../utils/cn';
13-
import {DEVELOPER_UI_TITLE} from '../../utils/constants';
14+
import {DEVELOPER_UI_TITLE, ENABLE_AI_ASSISTANT} from '../../utils/constants';
1415
import {createDeveloperUIInternalPageHref} from '../../utils/developerUI/developerUI';
15-
import {useTypedSelector} from '../../utils/hooks';
16+
import {useSetting, useTypedSelector} from '../../utils/hooks';
1617
import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery';
1718
import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedToMakeChanges';
1819

@@ -27,6 +28,7 @@ function Header() {
2728
const {page, pageBreadcrumbsOptions} = useTypedSelector((state) => state.header);
2829
const singleClusterMode = useTypedSelector((state) => state.singleClusterMode);
2930
const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges();
31+
const [isAiAssistantEnabled] = useSetting(ENABLE_AI_ASSISTANT);
3032

3133
const {title: clusterTitle} = useClusterBaseInfo();
3234

@@ -76,6 +78,10 @@ function Header() {
7678
);
7779
}
7880

81+
if (isAiAssistantEnabled) {
82+
elements.push(<AIAssistantButton />);
83+
}
84+
7985
if (!isClustersPage && isUserAllowedToMakeChanges) {
8086
elements.push(
8187
<Button view="flat" href={createDeveloperUIInternalPageHref()} target="_blank">

src/containers/UserSettings/i18n/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
"settings.editor.codeAssistant.title": "Code Assistant",
1818
"settings.editor.codeAssistant.description": "Use Code Assistant for autocomplete.",
1919

20+
"settings.editor.aiAssistant.title": "AI Assistant",
21+
"settings.editor.aiAssistant.description": "Show AI Assistant.",
22+
2023
"settings.editor.queryStreaming.title": "Query Streaming",
2124
"settings.editor.queryStreaming.description": "Use streaming api for query results.",
2225

src/containers/UserSettings/settings.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {createNextState} from '@reduxjs/toolkit';
55
import {
66
AUTOCOMPLETE_ON_ENTER,
77
BINARY_DATA_IN_PLAIN_TEXT_DISPLAY,
8+
ENABLE_AI_ASSISTANT,
89
ENABLE_AUTOCOMPLETE,
910
ENABLE_CODE_ASSISTANT,
1011
ENABLE_NETWORK_TABLE_KEY,
@@ -129,6 +130,12 @@ export const enableCodeAssistantSetting: SettingProps = {
129130
description: i18n('settings.editor.codeAssistant.description'),
130131
};
131132

133+
export const enableAiAssistantSetting: SettingProps = {
134+
settingKey: ENABLE_AI_ASSISTANT,
135+
title: i18n('settings.editor.aiAssistant.title'),
136+
description: i18n('settings.editor.aiAssistant.title'),
137+
};
138+
132139
export const enableQueryStreamingSetting: SettingProps = {
133140
settingKey: ENABLE_QUERY_STREAMING,
134141
title: i18n('settings.editor.queryStreaming.title'),
@@ -220,16 +227,24 @@ export const aboutPage: SettingsPage = {
220227
export function getUserSettings({
221228
singleClusterMode,
222229
codeAssistantConfigured,
230+
aiAssistantConfigured,
223231
}: {
224232
singleClusterMode: boolean;
225233
codeAssistantConfigured?: boolean;
234+
aiAssistantConfigured?: boolean;
226235
}) {
227-
const experiments = singleClusterMode
236+
let experiments = singleClusterMode
228237
? experimentsPage
229238
: createNextState(experimentsPage, (draft) => {
230239
draft.sections[0].settings.push(useClusterBalancerAsBackendSetting);
231240
});
232241

242+
experiments = aiAssistantConfigured
243+
? createNextState(experiments, (draft) => {
244+
draft.sections[0].settings.push(enableAiAssistantSetting);
245+
})
246+
: experiments;
247+
233248
const editor = codeAssistantConfigured
234249
? createNextState(editorPage, (draft) => {
235250
draft.sections[0].settings.push(enableCodeAssistantSetting);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
.ai-assistant-button {
2+
position: relative;
3+
4+
&__indicator {
5+
position: absolute;
6+
top: 4px;
7+
right: 4px;
8+
width: 8px;
9+
height: 8px;
10+
border-radius: 50%;
11+
pointer-events: none;
12+
13+
&--streaming {
14+
background-color: var(--g-color-text-info);
15+
animation: pulse 1.5s ease-in-out infinite;
16+
}
17+
18+
&--unread {
19+
background-color: var(--g-color-text-danger);
20+
}
21+
}
22+
23+
&--has-messages {
24+
.ai-assistant-button__indicator--unread {
25+
display: block;
26+
}
27+
}
28+
}
29+
30+
@keyframes pulse {
31+
0% {
32+
opacity: 1;
33+
transform: scale(1);
34+
}
35+
50% {
36+
opacity: 0.5;
37+
transform: scale(1.2);
38+
}
39+
100% {
40+
opacity: 1;
41+
transform: scale(1);
42+
}
43+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {Star} from '@gravity-ui/icons';
2+
import {Button, Icon} from '@gravity-ui/uikit';
3+
import {useSelector} from 'react-redux';
4+
5+
import {useChat} from '../../hooks/useChat';
6+
import type {ChatState} from '../../types/chat';
7+
8+
import './AIAssistantButton.scss';
9+
10+
interface AIAssistantButtonProps {
11+
className?: string;
12+
}
13+
14+
export const AIAssistantButton = ({className}: AIAssistantButtonProps) => {
15+
const {isOpen, isStreaming, messages} = useSelector((state: {chat: ChatState}) => state.chat);
16+
const {toggleChat} = useChat();
17+
18+
const hasUnreadMessages = messages.length > 0 && !isOpen;
19+
20+
return (
21+
<Button
22+
view="flat-utility"
23+
onClick={toggleChat}
24+
className={`ai-assistant-button ${className || ''} ${hasUnreadMessages ? 'ai-assistant-button--has-messages' : ''}`}
25+
title={isOpen ? 'Close AI Assistant' : 'Open AI Assistant'}
26+
>
27+
<Icon data={Star} />
28+
AI Assistant
29+
{isStreaming && (
30+
<span className="ai-assistant-button__indicator ai-assistant-button__indicator--streaming" />
31+
)}
32+
{hasUnreadMessages && (
33+
<span className="ai-assistant-button__indicator ai-assistant-button__indicator--unread" />
34+
)}
35+
</Button>
36+
);
37+
};
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
.chat-input {
2+
display: flex;
3+
gap: 12px;
4+
align-items: flex-end;
5+
6+
&__field {
7+
flex: 1;
8+
position: relative;
9+
}
10+
11+
&__textarea {
12+
resize: none;
13+
min-height: 40px;
14+
max-height: 200px;
15+
font-size: 14px;
16+
line-height: 1.4;
17+
18+
&:focus {
19+
outline: none;
20+
}
21+
}
22+
23+
&__actions {
24+
display: flex;
25+
flex-direction: column;
26+
gap: 8px;
27+
}
28+
29+
&__send-button,
30+
&__stop-button {
31+
min-width: 80px;
32+
height: 40px;
33+
display: flex;
34+
align-items: center;
35+
justify-content: center;
36+
gap: 6px;
37+
font-size: 14px;
38+
font-weight: 500;
39+
}
40+
41+
&__send-button {
42+
&:disabled {
43+
opacity: 0.5;
44+
cursor: not-allowed;
45+
}
46+
}
47+
48+
&__stop-button {
49+
background: var(--g-color-base-danger);
50+
border-color: var(--g-color-line-danger);
51+
color: var(--g-color-text-danger);
52+
53+
&:hover:not(:disabled) {
54+
background: var(--g-color-base-danger-hover);
55+
}
56+
}
57+
}
58+
59+
// Auto-resize textarea behavior
60+
.chat-input__textarea {
61+
transition: height 0.1s ease;
62+
overflow-y: hidden;
63+
}
64+
65+
// Focus states
66+
.chat-input:focus-within {
67+
.chat-input__send-button:not(:disabled) {
68+
background: var(--g-color-base-brand);
69+
border-color: var(--g-color-line-brand);
70+
}
71+
}
72+
73+
// Mobile responsive
74+
@media (max-width: 768px) {
75+
.chat-input {
76+
flex-direction: column;
77+
gap: 8px;
78+
79+
&__actions {
80+
flex-direction: row;
81+
justify-content: flex-end;
82+
}
83+
84+
&__send-button,
85+
&__stop-button {
86+
min-width: 60px;
87+
height: 36px;
88+
font-size: 13px;
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)