Skip to content

Commit 1d32923

Browse files
committed
fix: better ui
1 parent b1c804b commit 1d32923

File tree

8 files changed

+145
-52
lines changed

8 files changed

+145
-52
lines changed

src/features/chat/components/ChatMessage/ChatMessage.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from 'react';
22

3-
import {Button, Icon} from '@gravity-ui/uikit';
3+
import {ClipboardButton} from '@gravity-ui/uikit';
44

55
import type {ChatMessage as ChatMessageType} from '../../types/chat';
66
import {ToolCallBlock} from '../ToolCallBlock/ToolCallBlock';
@@ -12,17 +12,18 @@ import './ChatMessage.scss';
1212
const UserIcon = () => <span>👤</span>;
1313
const AssistantIcon = () => <span>🤖</span>;
1414
const ToolIcon = () => <span>🔧</span>;
15-
const CopyIcon = () => <span>📋</span>;
1615

1716
interface ChatMessageProps {
1817
message: ChatMessageType;
18+
isStreaming?: boolean;
19+
isLastMessage?: boolean;
1920
}
2021

21-
export const ChatMessage = ({message}: ChatMessageProps) => {
22-
const handleCopyContent = () => {
23-
navigator.clipboard.writeText(message.content);
24-
};
25-
22+
export const ChatMessage = ({
23+
message,
24+
isStreaming = false,
25+
isLastMessage = false,
26+
}: ChatMessageProps) => {
2627
const formatTimestamp = (timestamp: number) => {
2728
return new Date(timestamp).toLocaleTimeString();
2829
};
@@ -256,18 +257,25 @@ export const ChatMessage = ({message}: ChatMessageProps) => {
256257
const getMessageIcon = () => {
257258
switch (message.role) {
258259
case 'user':
259-
return <Icon data={UserIcon} />;
260+
return <UserIcon />;
260261
case 'assistant':
261-
return <Icon data={AssistantIcon} />;
262+
return <AssistantIcon />;
262263
case 'tool':
263-
return <Icon data={ToolIcon} />;
264+
return <ToolIcon />;
264265
default:
265266
return null;
266267
}
267268
};
268269

269270
const getMessageClass = () => {
270-
return `chat-message chat-message--${message.role}`;
271+
let className = `chat-message chat-message--${message.role}`;
272+
273+
// Add complete class for assistant messages that are not currently streaming
274+
if (message.role === 'assistant' && (!isStreaming || !isLastMessage)) {
275+
className += ' chat-message--complete';
276+
}
277+
278+
return className;
271279
};
272280

273281
return (
@@ -287,9 +295,7 @@ export const ChatMessage = ({message}: ChatMessageProps) => {
287295
</span>
288296
</div>
289297
<div className="chat-message__actions">
290-
<Button view="flat" size="xs" onClick={handleCopyContent} title="Copy message">
291-
<Icon data={CopyIcon} />
292-
</Button>
298+
<ClipboardButton text={message.content} size="xs" />
293299
</div>
294300
</div>
295301

src/features/chat/components/ChatPanel/ChatPanel.scss

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,39 @@
107107
font-size: 16px;
108108
}
109109

110+
&__loading-message {
111+
display: flex;
112+
align-items: flex-start;
113+
gap: 12px;
114+
115+
padding: 12px 16px;
116+
}
117+
118+
&__assistant-avatar {
119+
flex-shrink: 0;
120+
121+
width: 32px;
122+
height: 32px;
123+
124+
font-size: 16px;
125+
line-height: 32px;
126+
text-align: center;
127+
128+
border-radius: 50%;
129+
background: var(--g-color-base-generic-light);
130+
}
131+
132+
&__loading-content {
133+
display: flex;
134+
align-items: center;
135+
gap: 8px;
136+
137+
padding: 8px 12px;
138+
139+
border-radius: 12px;
140+
background: var(--g-color-base-generic-light);
141+
}
142+
110143
&__quota {
111144
flex-shrink: 0;
112145

src/features/chat/components/ChatPanel/ChatPanel.tsx

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react';
22

3-
import {Xmark} from '@gravity-ui/icons';
3+
import {TrashBin, Xmark} from '@gravity-ui/icons';
44
import {Drawer, DrawerItem} from '@gravity-ui/navigation';
55
import {ActionTooltip, Button, Icon, Text} from '@gravity-ui/uikit';
66
import {useSelector} from 'react-redux';
77

8+
import {Loader} from '../../../../components/Loader/Loader';
89
import {cn} from '../../../../utils/cn';
910
import {useChat} from '../../hooks/useChat';
1011
import {formatContextForAI, useCurrentContext} from '../../services/contextService';
@@ -18,7 +19,7 @@ import './ChatPanel.scss';
1819
const b = cn('ydb-chat-drawer');
1920

2021
export const ChatPanel = () => {
21-
const {messages, isLoading, isStreaming, error, isOpen} = useSelector(
22+
const {messages, isStreaming, error, isOpen} = useSelector(
2223
(state: {chat: ChatState}) => state.chat,
2324
);
2425
const messagesEndRef = React.useRef<HTMLDivElement>(null);
@@ -47,8 +48,8 @@ export const ChatPanel = () => {
4748

4849
// Update quota after chat completion
4950
React.useEffect(() => {
50-
// When streaming stops and loading is complete, refresh quota
51-
if (!isStreaming && !isLoading && messages.length > 0) {
51+
// When streaming stops, refresh quota
52+
if (!isStreaming && messages.length > 0) {
5253
// Add a small delay to ensure the message is fully processed
5354
const timer = setTimeout(() => {
5455
setQuotaRefreshTrigger((prev) => prev + 1);
@@ -57,7 +58,7 @@ export const ChatPanel = () => {
5758
return () => clearTimeout(timer);
5859
}
5960
return undefined;
60-
}, [isStreaming, isLoading, messages.length]);
61+
}, [isStreaming, messages.length]);
6162

6263
const handleSendMessage = async (content: string) => {
6364
const contextString = formatContextForAI(context);
@@ -111,9 +112,9 @@ export const ChatPanel = () => {
111112
<div className={b('header')}>
112113
<div className={b('header-left')}>
113114
<Text variant="subheader-2">AI Ассистент</Text>
114-
<QuotaDisplay compact refreshTrigger={quotaRefreshTrigger} />
115115
</div>
116116
<div className={b('controls')}>
117+
<QuotaDisplay compact refreshTrigger={quotaRefreshTrigger} />
117118
<ActionTooltip title="Очистить историю (⌘K)">
118119
<Button
119120
view="flat"
@@ -123,7 +124,7 @@ export const ChatPanel = () => {
123124
messages.filter((msg) => msg.role !== 'tool').length === 0
124125
}
125126
>
126-
🗑
127+
<Icon data={TrashBin} size={16} />
127128
</Button>
128129
</ActionTooltip>
129130
<ActionTooltip title="Закрыть">
@@ -200,9 +201,33 @@ export const ChatPanel = () => {
200201

201202
{messages
202203
.filter((message) => message.role !== 'tool')
203-
.map((message) => (
204-
<ChatMessage key={message.id} message={message} />
205-
))}
204+
.map((message, index, filteredMessages) => {
205+
const isLastMessage = index === filteredMessages.length - 1;
206+
return (
207+
<ChatMessage
208+
key={message.id}
209+
message={message}
210+
isStreaming={isStreaming}
211+
isLastMessage={isLastMessage}
212+
/>
213+
);
214+
})}
215+
216+
{isStreaming &&
217+
(() => {
218+
const nonToolMessages = messages.filter((msg) => msg.role !== 'tool');
219+
const lastMessage = nonToolMessages[nonToolMessages.length - 1];
220+
return lastMessage?.role === 'user';
221+
})() && (
222+
<div className={b('loading-message')}>
223+
<div className={b('loading-content')}>
224+
<Loader size="s" delay={0} />
225+
<Text color="secondary" variant="body-2">
226+
AI обрабатывает запрос...
227+
</Text>
228+
</div>
229+
</div>
230+
)}
206231

207232
{error && (
208233
<div className={b('error')}>
@@ -218,7 +243,7 @@ export const ChatPanel = () => {
218243
<div className={b('input')}>
219244
<ChatInput
220245
onSendMessage={handleSendMessage}
221-
disabled={isLoading}
246+
disabled={isStreaming}
222247
isStreaming={isStreaming}
223248
onStopGeneration={stopGeneration}
224249
/>

src/features/chat/components/QuotaDisplay/QuotaDisplay.tsx

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22

33
import {CircleDollar, Clock} from '@gravity-ui/icons';
4-
import {Icon, Progress, Text} from '@gravity-ui/uikit';
4+
import {Icon, Label, Progress, Text} from '@gravity-ui/uikit';
55

66
import {cn} from '../../../../utils/cn';
77
import type {QuotaInfo} from '../../types/chat';
@@ -83,17 +83,38 @@ export const QuotaDisplay = ({className, refreshTrigger, compact = false}: Quota
8383

8484
// Compact mode - single line display
8585
if (compact) {
86-
const highestUsage = Math.max(quota.daily.percentage, quota.monthly.percentage);
87-
const isHighUsage = highestUsage > 80;
86+
const getDailyTheme = () => {
87+
if (quota.daily.percentage > 90) {
88+
return 'danger';
89+
}
90+
if (quota.daily.percentage > 80) {
91+
return 'warning';
92+
}
93+
return 'normal';
94+
};
95+
96+
const getMonthlyTheme = () => {
97+
if (quota.monthly.percentage > 90) {
98+
return 'danger';
99+
}
100+
if (quota.monthly.percentage > 80) {
101+
return 'warning';
102+
}
103+
return 'normal';
104+
};
88105

89106
return (
90107
<div className={b({compact}, className)}>
91108
<div className={b('compact-content')}>
92-
<Icon data={CircleDollar} size={12} />
93-
<Text variant="caption-2" color={isHighUsage ? 'danger' : 'secondary'}>
94-
{formatCost(quota.daily.used)}/{formatLimit(quota.daily.limit)}{' '}
95-
{formatCost(quota.monthly.used)}/{formatLimit(quota.monthly.limit)}
109+
<Text variant="body-2" color="secondary">
110+
Квота:
96111
</Text>
112+
<Label theme={getDailyTheme()} size="xs">
113+
День: {formatCost(quota.daily.used)}/{formatLimit(quota.daily.limit)}
114+
</Label>
115+
<Label theme={getMonthlyTheme()} size="xs">
116+
Месяц: {formatCost(quota.monthly.used)}/{formatLimit(quota.monthly.limit)}
117+
</Label>
97118
</div>
98119
</div>
99120
);
@@ -120,9 +141,18 @@ export const QuotaDisplay = ({className, refreshTrigger, compact = false}: Quota
120141
</div>
121142

122143
<div className={b('period-info')}>
123-
<Text variant="caption-2" color="secondary">
144+
<Label
145+
theme={
146+
quota.daily.percentage > 90
147+
? 'danger'
148+
: quota.daily.percentage > 80
149+
? 'warning'
150+
: 'normal'
151+
}
152+
size="xs"
153+
>
124154
{formatCost(quota.daily.used)} / {formatLimit(quota.daily.limit)}
125-
</Text>
155+
</Label>
126156

127157
{quota.daily.limit !== Infinity && (
128158
<Progress
@@ -145,9 +175,18 @@ export const QuotaDisplay = ({className, refreshTrigger, compact = false}: Quota
145175
</div>
146176

147177
<div className={b('period-info')}>
148-
<Text variant="caption-2" color="secondary">
178+
<Label
179+
theme={
180+
quota.monthly.percentage > 90
181+
? 'danger'
182+
: quota.monthly.percentage > 80
183+
? 'warning'
184+
: 'normal'
185+
}
186+
size="xs"
187+
>
149188
{formatCost(quota.monthly.used)} / {formatLimit(quota.monthly.limit)}
150-
</Text>
189+
</Label>
151190

152191
{quota.monthly.limit !== Infinity && (
153192
<Progress

src/features/chat/components/UsageDisplay/UsageDisplay.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {CircleDollar} from '@gravity-ui/icons';
2-
import {Icon, Text} from '@gravity-ui/uikit';
2+
import {Icon, Label} from '@gravity-ui/uikit';
33

44
import {cn} from '../../../../utils/cn';
55
import type {UsageBreakdown} from '../../types/chat';
@@ -32,9 +32,9 @@ export const UsageDisplay = ({usage, className}: UsageDisplayProps) => {
3232
<div className={b(null, className)}>
3333
<div className={b('content')}>
3434
<Icon data={CircleDollar} size={12} />
35-
<Text variant="caption-2" color="secondary">
35+
<Label theme="normal" size="xs">
3636
{formatTokens(usage.totalTokens)} tokens • {formatCost(usage.estimatedCost)}
37-
</Text>
37+
</Label>
3838
</div>
3939
</div>
4040
);

src/features/chat/hooks/useChat.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,14 @@ import type {ChatDelta, ChatMessage} from '../types/chat';
77

88
export function useChat() {
99
const dispatch = useDispatch();
10-
const {messages, isLoading, isStreaming, error, isOpen} = useSelector(
11-
(state: any) => state.chat,
12-
);
10+
const {messages, isStreaming, error, isOpen} = useSelector((state: any) => state.chat);
1311
const abortControllerRef = React.useRef<AbortController | null>(null);
1412

1513
const generateMessageId = () => `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1614

1715
const sendMessage = React.useCallback(
1816
async (content: string, context?: string) => {
19-
if (!content.trim() || isLoading) {
17+
if (!content.trim() || isStreaming) {
2018
return;
2119
}
2220

@@ -58,7 +56,7 @@ export function useChat() {
5856
}
5957
}
6058
},
61-
[dispatch, messages, isLoading],
59+
[dispatch, messages, isStreaming],
6260
);
6361

6462
const stopGeneration = React.useCallback(() => {
@@ -109,7 +107,6 @@ export function useChat() {
109107

110108
return {
111109
messages,
112-
isLoading,
113110
isStreaming,
114111
error,
115112
isOpen,

0 commit comments

Comments
 (0)