@gravity-ui/aikit 0.0.1 → 0.1.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.
- package/dist/package.json +15 -3
- package/dist/src/components/atoms/ActionButton/ActionButton.js +1 -11
- package/dist/src/components/organisms/MessageList/ErrorAlert.d.ts +2 -1
- package/dist/src/components/organisms/MessageList/ErrorAlert.js +2 -2
- package/dist/src/components/organisms/MessageList/MessageList.d.ts +5 -2
- package/dist/src/components/organisms/MessageList/MessageList.js +14 -5
- package/dist/src/components/organisms/MessageList/__stories__/MessageList.stories.d.ts +2 -0
- package/dist/src/components/organisms/MessageList/__stories__/MessageList.stories.js +120 -0
- package/dist/src/components/organisms/ThinkingMessage/index.js +1 -11
- package/dist/src/components/pages/ChatContainer/__stories__/ChatContainer.stories.js +41 -5
- package/dist/src/components/templates/ChatContent/__stories__/ChatContent.stories.js +12 -0
- package/dist/src/components/templates/History/__stories__/History.stories.js +1 -11
- package/dist/src/demo/ContentWrapper/ContentWrapper.d.ts +2 -5
- package/dist/src/demo/ContentWrapper/ContentWrapper.js +4 -2
- package/dist/src/hooks/index.d.ts +1 -0
- package/dist/src/hooks/index.js +1 -0
- package/dist/src/hooks/useSmartScroll.d.ts +7 -0
- package/dist/src/hooks/useSmartScroll.js +70 -0
- package/dist/src/utils/messageUtils.d.ts +11 -0
- package/dist/src/utils/messageUtils.js +9 -0
- package/package.json +15 -3
package/dist/package.json
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravity-ui/aikit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Gravity UI base kit for building ai assistant chats",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"files": [
|
|
8
9
|
"dist"
|
|
9
10
|
],
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">= 20",
|
|
13
|
+
"npm": ">= 9",
|
|
14
|
+
"yarn": "Please use npm instead of yarn to install dependencies",
|
|
15
|
+
"pnpm": "Please use npm instead of pnpm to install dependencies"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"*.css",
|
|
19
|
+
"*.scss"
|
|
20
|
+
],
|
|
10
21
|
"repository": {
|
|
11
22
|
"type": "git",
|
|
12
23
|
"url": "git+https://github.com/gravity-ui/aikit.git"
|
|
@@ -26,10 +37,11 @@
|
|
|
26
37
|
"lint:styles:fix": "stylelint **/*.scss --fix",
|
|
27
38
|
"lint:prettier": "prettier --check '**/*.{js,jsx,ts,tsx,css,scss,json,yaml,yml,md,mdx}'",
|
|
28
39
|
"lint:prettier:fix": "prettier --write '**/*.{js,jsx,ts,tsx,css,scss,json,yaml,yml,md,mdx}'",
|
|
29
|
-
"test": "
|
|
40
|
+
"test": "npm run test:unit",
|
|
30
41
|
"build": "tsc",
|
|
31
42
|
"start": "TS_NODE_PROJECT=.storybook/tsconfig.json storybook dev",
|
|
32
43
|
"prepublishOnly": "npm run build",
|
|
44
|
+
"test:unit": "jest --passWithNoTests",
|
|
33
45
|
"playwright:install": "playwright install chromium webkit --with-deps",
|
|
34
46
|
"playwright": "playwright test --config=playwright-ct.config.ts",
|
|
35
47
|
"playwright:update": "npm run playwright -- -u all",
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var t = {};
|
|
3
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
-
t[p] = s[p];
|
|
5
|
-
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
-
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
-
t[p[i]] = s[p[i]];
|
|
9
|
-
}
|
|
10
|
-
return t;
|
|
11
|
-
};
|
|
1
|
+
import { __rest } from "tslib";
|
|
12
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
3
|
import React from 'react';
|
|
14
4
|
import { ActionTooltip, Button } from '@gravity-ui/uikit';
|
|
@@ -2,5 +2,6 @@ import { AlertProps } from '../../atoms/Alert';
|
|
|
2
2
|
export type ErrorAlertProps = {
|
|
3
3
|
onRetry?: () => void;
|
|
4
4
|
errorMessage?: AlertProps;
|
|
5
|
+
className?: string;
|
|
5
6
|
};
|
|
6
|
-
export declare function ErrorAlert({ onRetry, errorMessage }: ErrorAlertProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function ErrorAlert({ onRetry, errorMessage, className }: ErrorAlertProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -5,11 +5,11 @@ import { block } from '../../../utils/cn';
|
|
|
5
5
|
import { Alert } from '../../atoms/Alert';
|
|
6
6
|
import { i18n } from './i18n';
|
|
7
7
|
const b = block('message-list');
|
|
8
|
-
export function ErrorAlert({ onRetry, errorMessage }) {
|
|
8
|
+
export function ErrorAlert({ onRetry, errorMessage, className }) {
|
|
9
9
|
return (_jsx(Alert, Object.assign({ text: i18n('default-error-text'), variant: "warning", button: onRetry
|
|
10
10
|
? {
|
|
11
11
|
content: (_jsxs("div", { className: b('retry-button'), children: [_jsx(Icon, { data: ArrowRotateLeft, size: 14 }), i18n('retry-button')] })),
|
|
12
12
|
onClick: onRetry,
|
|
13
13
|
}
|
|
14
|
-
: undefined }, errorMessage)));
|
|
14
|
+
: undefined, className: className }, errorMessage)));
|
|
15
15
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { OptionsType } from '@diplodoc/transform/lib/typings';
|
|
2
2
|
import { ChatStatus } from '../../../types';
|
|
3
|
-
import type { TChatMessage, TMessageContent, TMessageMetadata } from '../../../types/messages';
|
|
3
|
+
import type { TAssistantMessage, TChatMessage, TMessageContent, TMessageMetadata, TUserMessage } from '../../../types/messages';
|
|
4
|
+
import { type DefaultMessageAction } from '../../../utils';
|
|
4
5
|
import { type MessageRendererRegistry } from '../../../utils/messageTypeRegistry';
|
|
5
6
|
import { AlertProps } from '../../atoms/Alert';
|
|
6
7
|
import './MessageList.scss';
|
|
@@ -14,7 +15,9 @@ export type MessageListProps<TContent extends TMessageContent = never> = {
|
|
|
14
15
|
showActionsOnHover?: boolean;
|
|
15
16
|
showTimestamp?: boolean;
|
|
16
17
|
showAvatar?: boolean;
|
|
18
|
+
userActions?: DefaultMessageAction<TUserMessage<TMessageMetadata>>[];
|
|
19
|
+
assistantActions?: DefaultMessageAction<TAssistantMessage<TContent, TMessageMetadata>>[];
|
|
17
20
|
className?: string;
|
|
18
21
|
qa?: string;
|
|
19
22
|
};
|
|
20
|
-
export declare function MessageList<TContent extends TMessageContent = never>({ messages, messageRendererRegistry, transformOptions, showActionsOnHover, showTimestamp, showAvatar, className, qa, status, errorMessage, onRetry, }: MessageListProps<TContent>): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
export declare function MessageList<TContent extends TMessageContent = never>({ messages, messageRendererRegistry, transformOptions, showActionsOnHover, showTimestamp, showAvatar, userActions, assistantActions, className, qa, status, errorMessage, onRetry, }: MessageListProps<TContent>): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useSmartScroll } from '../../../hooks';
|
|
3
|
+
import { isAssistantMessage, isUserMessage, resolveMessageActions, } from '../../../utils';
|
|
3
4
|
import { block } from '../../../utils/cn';
|
|
4
5
|
import { Loader } from '../../atoms/Loader';
|
|
5
6
|
import { AssistantMessage } from '../AssistantMessage';
|
|
@@ -7,15 +8,23 @@ import { UserMessage } from '../UserMessage';
|
|
|
7
8
|
import { ErrorAlert } from './ErrorAlert';
|
|
8
9
|
import './MessageList.scss';
|
|
9
10
|
const b = block('message-list');
|
|
10
|
-
export function MessageList({ messages, messageRendererRegistry, transformOptions, showActionsOnHover, showTimestamp, showAvatar, className, qa, status, errorMessage, onRetry, }) {
|
|
11
|
+
export function MessageList({ messages, messageRendererRegistry, transformOptions, showActionsOnHover, showTimestamp, showAvatar, userActions, assistantActions, className, qa, status, errorMessage, onRetry, }) {
|
|
12
|
+
const isStreaming = status === 'streaming';
|
|
13
|
+
const messagesCount = messages.length;
|
|
14
|
+
const { containerRef, endRef } = useSmartScroll(isStreaming, messagesCount);
|
|
11
15
|
const renderMessage = (message, index) => {
|
|
12
16
|
if (isUserMessage(message)) {
|
|
13
|
-
|
|
17
|
+
const actions = resolveMessageActions(message, userActions);
|
|
18
|
+
return (_jsx(UserMessage, { content: message.content, actions: actions, timestamp: message.timestamp, format: message.format, avatarUrl: message.avatarUrl, transformOptions: transformOptions, showActionsOnHover: showActionsOnHover, showTimestamp: showTimestamp, showAvatar: showAvatar }, message.id || `message-${index}`));
|
|
14
19
|
}
|
|
15
20
|
if (isAssistantMessage(message)) {
|
|
16
|
-
|
|
21
|
+
const showActions = message.status === 'complete';
|
|
22
|
+
const actions = showActions
|
|
23
|
+
? resolveMessageActions(message, assistantActions)
|
|
24
|
+
: undefined;
|
|
25
|
+
return (_jsx(AssistantMessage, { content: message.content, actions: actions, timestamp: message.timestamp, id: message.id, messageRendererRegistry: messageRendererRegistry, transformOptions: transformOptions, showActionsOnHover: showActionsOnHover, showTimestamp: showTimestamp }, message.id || `message-${index}`));
|
|
17
26
|
}
|
|
18
27
|
return null;
|
|
19
28
|
};
|
|
20
|
-
return (_jsxs("div", { className: b(null, className), "data-qa": qa, children: [_jsx("div", { className: b('messages', className), "data-qa": qa, children: messages.map(renderMessage) }), status === 'submitted' && _jsx(Loader, {}), status === 'error' && _jsx(ErrorAlert, { onRetry: onRetry, errorMessage: errorMessage })] }));
|
|
29
|
+
return (_jsxs("div", { ref: containerRef, className: b(null, className), "data-qa": qa, children: [_jsx("div", { className: b('messages', className), "data-qa": qa, children: messages.map(renderMessage) }), status === 'submitted' && _jsx(Loader, { className: b('loader') }), status === 'error' && (_jsx(ErrorAlert, { className: b('error-alert'), onRetry: onRetry, errorMessage: errorMessage })), _jsx("div", { ref: endRef })] }));
|
|
21
30
|
}
|
|
@@ -20,3 +20,5 @@ interface ChartMessageData {
|
|
|
20
20
|
}
|
|
21
21
|
type ChartMessageContent = TMessageContent<'chart', ChartMessageData>;
|
|
22
22
|
export declare const WithCustomMessageType: StoryObj<MessageListProps<ChartMessageContent>>;
|
|
23
|
+
export declare const WithStreamingMessage: StoryObj<MessageListProps>;
|
|
24
|
+
export declare const WithDefaultActions: StoryObj<MessageListProps>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
2
3
|
import { Pencil } from '@gravity-ui/icons';
|
|
3
4
|
import { Icon, Text } from '@gravity-ui/uikit';
|
|
4
5
|
import { MessageList } from '..';
|
|
@@ -6,6 +7,7 @@ import { ContentWrapper } from '../../../../demo/ContentWrapper';
|
|
|
6
7
|
import { Showcase } from '../../../../demo/Showcase';
|
|
7
8
|
import { ShowcaseItem } from '../../../../demo/ShowcaseItem';
|
|
8
9
|
import { createMessageRendererRegistry, registerMessageRenderer, } from '../../../../utils/messageTypeRegistry';
|
|
10
|
+
import { BaseMessageAction } from '../../../molecules/BaseMessage';
|
|
9
11
|
import MDXDocs from './Docs.mdx';
|
|
10
12
|
export default {
|
|
11
13
|
title: 'organisms/MessageList',
|
|
@@ -166,3 +168,121 @@ export const WithCustomMessageType = {
|
|
|
166
168
|
},
|
|
167
169
|
decorators: defaultDecorators,
|
|
168
170
|
};
|
|
171
|
+
const streamingText = 'React Hooks are functions that let you use state and other React features without writing a class. ' +
|
|
172
|
+
'The most commonly used hooks are:\n\n' +
|
|
173
|
+
'1. **useState** - for managing component state\n' +
|
|
174
|
+
'2. **useEffect** - for side effects like data fetching or subscriptions\n' +
|
|
175
|
+
'3. **useContext** - for consuming context values\n' +
|
|
176
|
+
'4. **useCallback** - for memoizing functions\n' +
|
|
177
|
+
'5. **useMemo** - for memoizing computed values\n\n' +
|
|
178
|
+
'Each hook serves a specific purpose and helps you write cleaner, more maintainable React code. ' +
|
|
179
|
+
'Would you like examples of how to use any of these hooks?';
|
|
180
|
+
export const WithStreamingMessage = {
|
|
181
|
+
render: (args) => {
|
|
182
|
+
const [streamedText, setStreamedText] = useState('');
|
|
183
|
+
const [isStreaming, setIsStreaming] = useState(true);
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
setStreamedText('');
|
|
186
|
+
setIsStreaming(true);
|
|
187
|
+
const resultText = streamingText.repeat(10);
|
|
188
|
+
let currentIndex = 0;
|
|
189
|
+
const interval = setInterval(() => {
|
|
190
|
+
if (currentIndex < resultText.length) {
|
|
191
|
+
const nextChunk = resultText.slice(0, currentIndex + 1);
|
|
192
|
+
setStreamedText(nextChunk);
|
|
193
|
+
currentIndex += Math.floor(Math.random() * 4) + 3;
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
setIsStreaming(false);
|
|
197
|
+
clearInterval(interval);
|
|
198
|
+
}
|
|
199
|
+
}, 20);
|
|
200
|
+
return () => {
|
|
201
|
+
clearInterval(interval);
|
|
202
|
+
};
|
|
203
|
+
}, []);
|
|
204
|
+
const messages = [
|
|
205
|
+
{
|
|
206
|
+
id: 'user-1',
|
|
207
|
+
role: 'user',
|
|
208
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
209
|
+
content: 'Can you explain React Hooks to me?',
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: 'assistant-1',
|
|
213
|
+
role: 'assistant',
|
|
214
|
+
timestamp: '2024-01-01T00:00:01Z',
|
|
215
|
+
content: streamedText || ' ',
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
return (_jsx(ShowcaseItem, { title: "With Streaming Message", children: _jsx(ContentWrapper, { width: "480px", height: "200px", display: "flex", children: _jsx(MessageList, Object.assign({}, args, { messages: messages, status: isStreaming ? 'streaming' : 'ready' })) }) }));
|
|
219
|
+
},
|
|
220
|
+
decorators: defaultDecorators,
|
|
221
|
+
};
|
|
222
|
+
export const WithDefaultActions = {
|
|
223
|
+
render: (args) => {
|
|
224
|
+
const userActions = [
|
|
225
|
+
{
|
|
226
|
+
type: BaseMessageAction.Edit,
|
|
227
|
+
onClick: (message) => {
|
|
228
|
+
// eslint-disable-next-line no-console
|
|
229
|
+
console.log('Edit user message:', message.id);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
type: BaseMessageAction.Delete,
|
|
234
|
+
onClick: (message) => {
|
|
235
|
+
// eslint-disable-next-line no-console
|
|
236
|
+
console.log('Delete user message:', message.id);
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
];
|
|
240
|
+
const assistantActions = [
|
|
241
|
+
{
|
|
242
|
+
type: BaseMessageAction.Copy,
|
|
243
|
+
onClick: (message) => {
|
|
244
|
+
// eslint-disable-next-line no-console
|
|
245
|
+
console.log('Copy assistant message:', message.id);
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
type: BaseMessageAction.Like,
|
|
250
|
+
onClick: (message) => {
|
|
251
|
+
// eslint-disable-next-line no-console
|
|
252
|
+
console.log('Like assistant message:', message.id);
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
return (_jsx(ShowcaseItem, { title: "With Default Actions", children: _jsx(ContentWrapper, { width: "480px", children: _jsx(MessageList, Object.assign({}, args, { messages: [
|
|
257
|
+
{
|
|
258
|
+
id: 'user-1',
|
|
259
|
+
role: 'user',
|
|
260
|
+
timestamp: '2024-01-01T00:00:00Z',
|
|
261
|
+
content: 'Hello! This message has default actions.',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: 'assistant-1',
|
|
265
|
+
role: 'assistant',
|
|
266
|
+
timestamp: '2024-01-01T00:00:01Z',
|
|
267
|
+
content: 'Hi! This message also has default actions.',
|
|
268
|
+
status: 'complete',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: 'user-2',
|
|
272
|
+
role: 'user',
|
|
273
|
+
timestamp: '2024-01-01T00:00:02Z',
|
|
274
|
+
content: 'This message has custom actions.',
|
|
275
|
+
actions: [
|
|
276
|
+
{
|
|
277
|
+
type: 'custom',
|
|
278
|
+
onClick: () => {
|
|
279
|
+
// eslint-disable-next-line no-console
|
|
280
|
+
console.log('Custom action clicked');
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
], userActions: userActions, assistantActions: assistantActions })) }) }));
|
|
286
|
+
},
|
|
287
|
+
decorators: defaultDecorators,
|
|
288
|
+
};
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
|
|
3
|
-
var t = {};
|
|
4
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
-
t[p] = s[p];
|
|
6
|
-
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
-
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
-
t[p[i]] = s[p[i]];
|
|
10
|
-
}
|
|
11
|
-
return t;
|
|
12
|
-
};
|
|
2
|
+
import { __rest } from "tslib";
|
|
13
3
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
14
4
|
import { ChevronDown, ChevronUp, Copy } from '@gravity-ui/icons';
|
|
15
5
|
import { Button, Icon, Text } from '@gravity-ui/uikit';
|
|
@@ -79,24 +79,28 @@ const mockChatMessages = {
|
|
|
79
79
|
role: 'user',
|
|
80
80
|
content: 'What is quantum computing?',
|
|
81
81
|
timestamp: new Date(Date.now() - 1000 * 60 * 60).toISOString(),
|
|
82
|
+
status: 'complete',
|
|
82
83
|
},
|
|
83
84
|
{
|
|
84
85
|
id: '1-2',
|
|
85
86
|
role: 'assistant',
|
|
86
87
|
content: 'Quantum computing is a revolutionary approach to computation that leverages the principles of quantum mechanics. Unlike classical computers that use bits (0 or 1), quantum computers use quantum bits or "qubits" that can exist in multiple states simultaneously through a phenomenon called superposition.\n\nKey concepts include:\n1. **Superposition**: Qubits can be in multiple states at once\n2. **Entanglement**: Qubits can be correlated in ways that classical bits cannot\n3. **Quantum Interference**: Used to amplify correct answers and cancel out wrong ones',
|
|
87
88
|
timestamp: new Date(Date.now() - 1000 * 60 * 59).toISOString(),
|
|
89
|
+
status: 'complete',
|
|
88
90
|
},
|
|
89
91
|
{
|
|
90
92
|
id: '1-3',
|
|
91
93
|
role: 'user',
|
|
92
94
|
content: 'Can you explain quantum entanglement in simple terms?',
|
|
93
95
|
timestamp: new Date(Date.now() - 1000 * 60 * 58).toISOString(),
|
|
96
|
+
status: 'complete',
|
|
94
97
|
},
|
|
95
98
|
{
|
|
96
99
|
id: '1-4',
|
|
97
100
|
role: 'assistant',
|
|
98
101
|
content: 'Quantum entanglement is like having two magic coins that are mysteriously connected. When you flip one coin and it lands on heads, the other coin - no matter how far away - will instantly land on tails (or vice versa).\n\nIn quantum physics terms:\n- Two particles become "entangled" and share a quantum state\n- Measuring one particle instantly affects the other\n- This happens regardless of the distance between them\n- Einstein called it "spooky action at a distance"\n\nThis phenomenon is fundamental to quantum computing and quantum communication technologies.',
|
|
99
102
|
timestamp: new Date(Date.now() - 1000 * 60 * 57).toISOString(),
|
|
103
|
+
status: 'complete',
|
|
100
104
|
},
|
|
101
105
|
],
|
|
102
106
|
'2': [
|
|
@@ -105,24 +109,28 @@ const mockChatMessages = {
|
|
|
105
109
|
role: 'user',
|
|
106
110
|
content: 'Write a poem about autumn leaves',
|
|
107
111
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
|
|
112
|
+
status: 'complete',
|
|
108
113
|
},
|
|
109
114
|
{
|
|
110
115
|
id: '2-2',
|
|
111
116
|
role: 'assistant',
|
|
112
117
|
content: "Golden whispers fall from trees,\nDancing softly on the breeze,\nCrimsons, ambers, russet bright,\nPainting pathways with their light.\n\nNature's canvas, rich and warm,\nBefore the coming winter storm,\nEach leaf a story, each one told,\nOf seasons passing, growing old.",
|
|
113
118
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 + 60000).toISOString(),
|
|
119
|
+
status: 'complete',
|
|
114
120
|
},
|
|
115
121
|
{
|
|
116
122
|
id: '2-3',
|
|
117
123
|
role: 'user',
|
|
118
124
|
content: 'That was beautiful! Can you write one about winter snow?',
|
|
119
125
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 + 120000).toISOString(),
|
|
126
|
+
status: 'complete',
|
|
120
127
|
},
|
|
121
128
|
{
|
|
122
129
|
id: '2-4',
|
|
123
130
|
role: 'assistant',
|
|
124
131
|
content: "Snowflakes drift from silent skies,\nBlankets white before our eyes,\nCrystal diamonds, soft and pure,\nWinter's gentle signature.\n\nFrosted windows, frozen streams,\nLandscapes wrapped in silver dreams,\nQuiet peace in cold embrace,\nNature sleeps in gentle grace.",
|
|
125
132
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 + 180000).toISOString(),
|
|
133
|
+
status: 'complete',
|
|
126
134
|
},
|
|
127
135
|
],
|
|
128
136
|
'3': [
|
|
@@ -131,24 +139,28 @@ const mockChatMessages = {
|
|
|
131
139
|
role: 'user',
|
|
132
140
|
content: "I have a bug in my async function. It's not waiting for the promise to resolve.",
|
|
133
141
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2).toISOString(),
|
|
142
|
+
status: 'complete',
|
|
134
143
|
},
|
|
135
144
|
{
|
|
136
145
|
id: '3-2',
|
|
137
146
|
role: 'assistant',
|
|
138
147
|
content: "Let's debug this step by step. The most common issues with async/await are:\n\n1. **Missing await keyword**: Make sure you're using `await` before the promise\n2. **Function not marked as async**: The function must be declared with `async` keyword\n3. **Promise not being returned**: Ensure the function returns a promise\n\nCan you share your code? Here's a common pattern:\n\n```javascript\nasync function fetchData() {\n const result = await fetch('/api/data');\n const data = await result.json();\n return data;\n}\n```",
|
|
139
148
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2 + 60000).toISOString(),
|
|
149
|
+
status: 'complete',
|
|
140
150
|
},
|
|
141
151
|
{
|
|
142
152
|
id: '3-3',
|
|
143
153
|
role: 'user',
|
|
144
154
|
content: 'Oh! I forgot to mark the parent function as async. That was the issue. Thanks!',
|
|
145
155
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2 + 120000).toISOString(),
|
|
156
|
+
status: 'complete',
|
|
146
157
|
},
|
|
147
158
|
{
|
|
148
159
|
id: '3-4',
|
|
149
160
|
role: 'assistant',
|
|
150
161
|
content: "Great! That's a very common mistake. Remember: to use `await`, the containing function must be `async`. Also, if you're using `await` in a callback (like in `.map()` or `.forEach()`), make sure to make the callback function async too. Happy coding!",
|
|
151
162
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2 + 180000).toISOString(),
|
|
163
|
+
status: 'complete',
|
|
152
164
|
},
|
|
153
165
|
],
|
|
154
166
|
'4': [
|
|
@@ -157,12 +169,14 @@ const mockChatMessages = {
|
|
|
157
169
|
role: 'user',
|
|
158
170
|
content: 'What are the latest developments in AI?',
|
|
159
171
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3).toISOString(),
|
|
172
|
+
status: 'complete',
|
|
160
173
|
},
|
|
161
174
|
{
|
|
162
175
|
id: '4-2',
|
|
163
176
|
role: 'assistant',
|
|
164
177
|
content: 'Recent AI developments include:\n\n**Large Language Models**\n- More efficient training methods\n- Better reasoning capabilities\n- Multimodal models (text, image, audio)\n\n**Computer Vision**\n- Improved object detection and segmentation\n- Real-time video analysis\n- 3D reconstruction from 2D images\n\n**AI Safety**\n- Constitutional AI and alignment research\n- Better interpretability tools\n- Red-teaming and safety testing\n\n**Edge AI**\n- Running models on mobile devices\n- Reduced latency for real-time applications\n- Privacy-preserving local inference',
|
|
165
178
|
timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24 * 3 + 60000).toISOString(),
|
|
179
|
+
status: 'complete',
|
|
166
180
|
},
|
|
167
181
|
],
|
|
168
182
|
};
|
|
@@ -235,6 +249,7 @@ export const Playground = {
|
|
|
235
249
|
role: 'user',
|
|
236
250
|
content: data.content,
|
|
237
251
|
timestamp: new Date().toISOString(),
|
|
252
|
+
status: 'complete',
|
|
238
253
|
actions: createMessageActions(Date.now().toString(), 'user'),
|
|
239
254
|
};
|
|
240
255
|
setMessages((prev) => [...prev, userMessage]);
|
|
@@ -247,6 +262,7 @@ export const Playground = {
|
|
|
247
262
|
role: 'assistant',
|
|
248
263
|
content: `This is a mock response to: "${data.content}". In a real application, this would be a streamed response from an AI model.`,
|
|
249
264
|
timestamp: new Date().toISOString(),
|
|
265
|
+
status: 'complete',
|
|
250
266
|
actions: createMessageActions(assistantMessageId, 'assistant'),
|
|
251
267
|
};
|
|
252
268
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
@@ -287,6 +303,7 @@ export const EmptyState = {
|
|
|
287
303
|
id: Date.now().toString(),
|
|
288
304
|
role: 'user',
|
|
289
305
|
content: data.content,
|
|
306
|
+
status: 'complete',
|
|
290
307
|
};
|
|
291
308
|
setMessages((prev) => [...prev, userMessage]);
|
|
292
309
|
};
|
|
@@ -312,6 +329,7 @@ export const WithMessages = {
|
|
|
312
329
|
id: userMessageId,
|
|
313
330
|
role: 'user',
|
|
314
331
|
content: data.content,
|
|
332
|
+
status: 'complete',
|
|
315
333
|
actions: createMessageActions(userMessageId, 'user'),
|
|
316
334
|
};
|
|
317
335
|
setMessages((prev) => [...prev, userMessage]);
|
|
@@ -337,6 +355,7 @@ export const WithStreaming = {
|
|
|
337
355
|
id: userMessageId,
|
|
338
356
|
role: 'user',
|
|
339
357
|
content: data.content,
|
|
358
|
+
status: 'complete',
|
|
340
359
|
actions: createMessageActions(userMessageId, 'user'),
|
|
341
360
|
};
|
|
342
361
|
setMessages((prev) => [...prev, userMessage]);
|
|
@@ -351,6 +370,7 @@ export const WithStreaming = {
|
|
|
351
370
|
id: assistantMessageId,
|
|
352
371
|
role: 'assistant',
|
|
353
372
|
content: '',
|
|
373
|
+
status: 'streaming',
|
|
354
374
|
actions: createMessageActions(assistantMessageId, 'assistant'),
|
|
355
375
|
},
|
|
356
376
|
]);
|
|
@@ -359,7 +379,9 @@ export const WithStreaming = {
|
|
|
359
379
|
for (let i = 0; i < words.length; i++) {
|
|
360
380
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
361
381
|
const currentText = words.slice(0, i + 1).join(' ');
|
|
362
|
-
|
|
382
|
+
const isLast = i === words.length - 1;
|
|
383
|
+
setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId
|
|
384
|
+
? Object.assign(Object.assign({}, msg), { content: currentText, status: isLast ? 'complete' : 'streaming' }) : msg));
|
|
363
385
|
}
|
|
364
386
|
setStatus('ready');
|
|
365
387
|
};
|
|
@@ -391,6 +413,7 @@ export const WithHistory = {
|
|
|
391
413
|
id: userMessageId,
|
|
392
414
|
role: 'user',
|
|
393
415
|
content: data.content,
|
|
416
|
+
status: 'complete',
|
|
394
417
|
actions: createMessageActions(userMessageId, 'user'),
|
|
395
418
|
};
|
|
396
419
|
setMessages((prev) => [...prev, userMessage]);
|
|
@@ -455,6 +478,7 @@ export const WithI18nConfig = {
|
|
|
455
478
|
id: Date.now().toString(),
|
|
456
479
|
role: 'user',
|
|
457
480
|
content: data.content,
|
|
481
|
+
status: 'complete',
|
|
458
482
|
};
|
|
459
483
|
setMessages((prev) => [...prev, userMessage]);
|
|
460
484
|
};
|
|
@@ -486,6 +510,7 @@ export const WithComponentPropsOverride = {
|
|
|
486
510
|
id: Date.now().toString(),
|
|
487
511
|
role: 'user',
|
|
488
512
|
content: data.content,
|
|
513
|
+
status: 'complete',
|
|
489
514
|
};
|
|
490
515
|
setMessages((prev) => [...prev, userMessage]);
|
|
491
516
|
};
|
|
@@ -515,6 +540,7 @@ export const WithContextItems = {
|
|
|
515
540
|
id: Date.now().toString(),
|
|
516
541
|
role: 'user',
|
|
517
542
|
content: data.content,
|
|
543
|
+
status: 'complete',
|
|
518
544
|
};
|
|
519
545
|
setMessages((prev) => [...prev, userMessage]);
|
|
520
546
|
};
|
|
@@ -553,6 +579,7 @@ export const WithContextItemsAndIndicator = {
|
|
|
553
579
|
id: Date.now().toString(),
|
|
554
580
|
role: 'user',
|
|
555
581
|
content: data.content,
|
|
582
|
+
status: 'complete',
|
|
556
583
|
};
|
|
557
584
|
setMessages((prev) => [...prev, userMessage]);
|
|
558
585
|
};
|
|
@@ -595,6 +622,7 @@ export const ErrorState = {
|
|
|
595
622
|
id: userMessageId,
|
|
596
623
|
role: 'user',
|
|
597
624
|
content: data.content,
|
|
625
|
+
status: 'complete',
|
|
598
626
|
actions: createMessageActions(userMessageId, 'user'),
|
|
599
627
|
};
|
|
600
628
|
setMessages((prev) => [...prev, userMessage]);
|
|
@@ -636,11 +664,12 @@ export const FullStreamingExample = {
|
|
|
636
664
|
role: 'user',
|
|
637
665
|
content: data.content,
|
|
638
666
|
timestamp: new Date().toISOString(),
|
|
667
|
+
status: 'complete',
|
|
639
668
|
actions: createMessageActions(userMessageId, 'user'),
|
|
640
669
|
};
|
|
641
670
|
setMessages((prev) => [...prev, userMessage]);
|
|
642
|
-
//
|
|
643
|
-
setStatus('
|
|
671
|
+
// Show submitted state first
|
|
672
|
+
setStatus('submitted');
|
|
644
673
|
const abortController = new AbortController();
|
|
645
674
|
setController(abortController);
|
|
646
675
|
try {
|
|
@@ -654,14 +683,19 @@ export const FullStreamingExample = {
|
|
|
654
683
|
// Simulate streaming for demo
|
|
655
684
|
const assistantMessageId = (Date.now() + 1).toString();
|
|
656
685
|
const fullResponse = `This is a detailed response to your question: "${data.content}"\n\nIn a production environment, this text would be streamed from an AI model in real-time. The streaming provides several benefits:\n\n1. **Better User Experience**: Users see the response as it's being generated\n2. **Lower Perceived Latency**: The wait feels shorter when content appears incrementally\n3. **Ability to Cancel**: Users can stop generation if they have enough information\n4. **Resource Efficiency**: Responses can be processed as they arrive\n\nThe implementation would use Server-Sent Events (SSE) or streaming fetch API to receive chunks of text from the backend, updating the message content in real-time.`;
|
|
686
|
+
// Wait a bit before starting streaming
|
|
687
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
688
|
+
// Start streaming
|
|
689
|
+
setStatus('streaming');
|
|
657
690
|
// Create empty assistant message
|
|
658
691
|
setMessages((prev) => [
|
|
659
692
|
...prev,
|
|
660
693
|
{
|
|
661
694
|
id: assistantMessageId,
|
|
662
695
|
role: 'assistant',
|
|
663
|
-
content: '',
|
|
696
|
+
content: ' ',
|
|
664
697
|
timestamp: new Date().toISOString(),
|
|
698
|
+
status: 'streaming',
|
|
665
699
|
actions: createMessageActions(assistantMessageId, 'assistant'),
|
|
666
700
|
},
|
|
667
701
|
]);
|
|
@@ -673,7 +707,9 @@ export const FullStreamingExample = {
|
|
|
673
707
|
}
|
|
674
708
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
675
709
|
const currentText = words.slice(0, i + 1).join(' ');
|
|
676
|
-
|
|
710
|
+
const isLast = i === words.length - 1;
|
|
711
|
+
setMessages((prev) => prev.map((msg) => msg.id === assistantMessageId
|
|
712
|
+
? Object.assign(Object.assign({}, msg), { content: currentText, status: isLast ? 'complete' : 'streaming' }) : msg));
|
|
677
713
|
}
|
|
678
714
|
}
|
|
679
715
|
catch (error) {
|
|
@@ -52,6 +52,7 @@ const sampleMessages = [
|
|
|
52
52
|
role: 'user',
|
|
53
53
|
content: 'Hello! Can you help me with React?',
|
|
54
54
|
timestamp: '2024-01-01T12:00:00Z',
|
|
55
|
+
status: 'complete',
|
|
55
56
|
actions: sampleActions,
|
|
56
57
|
},
|
|
57
58
|
{
|
|
@@ -59,6 +60,7 @@ const sampleMessages = [
|
|
|
59
60
|
role: 'assistant',
|
|
60
61
|
content: 'Of course! I would be happy to help you with React. What would you like to know?',
|
|
61
62
|
timestamp: '2024-01-01T12:00:05Z',
|
|
63
|
+
status: 'complete',
|
|
62
64
|
actions: sampleActions,
|
|
63
65
|
},
|
|
64
66
|
{
|
|
@@ -66,6 +68,7 @@ const sampleMessages = [
|
|
|
66
68
|
role: 'user',
|
|
67
69
|
content: 'How do I use hooks?',
|
|
68
70
|
timestamp: '2024-01-01T12:01:00Z',
|
|
71
|
+
status: 'complete',
|
|
69
72
|
actions: sampleActions,
|
|
70
73
|
},
|
|
71
74
|
{
|
|
@@ -73,6 +76,7 @@ const sampleMessages = [
|
|
|
73
76
|
role: 'assistant',
|
|
74
77
|
content: 'React Hooks are functions that let you use state and other React features without writing a class. The most commonly used hooks are:\n\n1. **useState** - for managing component state\n2. **useEffect** - for side effects\n3. **useContext** - for consuming context\n\nWould you like examples of how to use any of these?',
|
|
75
78
|
timestamp: '2024-01-01T12:01:10Z',
|
|
79
|
+
status: 'complete',
|
|
76
80
|
actions: sampleActions,
|
|
77
81
|
},
|
|
78
82
|
];
|
|
@@ -169,6 +173,7 @@ export const WithManyMessages = {
|
|
|
169
173
|
role: 'user',
|
|
170
174
|
content: 'Can you show me an example?',
|
|
171
175
|
timestamp: '2024-01-01T12:02:00Z',
|
|
176
|
+
status: 'complete',
|
|
172
177
|
actions: sampleActions,
|
|
173
178
|
},
|
|
174
179
|
{
|
|
@@ -193,6 +198,7 @@ function Counter() {
|
|
|
193
198
|
|
|
194
199
|
This component maintains a count state and updates it when the button is clicked.`,
|
|
195
200
|
timestamp: '2024-01-01T12:02:05Z',
|
|
201
|
+
status: 'complete',
|
|
196
202
|
actions: sampleActions,
|
|
197
203
|
},
|
|
198
204
|
],
|
|
@@ -212,6 +218,7 @@ export const Interactive = {
|
|
|
212
218
|
role: 'user',
|
|
213
219
|
content,
|
|
214
220
|
timestamp: new Date().toISOString(),
|
|
221
|
+
status: 'complete',
|
|
215
222
|
};
|
|
216
223
|
setMessages([userMessage]);
|
|
217
224
|
setView('chat');
|
|
@@ -222,6 +229,7 @@ export const Interactive = {
|
|
|
222
229
|
role: 'assistant',
|
|
223
230
|
content: `I received your message: "${content}". How can I help you further?`,
|
|
224
231
|
timestamp: new Date().toISOString(),
|
|
232
|
+
status: 'complete',
|
|
225
233
|
};
|
|
226
234
|
setMessages((prev) => [...prev, assistantMessage]);
|
|
227
235
|
}, 1000);
|
|
@@ -250,6 +258,7 @@ export const LongConversation = {
|
|
|
250
258
|
role: 'user',
|
|
251
259
|
content: 'Can you show me an example?',
|
|
252
260
|
timestamp: '2024-01-01T12:02:00Z',
|
|
261
|
+
status: 'complete',
|
|
253
262
|
actions: sampleActions,
|
|
254
263
|
},
|
|
255
264
|
{
|
|
@@ -274,6 +283,7 @@ function Counter() {
|
|
|
274
283
|
|
|
275
284
|
This component maintains a count state and updates it when the button is clicked.`,
|
|
276
285
|
timestamp: '2024-01-01T12:02:05Z',
|
|
286
|
+
status: 'complete',
|
|
277
287
|
actions: sampleActions,
|
|
278
288
|
},
|
|
279
289
|
{
|
|
@@ -281,6 +291,7 @@ This component maintains a count state and updates it when the button is clicked
|
|
|
281
291
|
role: 'user',
|
|
282
292
|
content: 'What about useEffect?',
|
|
283
293
|
timestamp: '2024-01-01T12:03:00Z',
|
|
294
|
+
status: 'complete',
|
|
284
295
|
actions: sampleActions,
|
|
285
296
|
},
|
|
286
297
|
{
|
|
@@ -304,6 +315,7 @@ function DataFetcher() {
|
|
|
304
315
|
}
|
|
305
316
|
\`\`\``,
|
|
306
317
|
timestamp: '2024-01-01T12:03:10Z',
|
|
318
|
+
status: 'complete',
|
|
307
319
|
actions: sampleActions,
|
|
308
320
|
},
|
|
309
321
|
],
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
var t = {};
|
|
3
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
-
t[p] = s[p];
|
|
5
|
-
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
-
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
-
t[p[i]] = s[p[i]];
|
|
9
|
-
}
|
|
10
|
-
return t;
|
|
11
|
-
};
|
|
1
|
+
import { __rest } from "tslib";
|
|
12
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
13
3
|
import { useRef, useState } from 'react';
|
|
14
4
|
import { ClockArrowRotateLeft } from '@gravity-ui/icons';
|
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import './ContentWrapper.scss';
|
|
2
|
-
type Props = React.PropsWithChildren<
|
|
3
|
-
|
|
4
|
-
height?: string;
|
|
5
|
-
}>;
|
|
6
|
-
export declare function ContentWrapper({ children, width, height }: Props): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
type Props = React.PropsWithChildren<React.CSSProperties>;
|
|
3
|
+
export declare function ContentWrapper(props: Props): import("react/jsx-runtime").JSX.Element;
|
|
7
4
|
export {};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { __rest } from "tslib";
|
|
1
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
3
|
import { cn } from '../../utils/cn';
|
|
3
4
|
import './ContentWrapper.scss';
|
|
4
5
|
const b = cn('content-wrapper');
|
|
5
|
-
export function ContentWrapper(
|
|
6
|
-
|
|
6
|
+
export function ContentWrapper(props) {
|
|
7
|
+
const { children } = props, style = __rest(props, ["children"]);
|
|
8
|
+
return (_jsx("div", { className: b(), style: style, children: children }));
|
|
7
9
|
}
|
package/dist/src/hooks/index.js
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
export interface UseSmartScrollReturn<T extends HTMLElement> {
|
|
3
|
+
containerRef: RefObject<T>;
|
|
4
|
+
endRef: RefObject<T>;
|
|
5
|
+
scrollToBottom: () => void;
|
|
6
|
+
}
|
|
7
|
+
export declare function useSmartScroll<T extends HTMLElement>(isStreaming?: boolean, messagesCount?: number): UseSmartScrollReturn<T>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
const SCROLL_THRESHOLD = 10;
|
|
3
|
+
export function useSmartScroll(isStreaming = false, messagesCount = 0) {
|
|
4
|
+
const containerRef = useRef(null);
|
|
5
|
+
const endRef = useRef(null);
|
|
6
|
+
const [userScrolledUp, setUserScrolledUp] = useState(false);
|
|
7
|
+
const prevMessagesCount = useRef(messagesCount);
|
|
8
|
+
const checkIfUserScrolledUp = useCallback(() => {
|
|
9
|
+
const container = containerRef.current;
|
|
10
|
+
if (!container)
|
|
11
|
+
return false;
|
|
12
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
13
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
14
|
+
return distanceFromBottom > SCROLL_THRESHOLD;
|
|
15
|
+
}, []);
|
|
16
|
+
const scrollToBottom = useCallback((behavior = 'instant') => {
|
|
17
|
+
if (!userScrolledUp) {
|
|
18
|
+
const end = endRef.current;
|
|
19
|
+
if (end) {
|
|
20
|
+
end.scrollIntoView({ behavior, block: 'end' });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}, [userScrolledUp]);
|
|
24
|
+
// Handle user scroll events
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const container = containerRef.current;
|
|
27
|
+
if (!container) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const handleScroll = () => {
|
|
31
|
+
const scrolledUp = checkIfUserScrolledUp();
|
|
32
|
+
setUserScrolledUp(scrolledUp);
|
|
33
|
+
};
|
|
34
|
+
container.addEventListener('scroll', handleScroll, { passive: true });
|
|
35
|
+
return () => {
|
|
36
|
+
container.removeEventListener('scroll', handleScroll);
|
|
37
|
+
};
|
|
38
|
+
}, [checkIfUserScrolledUp]);
|
|
39
|
+
// Handle DOM mutations during streaming
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const container = containerRef.current;
|
|
42
|
+
if (!container || !isStreaming) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const observer = new MutationObserver(() => {
|
|
46
|
+
scrollToBottom('instant');
|
|
47
|
+
});
|
|
48
|
+
observer.observe(container, {
|
|
49
|
+
childList: true,
|
|
50
|
+
subtree: true,
|
|
51
|
+
attributes: true,
|
|
52
|
+
characterData: true,
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
observer.disconnect();
|
|
56
|
+
};
|
|
57
|
+
}, [isStreaming, scrollToBottom]);
|
|
58
|
+
// Handle new messages
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (messagesCount > prevMessagesCount.current) {
|
|
61
|
+
scrollToBottom('smooth');
|
|
62
|
+
}
|
|
63
|
+
prevMessagesCount.current = messagesCount;
|
|
64
|
+
}, [messagesCount, scrollToBottom]);
|
|
65
|
+
return {
|
|
66
|
+
containerRef,
|
|
67
|
+
endRef,
|
|
68
|
+
scrollToBottom,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
|
+
import type { IconData } from '@gravity-ui/uikit';
|
|
1
2
|
import type { TAssistantMessage, TChatMessage, TMessageContent, TMessageContentUnion, TMessageMetadata, TUserMessage } from '../types';
|
|
2
3
|
export declare function isUserMessage<Metadata = TMessageMetadata, TCustomMessageContent extends TMessageContent = never>(message: TChatMessage<TCustomMessageContent, Metadata>): message is TUserMessage<Metadata>;
|
|
3
4
|
export declare function isAssistantMessage<Metadata = TMessageMetadata, TCustomMessageContent extends TMessageContent = never>(message: TChatMessage<TCustomMessageContent, Metadata>): message is TAssistantMessage<TCustomMessageContent, Metadata>;
|
|
4
5
|
export declare function normalizeContent<TCustomMessageContent extends TMessageContent = never>(content: TAssistantMessage<TCustomMessageContent, TMessageMetadata>['content']): TMessageContentUnion<TCustomMessageContent>[];
|
|
6
|
+
export type DefaultMessageAction<TMessage> = {
|
|
7
|
+
type: string;
|
|
8
|
+
onClick: (message: TMessage) => void;
|
|
9
|
+
icon?: IconData;
|
|
10
|
+
};
|
|
11
|
+
export declare function resolveMessageActions<TMessage extends TChatMessage<TMessageContent, TMessageMetadata>>(message: TMessage, defaultActions?: DefaultMessageAction<TMessage>[]): Array<{
|
|
12
|
+
type: string;
|
|
13
|
+
onClick: () => void;
|
|
14
|
+
icon?: IconData;
|
|
15
|
+
}> | undefined;
|
|
@@ -23,3 +23,12 @@ export function normalizeContent(content) {
|
|
|
23
23
|
}
|
|
24
24
|
return [content];
|
|
25
25
|
}
|
|
26
|
+
export function resolveMessageActions(message, defaultActions) {
|
|
27
|
+
if (message.actions) {
|
|
28
|
+
return message.actions;
|
|
29
|
+
}
|
|
30
|
+
if (defaultActions) {
|
|
31
|
+
return defaultActions.map((action) => (Object.assign(Object.assign({}, action), { onClick: () => action.onClick(message) })));
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gravity-ui/aikit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Gravity UI base kit for building ai assistant chats",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"files": [
|
|
8
9
|
"dist"
|
|
9
10
|
],
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">= 20",
|
|
13
|
+
"npm": ">= 9",
|
|
14
|
+
"yarn": "Please use npm instead of yarn to install dependencies",
|
|
15
|
+
"pnpm": "Please use npm instead of pnpm to install dependencies"
|
|
16
|
+
},
|
|
17
|
+
"sideEffects": [
|
|
18
|
+
"*.css",
|
|
19
|
+
"*.scss"
|
|
20
|
+
],
|
|
10
21
|
"repository": {
|
|
11
22
|
"type": "git",
|
|
12
23
|
"url": "git+https://github.com/gravity-ui/aikit.git"
|
|
@@ -26,10 +37,11 @@
|
|
|
26
37
|
"lint:styles:fix": "stylelint **/*.scss --fix",
|
|
27
38
|
"lint:prettier": "prettier --check '**/*.{js,jsx,ts,tsx,css,scss,json,yaml,yml,md,mdx}'",
|
|
28
39
|
"lint:prettier:fix": "prettier --write '**/*.{js,jsx,ts,tsx,css,scss,json,yaml,yml,md,mdx}'",
|
|
29
|
-
"test": "
|
|
40
|
+
"test": "npm run test:unit",
|
|
30
41
|
"build": "tsc",
|
|
31
42
|
"start": "TS_NODE_PROJECT=.storybook/tsconfig.json storybook dev",
|
|
32
43
|
"prepublishOnly": "npm run build",
|
|
44
|
+
"test:unit": "jest --passWithNoTests",
|
|
33
45
|
"playwright:install": "playwright install chromium webkit --with-deps",
|
|
34
46
|
"playwright": "playwright test --config=playwright-ct.config.ts",
|
|
35
47
|
"playwright:update": "npm run playwright -- -u all",
|