@gravity-ui/aikit 1.2.0 → 1.3.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.
@@ -5,6 +5,7 @@ declare const _default: Meta;
5
5
  export default _default;
6
6
  export declare const Playground: StoryFn<AssistantMessageProps>;
7
7
  export declare const WithToolCall: StoryObj<AssistantMessageProps>;
8
+ export declare const WithThinkingContent: StoryObj<AssistantMessageProps>;
8
9
  interface CustomMessageData {
9
10
  title: string;
10
11
  description: string;
@@ -108,6 +108,37 @@ export const WithToolCall = {
108
108
  (StoryComponent) => (_jsx(Showcase, { children: _jsx(StoryComponent, {}) })),
109
109
  ],
110
110
  };
111
+ const thinkingWithTextMessage = {
112
+ id: '5',
113
+ role: 'assistant',
114
+ content: [
115
+ {
116
+ type: 'thinking',
117
+ data: {
118
+ title: 'Analyzing request',
119
+ content: [
120
+ 'Breaking down the task into steps',
121
+ 'Checking available context',
122
+ 'Planning the response structure',
123
+ ],
124
+ status: 'thought',
125
+ enabledCopy: true,
126
+ },
127
+ },
128
+ {
129
+ type: 'text',
130
+ data: {
131
+ text: "Based on my analysis, here's the answer to your question. The solution involves several key components that work together.",
132
+ },
133
+ },
134
+ ],
135
+ };
136
+ export const WithThinkingContent = {
137
+ render: (args) => (_jsx(ShowcaseItem, { title: "With Thinking Content", children: _jsx(ContentWrapper, { width: "480px", children: _jsx(AssistantMessage, Object.assign({}, args, { content: thinkingWithTextMessage.content, actions: actions, timestamp: thinkingWithTextMessage.timestamp, id: thinkingWithTextMessage.id })) }) })),
138
+ decorators: [
139
+ (StoryComponent) => (_jsx(Showcase, { children: _jsx(StoryComponent, {}) })),
140
+ ],
141
+ };
111
142
  const CustomMessageView = ({ part, }) => {
112
143
  const { title, description } = part.data;
113
144
  return (_jsxs("div", { style: {
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useScrollPreservation, useSmartScroll } from '../../../hooks';
3
- import { isAssistantMessage, isUserMessage, resolveMessageActions, } from '../../../utils';
3
+ import { hasOnlyThinkingContent, isAssistantMessage, isUserMessage, resolveMessageActions, } from '../../../utils';
4
4
  import { block } from '../../../utils/cn';
5
5
  import { IntersectionContainer } from '../../atoms/IntersectionContainer';
6
6
  import { Loader } from '../../atoms/Loader';
@@ -29,7 +29,9 @@ export function MessageList({ messages, messageRendererRegistry, transformOption
29
29
  const isLastMessage = index === messages.length - 1;
30
30
  const isNotCompleted = isSubmitted || isStreaming;
31
31
  const showActions = !(isLastMessage && isNotCompleted);
32
- const actions = showActions
32
+ // Don't show assistantActions for messages with ONLY thinking content
33
+ // For mixed content (thinking + text), actions are shown and copy entire message
34
+ const actions = showActions && !hasOnlyThinkingContent(message.content)
33
35
  ? resolveMessageActions(message, assistantActions)
34
36
  : undefined;
35
37
  return (_jsx(AssistantMessage, { content: message.content, actions: actions, timestamp: message.timestamp, id: message.id, messageRendererRegistry: messageRendererRegistry, transformOptions: transformOptions, shouldParseIncompleteMarkdown: shouldParseIncompleteMarkdown, showActionsOnHover: showActionsOnHover, showTimestamp: showTimestamp }, message.id || `message-${index}`));
@@ -9,4 +9,6 @@ export declare const SingleContent: StoryObj<ThinkingMessageProps>;
9
9
  export declare const Collapsed: StoryObj<ThinkingMessageProps>;
10
10
  export declare const WithoutLoader: StoryObj<ThinkingMessageProps>;
11
11
  export declare const WithCopyAction: StoryObj<ThinkingMessageProps>;
12
+ export declare const WithEnabledCopy: StoryObj<ThinkingMessageProps>;
13
+ export declare const CopyOnlyWhenThought: StoryObj<ThinkingMessageProps>;
12
14
  export declare const WithCustomStyle: StoryObj<ThinkingMessageProps>;
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { ThinkingMessage } from '..';
3
3
  import { ContentWrapper } from '../../../../demo/ContentWrapper';
4
4
  import { Showcase } from '../../../../demo/Showcase';
@@ -44,7 +44,11 @@ export default {
44
44
  },
45
45
  onCopyClick: {
46
46
  action: 'copy clicked',
47
- description: 'Copy button click handler',
47
+ description: 'Copy button click handler (custom logic)',
48
+ },
49
+ enabledCopy: {
50
+ control: 'boolean',
51
+ description: 'Enable default copy functionality',
48
52
  },
49
53
  },
50
54
  };
@@ -89,7 +93,15 @@ export const WithoutLoader = {
89
93
  decorators: defaultDecorators,
90
94
  };
91
95
  export const WithCopyAction = {
92
- render: () => (_jsx(ShowcaseItem, { title: "With Copy Action", children: _jsx(ContentWrapper, { width: "600px", children: _jsx(ThinkingMessage, Object.assign({}, thoughtData, { defaultExpanded: true, onCopyClick: () => alert('Content copied to clipboard!') })) }) })),
96
+ render: () => (_jsx(ShowcaseItem, { title: "With Copy Action (Custom Handler)", children: _jsx(ContentWrapper, { width: "600px", children: _jsx(ThinkingMessage, Object.assign({}, thoughtData, { defaultExpanded: true, onCopyClick: () => alert('Custom copy handler called!') })) }) })),
97
+ decorators: defaultDecorators,
98
+ };
99
+ export const WithEnabledCopy = {
100
+ render: () => (_jsx(ShowcaseItem, { title: "With Enabled Copy (Default Logic)", children: _jsx(ContentWrapper, { width: "600px", children: _jsx(ThinkingMessage, Object.assign({}, thoughtData, { defaultExpanded: true, enabledCopy: true })) }) })),
101
+ decorators: defaultDecorators,
102
+ };
103
+ export const CopyOnlyWhenThought = {
104
+ render: () => (_jsxs(_Fragment, { children: [_jsx(ShowcaseItem, { title: "Thinking Status - No Copy Button", children: _jsx(ContentWrapper, { width: "600px", children: _jsx(ThinkingMessage, Object.assign({}, thinkingData, { defaultExpanded: true, enabledCopy: true })) }) }), _jsx(ShowcaseItem, { title: "Thought Status - With Copy Button", children: _jsx(ContentWrapper, { width: "600px", children: _jsx(ThinkingMessage, Object.assign({}, thoughtData, { defaultExpanded: true, enabledCopy: true })) }) })] })),
93
105
  decorators: defaultDecorators,
94
106
  };
95
107
  export const WithCustomStyle = {
@@ -18,7 +18,7 @@ const b = block('thinking-message');
18
18
  * @returns Rendered thinking message component
19
19
  */
20
20
  export const ThinkingMessage = (props) => {
21
- const { className, qa, style, onCopyClick } = props, data = __rest(props, ["className", "qa", "style", "onCopyClick"]);
22
- const { isExpanded, toggleExpanded, buttonTitle, content, showLoader } = useThinkingMessage(Object.assign({}, data));
23
- return (_jsxs("div", { className: b(null, className), "data-qa": qa, style: style, children: [_jsxs("div", { className: b('buttons'), children: [_jsxs(Button, { size: "s", onClick: toggleExpanded, children: [buttonTitle, _jsx(Icon, { data: isExpanded ? ChevronUp : ChevronDown })] }), showLoader ? (_jsx(Loader, { view: "loading", size: "xs" })) : (onCopyClick && (_jsx(ActionButton, { size: "s", onClick: onCopyClick, children: _jsx(Icon, { data: Copy, size: 16 }) })))] }), isExpanded && (_jsx("div", { className: b('container'), children: content.map((item, index) => (_jsx(Text, { className: b('content'), children: item }, index))) }))] }));
21
+ const { className, qa, style } = props, data = __rest(props, ["className", "qa", "style"]);
22
+ const { isExpanded, toggleExpanded, buttonTitle, content, showLoader, handleCopy, showCopyButton, } = useThinkingMessage(data);
23
+ return (_jsxs("div", { className: b(null, className), "data-qa": qa, style: style, children: [_jsxs("div", { className: b('buttons'), children: [_jsxs(Button, { size: "s", onClick: toggleExpanded, children: [buttonTitle, _jsx(Icon, { data: isExpanded ? ChevronUp : ChevronDown })] }), showLoader ? (_jsx(Loader, { view: "loading", size: "xs" })) : (showCopyButton && (_jsx(ActionButton, { size: "s", onClick: handleCopy, children: _jsx(Icon, { data: Copy, size: 16 }) })))] }), isExpanded && (_jsx("div", { className: b('container'), children: content.map((item, index) => (_jsx(Text, { className: b('content'), children: item }, index))) }))] }));
24
24
  };
@@ -5,4 +5,6 @@ export declare function useThinkingMessage(options: ThinkingMessageContentData):
5
5
  buttonTitle: string;
6
6
  content: string[];
7
7
  showLoader: boolean;
8
+ handleCopy: () => void;
9
+ showCopyButton: boolean;
8
10
  };
@@ -1,7 +1,8 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
+ import { copyToClipboard } from '../../../utils';
2
3
  import { i18n } from './i18n';
3
4
  export function useThinkingMessage(options) {
4
- const { defaultExpanded = true, showStatusIndicator = true, status, content } = options;
5
+ const { defaultExpanded = true, showStatusIndicator = true, status, content, onCopyClick, enabledCopy = false, } = options;
5
6
  const [isExpanded, setIsExpanded] = useState(defaultExpanded);
6
7
  const buttonTitle = useMemo(() => {
7
8
  return i18n(`title-${status}`);
@@ -12,11 +13,24 @@ export function useThinkingMessage(options) {
12
13
  const toggleExpanded = useCallback(() => {
13
14
  setIsExpanded((prev) => !prev);
14
15
  }, []);
16
+ const handleCopy = useCallback(() => {
17
+ if (onCopyClick) {
18
+ // Priority 1: Use custom handler for backward compatibility
19
+ onCopyClick();
20
+ }
21
+ else if (enabledCopy) {
22
+ // Priority 2: Use default copy logic
23
+ copyToClipboard(content);
24
+ }
25
+ }, [onCopyClick, enabledCopy, content]);
26
+ const showCopyButton = Boolean(onCopyClick || enabledCopy);
15
27
  return {
16
28
  isExpanded,
17
29
  toggleExpanded,
18
30
  buttonTitle,
19
31
  content: contentArray,
20
32
  showLoader: showStatusIndicator && status === 'thinking',
33
+ handleCopy,
34
+ showCopyButton,
21
35
  };
22
36
  }
@@ -1 +1,3 @@
1
- @import '@gravity-ui/uikit/styles/styles.css';
1
+ @import '../themes/common.css';
2
+ @import '../themes/light.css';
3
+ @import '../themes/dark.css';
@@ -0,0 +1,73 @@
1
+ /**
2
+ * CSS variables for library theming
3
+ */
4
+
5
+ .g-root {
6
+ /* === Colors === */
7
+ --g-aikit-color-bg-primary: var(--g-aikit-bg-primary, var(--g-color-base-float));
8
+ --g-aikit-color-bg-secondary: #f5f5f5;
9
+
10
+ /* === Layout === */
11
+ --g-aikit-layout-base-padding-m: 12px;
12
+
13
+ /* === Messages === */
14
+ --g-aikit-color-bg-message-user: #0077ff;
15
+ --g-aikit-color-bg-message-assistant: #f0f0f0;
16
+
17
+ /* === Disclaimer === */
18
+ --g-aikit-disclaimer-gap: 10px;
19
+
20
+ /* === Suggestions === */
21
+ --g-aikit-suggestions-box-shadow: 0 3px 10px rgba(198, 172, 255, 0.52);
22
+
23
+ /* === Header === */
24
+ --g-aikit-header-background: none;
25
+
26
+ /* === Context Indicator === */
27
+ --g-aikit-ci-color-progress-1: var(
28
+ --g-aikit-ci-progress-1,
29
+ var(--g-color-private-green-550-solid)
30
+ );
31
+ --g-aikit-ci-color-progress-2: var(
32
+ --g-aikit-ci-progress-2,
33
+ var(--g-color-private-orange-500-solid)
34
+ );
35
+ --g-aikit-ci-color-progress-3: var(
36
+ --g-aikit-ci-progress-3,
37
+ var(--g-color-private-red-500-solid)
38
+ );
39
+
40
+ /* === Shimmer === */
41
+ --g-aikit-shimmer-color-from: var(--g-aikit-shimmer-from, rgba(0, 0, 0, 0.35));
42
+ --g-aikit-shimmer-color-to: var(--g-aikit-shimmer-to, rgba(0, 0, 0, 0.85));
43
+ --g-aikit-shimmer-duration: var(--g-aikit-shimmer-time, 2.5s);
44
+ --g-aikit-shimmer-gradient-size: 200%;
45
+
46
+ /* === Chat History === */
47
+ --g-aikit-history-width: 360px;
48
+ --g-aikit-history-max-height: 560px;
49
+ --g-aikit-history-item-height: 24px;
50
+
51
+ /* === Prompt Input === */
52
+ --g-aikit-prompt-input-panel-max-height: 500px;
53
+
54
+ /* === Empty Container === */
55
+ --g-aikit-empty-container-background: var(--g-color-base-background);
56
+ --g-aikit-empty-container-content-gap: 48px;
57
+ --g-aikit-empty-container-padding: 48px 32px;
58
+
59
+ /* === Chat Content === */
60
+ --g-aikit-chat-content-background: var(--g-color-base-background);
61
+ --g-aikit-chat-content-padding: 0 var(--g-aikit-layout-base-padding-m) var(--g-spacing-4)
62
+ var(--g-aikit-layout-base-padding-m);
63
+
64
+ /* === Chat Container === */
65
+ --g-aikit-chat-container-header-background: var(--g-color-base-background);
66
+ --g-aikit-chat-container-content-background: var(--g-color-base-background);
67
+ --g-aikit-chat-container-footer-background: var(--g-color-base-background);
68
+ --g-aikit-chat-container-background: var(--g-color-base-background);
69
+ --g-aikit-chat-container-content-empty-background: var(--g-color-base-background);
70
+ --g-aikit-chat-container-content-chat-background: var(--g-color-base-background);
71
+ --g-aikit-chat-container-footer-empty-background: var(--g-color-base-background);
72
+ --g-aikit-chat-container-footer-chat-background: var(--g-color-base-background);
73
+ }
@@ -1,3 +1,5 @@
1
+ /* DEPRECATED: Use common.css instead */
2
+
1
3
  /**
2
4
  * CSS variables for library theming
3
5
  */
@@ -41,6 +41,7 @@ export type ThinkingMessageContentData = {
41
41
  defaultExpanded?: boolean;
42
42
  showStatusIndicator?: boolean;
43
43
  onCopyClick?: () => void;
44
+ enabledCopy?: boolean;
44
45
  className?: string;
45
46
  qa?: string;
46
47
  };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Formats content for copying to clipboard.
3
+ * Joins array items with double newline, or returns string as-is.
4
+ *
5
+ * @param content - Content to format (string or array of strings)
6
+ * @returns Formatted text ready for clipboard
7
+ */
8
+ export declare function formatCopyText(content: string | string[]): string;
9
+ /**
10
+ * Copy content to clipboard using execCommand.
11
+ * Reliable method that works in all environments including iframes, HTTP, and older browsers.
12
+ * Accepts both string and array of strings (arrays are joined with double newline).
13
+ *
14
+ * @param content - Content to copy (string or array of strings)
15
+ * @returns true if copy was successful, false otherwise
16
+ */
17
+ export declare function copyToClipboard(content: string | string[]): boolean;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Formats content for copying to clipboard.
3
+ * Joins array items with double newline, or returns string as-is.
4
+ *
5
+ * @param content - Content to format (string or array of strings)
6
+ * @returns Formatted text ready for clipboard
7
+ */
8
+ export function formatCopyText(content) {
9
+ if (Array.isArray(content)) {
10
+ return content.join('\n\n');
11
+ }
12
+ return content;
13
+ }
14
+ /**
15
+ * Copy content to clipboard using execCommand.
16
+ * Reliable method that works in all environments including iframes, HTTP, and older browsers.
17
+ * Accepts both string and array of strings (arrays are joined with double newline).
18
+ *
19
+ * @param content - Content to copy (string or array of strings)
20
+ * @returns true if copy was successful, false otherwise
21
+ */
22
+ export function copyToClipboard(content) {
23
+ const text = formatCopyText(content);
24
+ const textarea = document.createElement('textarea');
25
+ textarea.value = text;
26
+ textarea.style.top = '0';
27
+ textarea.style.left = '0';
28
+ textarea.style.position = 'fixed';
29
+ textarea.style.opacity = '0';
30
+ document.body.appendChild(textarea);
31
+ textarea.focus();
32
+ textarea.select();
33
+ let successful = false;
34
+ try {
35
+ successful = document.execCommand('copy');
36
+ }
37
+ catch (err) {
38
+ // eslint-disable-next-line no-console
39
+ console.error('Failed to copy text:', err);
40
+ }
41
+ document.body.removeChild(textarea);
42
+ return successful;
43
+ }
@@ -2,3 +2,4 @@ export * from './chatUtils';
2
2
  export * from './messageUtils';
3
3
  export * from './validation';
4
4
  export * from './messageTypeRegistry';
5
+ export * from './clipboardUtils';
@@ -3,3 +3,4 @@ export * from './chatUtils';
3
3
  export * from './messageUtils';
4
4
  export * from './validation';
5
5
  export * from './messageTypeRegistry';
6
+ export * from './clipboardUtils';
@@ -3,6 +3,12 @@ import type { BaseMessageAction, TAssistantMessage, TChatMessage, TMessageConten
3
3
  export declare function isUserMessage<Metadata = TMessageMetadata, TCustomMessageContent extends TMessageContent = never>(message: TChatMessage<TCustomMessageContent, Metadata>): message is TUserMessage<Metadata>;
4
4
  export declare function isAssistantMessage<Metadata = TMessageMetadata, TCustomMessageContent extends TMessageContent = never>(message: TChatMessage<TCustomMessageContent, Metadata>): message is TAssistantMessage<TCustomMessageContent, Metadata>;
5
5
  export declare function normalizeContent<TCustomMessageContent extends TMessageContent = never>(content: TAssistantMessage<TCustomMessageContent, TMessageMetadata>['content']): TMessageContentUnion<TCustomMessageContent>[];
6
+ /**
7
+ * Check if message content contains ONLY thinking content (no other content types).
8
+ * @param content - Message content to check
9
+ * @returns true if content contains only thinking parts and nothing else
10
+ */
11
+ export declare function hasOnlyThinkingContent<TCustomMessageContent extends TMessageContent = never>(content: TAssistantMessage<TCustomMessageContent, TMessageMetadata>['content']): boolean;
6
12
  export type DefaultMessageAction<TMessage> = {
7
13
  type?: string;
8
14
  onClick: (message: TMessage) => void;
@@ -23,6 +23,18 @@ export function normalizeContent(content) {
23
23
  }
24
24
  return [content];
25
25
  }
26
+ /**
27
+ * Check if message content contains ONLY thinking content (no other content types).
28
+ * @param content - Message content to check
29
+ * @returns true if content contains only thinking parts and nothing else
30
+ */
31
+ export function hasOnlyThinkingContent(content) {
32
+ const parts = normalizeContent(content);
33
+ if (parts.length === 0) {
34
+ return false;
35
+ }
36
+ return parts.every((part) => part.type === 'thinking');
37
+ }
26
38
  export function resolveMessageActions(message, defaultActions) {
27
39
  if (message.actions) {
28
40
  return message.actions;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravity-ui/aikit",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Gravity UI base kit for building ai assistant chats",
5
5
  "license": "MIT",
6
6
  "main": "./dist/index.js",
@@ -11,6 +11,7 @@
11
11
  "default": "./dist/index.js"
12
12
  },
13
13
  "./styles": "./dist/styles/styles.scss",
14
+ "./themes/common": "./dist/themes/common.css",
14
15
  "./themes/dark": "./dist/themes/dark.css",
15
16
  "./themes/light": "./dist/themes/light.css",
16
17
  "./themes/variables": "./dist/themes/variables.css",