@chainai/react 1.0.1 → 1.0.3

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.
@@ -99,7 +99,7 @@ const ChainAIWidgetInner = ({ wallets, activeChain, branding = {}, dimensions =
99
99
  const messagesEndRef = (0, react_1.useRef)(null);
100
100
  const inputRef = (0, react_1.useRef)(null);
101
101
  const { isReady } = (0, ChainAIProvider_1.useChainAI)();
102
- const { messages, isLoading, sendMessage } = (0, useChat_1.useChat)();
102
+ const { messages, isLoading, loadingStage, loadingLabel, sendMessage } = (0, useChat_1.useChat)();
103
103
  // Merge custom theme with defaults
104
104
  const theme = {
105
105
  ...themes_1.defaultTheme,
@@ -231,12 +231,13 @@ const ChainAIWidgetInner = ({ wallets, activeChain, branding = {}, dimensions =
231
231
  message.role === 'assistant' && (react_1.default.createElement(Avatar, { branding: branding, size: "small" })),
232
232
  react_1.default.createElement("div", { className: "chain-ai-widget-message-bubble" },
233
233
  react_1.default.createElement(ChatMessage_1.ChatMessage, { message: message, theme: theme })))))),
234
- isLoading && (react_1.default.createElement("div", { className: "chain-ai-widget-message chain-ai-widget-message-assistant" },
234
+ isLoading && loadingStage !== 'idle' && loadingStage !== 'complete' && (react_1.default.createElement("div", { className: "chain-ai-widget-message chain-ai-widget-message-assistant" },
235
235
  react_1.default.createElement(Avatar, { branding: branding, size: "small" }),
236
- react_1.default.createElement("div", { className: "chain-ai-widget-typing" },
237
- react_1.default.createElement("span", null),
238
- react_1.default.createElement("span", null),
239
- react_1.default.createElement("span", null)))),
236
+ react_1.default.createElement("div", { className: "chain-ai-widget-loading-stage" },
237
+ react_1.default.createElement("div", { className: "chain-ai-widget-loading-spinner" }),
238
+ react_1.default.createElement("span", { className: "chain-ai-widget-loading-text" },
239
+ loadingLabel,
240
+ "...")))),
240
241
  pendingAction && (react_1.default.createElement(ActionPrompt_1.ActionPrompt, { action: pendingAction.action, missing: pendingAction.missing, prompt: pendingAction.prompt, onSubmit: handleActionFieldSubmit, theme: theme })),
241
242
  react_1.default.createElement("div", { ref: messagesEndRef })),
242
243
  react_1.default.createElement("div", { className: "chain-ai-widget-input-area" },
@@ -2,6 +2,7 @@
2
2
  * useChat hook - Manages chat messages and state
3
3
  */
4
4
  import type { Message, WalletContext } from '@chainai/core';
5
+ export type LoadingStage = 'idle' | 'thinking' | 'analyzing' | 'generating' | 'complete';
5
6
  export interface SendMessageOptions {
6
7
  /** @deprecated Use walletContext instead */
7
8
  userContext?: any;
@@ -11,6 +12,8 @@ export interface SendMessageOptions {
11
12
  export interface UseChatReturn {
12
13
  messages: Message[];
13
14
  isLoading: boolean;
15
+ loadingStage: LoadingStage;
16
+ loadingLabel: string;
14
17
  error: string | null;
15
18
  /**
16
19
  * Send a message to the AI
@@ -6,11 +6,44 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.useChat = void 0;
7
7
  const react_1 = require("react");
8
8
  const ChainAIProvider_1 = require("../context/ChainAIProvider");
9
+ const LOADING_STAGES = [
10
+ { stage: 'thinking', label: 'Thinking', duration: 600 },
11
+ { stage: 'analyzing', label: 'Analyzing request', duration: 800 },
12
+ { stage: 'generating', label: 'Generating response', duration: 0 }, // stays until response
13
+ ];
9
14
  const useChat = (conversationId) => {
10
15
  const { client, isReady } = (0, ChainAIProvider_1.useChainAI)();
11
16
  const [messages, setMessages] = (0, react_1.useState)([]);
12
17
  const [isLoading, setIsLoading] = (0, react_1.useState)(false);
18
+ const [loadingStage, setLoadingStage] = (0, react_1.useState)('idle');
19
+ const [loadingLabel, setLoadingLabel] = (0, react_1.useState)('');
13
20
  const [error, setError] = (0, react_1.useState)(null);
21
+ const stageTimeoutsRef = (0, react_1.useRef)([]);
22
+ const clearStageTimeouts = () => {
23
+ stageTimeoutsRef.current.forEach(clearTimeout);
24
+ stageTimeoutsRef.current = [];
25
+ };
26
+ const startLoadingStages = () => {
27
+ clearStageTimeouts();
28
+ let cumulativeDelay = 0;
29
+ LOADING_STAGES.forEach(({ stage, label, duration }) => {
30
+ const timeout = setTimeout(() => {
31
+ setLoadingStage(stage);
32
+ setLoadingLabel(label);
33
+ }, cumulativeDelay);
34
+ stageTimeoutsRef.current.push(timeout);
35
+ cumulativeDelay += duration;
36
+ });
37
+ };
38
+ const stopLoadingStages = () => {
39
+ clearStageTimeouts();
40
+ setLoadingStage('complete');
41
+ setLoadingLabel('');
42
+ // Reset to idle after a brief moment
43
+ setTimeout(() => {
44
+ setLoadingStage('idle');
45
+ }, 100);
46
+ };
14
47
  const sendMessage = (0, react_1.useCallback)(async (text, options) => {
15
48
  if (!client || !isReady) {
16
49
  setError('Client not ready');
@@ -26,8 +59,10 @@ const useChat = (conversationId) => {
26
59
  setMessages((prev) => [...prev, userMessage]);
27
60
  setIsLoading(true);
28
61
  setError(null);
62
+ startLoadingStages();
29
63
  try {
30
64
  let assistantResponse = '';
65
+ let hasReceivedContent = false;
31
66
  const assistantMessage = {
32
67
  id: (Date.now() + 1).toString(),
33
68
  role: 'assistant',
@@ -42,6 +77,11 @@ const useChat = (conversationId) => {
42
77
  walletContext: options?.walletContext,
43
78
  }, {
44
79
  onChunk: (chunk) => {
80
+ // Once we receive content, stop showing loading stages
81
+ if (!hasReceivedContent && chunk.length > 0) {
82
+ hasReceivedContent = true;
83
+ stopLoadingStages();
84
+ }
45
85
  assistantResponse += chunk;
46
86
  setMessages((prev) => {
47
87
  const updated = [...prev];
@@ -53,15 +93,18 @@ const useChat = (conversationId) => {
53
93
  });
54
94
  },
55
95
  onDone: () => {
96
+ stopLoadingStages();
56
97
  setIsLoading(false);
57
98
  },
58
99
  onError: (err) => {
100
+ stopLoadingStages();
59
101
  setError(err);
60
102
  setIsLoading(false);
61
103
  },
62
104
  });
63
105
  }
64
106
  catch (err) {
107
+ stopLoadingStages();
65
108
  setError(err instanceof Error ? err.message : 'An error occurred');
66
109
  setIsLoading(false);
67
110
  }
@@ -69,10 +112,14 @@ const useChat = (conversationId) => {
69
112
  const clearMessages = (0, react_1.useCallback)(() => {
70
113
  setMessages([]);
71
114
  setError(null);
115
+ setLoadingStage('idle');
116
+ setLoadingLabel('');
72
117
  }, []);
73
118
  return {
74
119
  messages,
75
120
  isLoading,
121
+ loadingStage,
122
+ loadingLabel,
76
123
  error,
77
124
  sendMessage,
78
125
  clearMessages,
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@ export type { ActionPromptProps } from './components/ActionPrompt';
18
18
  export { ChainAIProvider, useChainAI } from './context/ChainAIProvider';
19
19
  export type { ChainAIContextValue, ChainAIProviderProps } from './context/ChainAIProvider';
20
20
  export { useChat } from './hooks/useChat';
21
- export type { UseChatReturn } from './hooks/useChat';
21
+ export type { UseChatReturn, LoadingStage } from './hooks/useChat';
22
22
  export { defaultTheme, darkTheme, lightTheme } from './styles/themes';
23
23
  export type { Theme } from './styles/themes';
24
24
  export type { ChainAIConfig, ResponseCustomization, ResponseStyle, ResponseLength, Message, WalletAddress, WalletContext, } from '@chainai/core';
@@ -85,6 +85,7 @@
85
85
  line-height: 1.5;
86
86
  white-space: pre-wrap;
87
87
  word-wrap: break-word;
88
+ text-align: left;
88
89
  }
89
90
 
90
91
  .chain-ai-message-timestamp {
@@ -278,6 +278,43 @@
278
278
  40% { transform: scale(1); opacity: 1; }
279
279
  }
280
280
 
281
+ /* Loading Stage Indicator */
282
+ .chain-ai-widget-loading-stage {
283
+ display: flex;
284
+ align-items: center;
285
+ gap: 10px;
286
+ padding: 12px 16px;
287
+ background: var(--chain-ai-assistant-bg, #f3f4f6);
288
+ border-radius: 16px;
289
+ border-bottom-left-radius: 4px;
290
+ color: var(--chain-ai-assistant-text, #6b7280);
291
+ font-size: 13px;
292
+ }
293
+
294
+ .chain-ai-widget-loading-spinner {
295
+ width: 16px;
296
+ height: 16px;
297
+ border: 2px solid var(--chain-ai-border, #e5e7eb);
298
+ border-top-color: var(--chain-ai-primary, #3b82f6);
299
+ border-radius: 50%;
300
+ animation: chain-ai-spin 0.8s linear infinite;
301
+ flex-shrink: 0;
302
+ }
303
+
304
+ @keyframes chain-ai-spin {
305
+ to { transform: rotate(360deg); }
306
+ }
307
+
308
+ .chain-ai-widget-loading-text {
309
+ display: flex;
310
+ align-items: center;
311
+ }
312
+
313
+ /* Message content alignment */
314
+ .chain-ai-widget-message-bubble .chain-ai-message {
315
+ text-align: left;
316
+ }
317
+
281
318
  /* Input Area */
282
319
  .chain-ai-widget-input-area {
283
320
  display: flex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chainai/react",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "React components for Chain AI - Embeddable blockchain AI assistant widget",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -44,7 +44,7 @@
44
44
  "author": "Chain AI",
45
45
  "license": "MIT",
46
46
  "dependencies": {
47
- "@chainai/core": "^1.0.1"
47
+ "@chainai/core": "^1.0.3"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@types/react": "^18.3.3",
@@ -85,6 +85,7 @@
85
85
  line-height: 1.5;
86
86
  white-space: pre-wrap;
87
87
  word-wrap: break-word;
88
+ text-align: left;
88
89
  }
89
90
 
90
91
  .chain-ai-message-timestamp {
package/styles/widget.css CHANGED
@@ -278,6 +278,43 @@
278
278
  40% { transform: scale(1); opacity: 1; }
279
279
  }
280
280
 
281
+ /* Loading Stage Indicator */
282
+ .chain-ai-widget-loading-stage {
283
+ display: flex;
284
+ align-items: center;
285
+ gap: 10px;
286
+ padding: 12px 16px;
287
+ background: var(--chain-ai-assistant-bg, #f3f4f6);
288
+ border-radius: 16px;
289
+ border-bottom-left-radius: 4px;
290
+ color: var(--chain-ai-assistant-text, #6b7280);
291
+ font-size: 13px;
292
+ }
293
+
294
+ .chain-ai-widget-loading-spinner {
295
+ width: 16px;
296
+ height: 16px;
297
+ border: 2px solid var(--chain-ai-border, #e5e7eb);
298
+ border-top-color: var(--chain-ai-primary, #3b82f6);
299
+ border-radius: 50%;
300
+ animation: chain-ai-spin 0.8s linear infinite;
301
+ flex-shrink: 0;
302
+ }
303
+
304
+ @keyframes chain-ai-spin {
305
+ to { transform: rotate(360deg); }
306
+ }
307
+
308
+ .chain-ai-widget-loading-text {
309
+ display: flex;
310
+ align-items: center;
311
+ }
312
+
313
+ /* Message content alignment */
314
+ .chain-ai-widget-message-bubble .chain-ai-message {
315
+ text-align: left;
316
+ }
317
+
281
318
  /* Input Area */
282
319
  .chain-ai-widget-input-area {
283
320
  display: flex;