@newfold/wp-module-ai-chat 1.0.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.
Files changed (51) hide show
  1. package/README.md +98 -0
  2. package/package.json +51 -0
  3. package/src/components/chat/ChatHeader.jsx +63 -0
  4. package/src/components/chat/ChatHistoryDropdown.jsx +182 -0
  5. package/src/components/chat/ChatHistoryList.jsx +257 -0
  6. package/src/components/chat/ChatInput.jsx +157 -0
  7. package/src/components/chat/ChatMessage.jsx +157 -0
  8. package/src/components/chat/ChatMessages.jsx +137 -0
  9. package/src/components/chat/WelcomeScreen.jsx +115 -0
  10. package/src/components/icons/CloseIcon.jsx +27 -0
  11. package/src/components/icons/SparklesOutlineIcon.jsx +30 -0
  12. package/src/components/icons/index.js +5 -0
  13. package/src/components/ui/AILogo.jsx +47 -0
  14. package/src/components/ui/BluBetaHeading.jsx +18 -0
  15. package/src/components/ui/ErrorAlert.jsx +30 -0
  16. package/src/components/ui/HeaderBar.jsx +34 -0
  17. package/src/components/ui/SuggestionButton.jsx +28 -0
  18. package/src/components/ui/ToolExecutionList.jsx +264 -0
  19. package/src/components/ui/TypingIndicator.jsx +268 -0
  20. package/src/constants/nfdAgents/input.js +13 -0
  21. package/src/constants/nfdAgents/storageKeys.js +102 -0
  22. package/src/constants/nfdAgents/typingStatus.js +40 -0
  23. package/src/constants/nfdAgents/websocket.js +44 -0
  24. package/src/hooks/useAIChat.js +432 -0
  25. package/src/hooks/useNfdAgentsWebSocket.js +964 -0
  26. package/src/index.js +66 -0
  27. package/src/services/mcpClient.js +433 -0
  28. package/src/services/openaiClient.js +416 -0
  29. package/src/styles/_branding.scss +151 -0
  30. package/src/styles/_history.scss +180 -0
  31. package/src/styles/_input.scss +170 -0
  32. package/src/styles/_messages.scss +272 -0
  33. package/src/styles/_mixins.scss +21 -0
  34. package/src/styles/_typing-indicator.scss +162 -0
  35. package/src/styles/_ui.scss +173 -0
  36. package/src/styles/_vars.scss +103 -0
  37. package/src/styles/_welcome.scss +81 -0
  38. package/src/styles/app.scss +10 -0
  39. package/src/utils/helpers.js +75 -0
  40. package/src/utils/markdownParser.js +319 -0
  41. package/src/utils/nfdAgents/archiveConversation.js +82 -0
  42. package/src/utils/nfdAgents/chatHistoryList.js +130 -0
  43. package/src/utils/nfdAgents/configFetcher.js +137 -0
  44. package/src/utils/nfdAgents/greeting.js +55 -0
  45. package/src/utils/nfdAgents/jwtUtils.js +59 -0
  46. package/src/utils/nfdAgents/messageHandler.js +328 -0
  47. package/src/utils/nfdAgents/storage.js +112 -0
  48. package/src/utils/nfdAgents/typingIndicatorToolDisplay.js +180 -0
  49. package/src/utils/nfdAgents/url.js +101 -0
  50. package/src/utils/restApi.js +87 -0
  51. package/src/utils/sanitizeHtml.js +94 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * NFD Agents Storage Key Constants
3
+ *
4
+ * Site-scoped localStorage key construction and site ID management.
5
+ * Used by useNfdAgentsWebSocket, archiveConversation, and ChatHistoryList.
6
+ * Key format: nfd-ai-chat-{siteId}-{consumer}-{suffix} (or nfd-ai-chat-{consumer}-{suffix} when siteId is empty).
7
+ */
8
+
9
+ /* global localStorage */
10
+
11
+ const SITE_ID_KEY = "nfd-ai-chat-site-id";
12
+
13
+ /** Suffixes for consumer-scoped keys. Single source of truth for migrateStorageKeys and getChatHistoryStorageKeys. */
14
+ const STORAGE_KEY_SUFFIXES = {
15
+ history: "history",
16
+ conversationId: "conversation-id",
17
+ sessionId: "session-id",
18
+ archive: "archive",
19
+ };
20
+
21
+ /**
22
+ * Get the cached site ID from localStorage.
23
+ * Returns '' if not yet cached (backwards compatible with pre-migration keys).
24
+ *
25
+ * @return {string} Cached site ID or empty string
26
+ */
27
+ export const getSiteId = () => {
28
+ try {
29
+ return localStorage.getItem(SITE_ID_KEY) || "";
30
+ } catch {
31
+ return "";
32
+ }
33
+ };
34
+
35
+ /**
36
+ * Cache the site ID in localStorage.
37
+ *
38
+ * @param {string} id Site ID to cache
39
+ */
40
+ export const setSiteId = (id) => {
41
+ try {
42
+ localStorage.setItem(SITE_ID_KEY, id);
43
+ } catch {
44
+ // Ignore storage errors (e.g. private mode, quota).
45
+ }
46
+ };
47
+
48
+ /**
49
+ * Build key prefix for a consumer (with or without site ID).
50
+ *
51
+ * @param {string} siteId Site ID or '' for pre-migration keys
52
+ * @param {string} consumer Consumer identifier
53
+ * @return {string} Key prefix for consumer-scoped keys (e.g. nfd-ai-chat-{siteId}-{consumer}).
54
+ */
55
+ const getKeyPrefix = (siteId, consumer) =>
56
+ siteId ? `nfd-ai-chat-${siteId}-${consumer}` : `nfd-ai-chat-${consumer}`;
57
+
58
+ /**
59
+ * Migrate localStorage data from old-prefix keys to new-prefix keys.
60
+ * Only copies when the new key has no data; then removes the old key.
61
+ *
62
+ * @param {string} oldSiteId Previous site ID ('' for pre-migration keys)
63
+ * @param {string} newSiteId New site ID from config
64
+ * @param {string} consumer Consumer identifier (must match useNfdAgentsWebSocket for same surface)
65
+ */
66
+ export const migrateStorageKeys = (oldSiteId, newSiteId, consumer) => {
67
+ const oldPrefix = getKeyPrefix(oldSiteId, consumer);
68
+ const newPrefix = getKeyPrefix(newSiteId, consumer);
69
+
70
+ try {
71
+ for (const suffix of Object.values(STORAGE_KEY_SUFFIXES)) {
72
+ const oldKey = `${oldPrefix}-${suffix}`;
73
+ const newKey = `${newPrefix}-${suffix}`;
74
+ const oldData = localStorage.getItem(oldKey);
75
+ if (oldData && !localStorage.getItem(newKey)) {
76
+ localStorage.setItem(newKey, oldData);
77
+ }
78
+ if (oldData) {
79
+ localStorage.removeItem(oldKey);
80
+ }
81
+ }
82
+ } catch {
83
+ // Ignore storage errors.
84
+ }
85
+ };
86
+
87
+ /**
88
+ * Get localStorage keys for chat history and related data for a given consumer.
89
+ * Includes the cached site ID in the key prefix for multisite isolation.
90
+ * Must match the keys used in useNfdAgentsWebSocket for the same consumer.
91
+ *
92
+ * @param {string} consumer Consumer identifier (must match useNfdAgentsWebSocket for same surface)
93
+ * @return {{ history: string, conversationId: string, sessionId: string, archive: string }} Object with localStorage key strings for history, conversationId, sessionId, and archive.
94
+ */
95
+ export const getChatHistoryStorageKeys = (consumer) => {
96
+ const prefix = getKeyPrefix(getSiteId(), consumer);
97
+ const keys = {};
98
+ for (const [name, suffix] of Object.entries(STORAGE_KEY_SUFFIXES)) {
99
+ keys[name] = `${prefix}-${suffix}`;
100
+ }
101
+ return keys;
102
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Typing indicator status keys and event mapping.
3
+ * Single source of truth for status values shown between typing_start and typing_stop.
4
+ * Hook maps WebSocket event types to these keys; UI maps keys to display strings.
5
+ */
6
+
7
+ /** Status keys for the typing indicator (no UI copy here) */
8
+ export const TYPING_STATUS = {
9
+ PROCESSING: "processing",
10
+ CONNECTING: "connecting",
11
+ WS_CONNECTING: "ws_connecting",
12
+ TOOL_CALL: "tool_call",
13
+ WORKING: "working",
14
+ // Existing keys used elsewhere
15
+ RECEIVED: "received",
16
+ GENERATING: "generating",
17
+ SUMMARIZING: "summarizing",
18
+ COMPLETED: "completed",
19
+ FAILED: "failed",
20
+ };
21
+
22
+ /**
23
+ * Map WebSocket message type to typing status key.
24
+ * Add new event types here to drive status without scattering branches.
25
+ * Returns null for typing_stop (and unknown types) to clear the indicator.
26
+ *
27
+ * @param {string} eventType data.type from WebSocket message
28
+ * @return {string|null} TYPING_STATUS key, or null to clear the typing indicator
29
+ */
30
+ export function getStatusForEventType(eventType) {
31
+ const map = {
32
+ typing_start: TYPING_STATUS.PROCESSING,
33
+ handoff_request: TYPING_STATUS.CONNECTING,
34
+ handoff_accept: TYPING_STATUS.CONNECTING,
35
+ tool_call: TYPING_STATUS.TOOL_CALL,
36
+ tool_result: TYPING_STATUS.WORKING,
37
+ typing_stop: null,
38
+ };
39
+ return map[eventType] ?? null;
40
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * NFD Agents WebSocket Configuration
3
+ *
4
+ * Constants for WebSocket connection, reconnection, typing indicator timeout,
5
+ * and JWT refresh. Storage key construction lives in storageKeys.js (getChatHistoryStorageKeys).
6
+ */
7
+
8
+ /** WebSocket close code: authentication failed (e.g. expired token). */
9
+ export const WS_CLOSE_AUTH_FAILED = 4000;
10
+
11
+ /** WebSocket close code: missing authentication token. */
12
+ export const WS_CLOSE_MISSING_TOKEN = 4001;
13
+
14
+ /** Buffer (ms) before JWT exp at which to proactively refresh (e.g. 5 min). */
15
+ const FIVE_MINUTES_MS = 5 * 60 * 1000;
16
+
17
+ /** Never schedule proactive refresh sooner than this (ms). */
18
+ const FIVE_MINUTES_MIN_DELAY_MS = 5 * 60 * 1000;
19
+
20
+ /** Don't schedule another proactive refresh sooner than this after the last one (ms). */
21
+ const ONE_HOUR_MS = 60 * 60 * 1000;
22
+
23
+ /** Don't clear config + reset attempts on 4000/4001 more than once per this window (ms). */
24
+ const AUTH_REFRESH_COOLDOWN_MS = 3 * 60 * 1000;
25
+
26
+ /** Consider token expired if exp is within this many ms from now (pre-connect check). */
27
+ const JWT_EXPIRED_BUFFER_MS = 60 * 1000;
28
+
29
+ /** When proactive refresh fires during an in-flight reply, reschedule after this many ms. */
30
+ const JWT_PROACTIVE_REFRESH_DEFER_MS = 30 * 1000;
31
+
32
+ export const NFD_AGENTS_WEBSOCKET = {
33
+ MAX_RECONNECT_ATTEMPTS: 5,
34
+ RECONNECT_DELAY: 1000, // Base delay between reconnect attempts (ms)
35
+ TYPING_TIMEOUT: 60000, // Hide typing indicator if no response within this time (ms)
36
+ WS_CLOSE_AUTH_FAILED,
37
+ WS_CLOSE_MISSING_TOKEN,
38
+ JWT_REFRESH_BUFFER_MS: FIVE_MINUTES_MS,
39
+ JWT_REFRESH_MIN_DELAY_MS: FIVE_MINUTES_MIN_DELAY_MS,
40
+ JWT_PROACTIVE_REFRESH_COOLDOWN_MS: ONE_HOUR_MS,
41
+ AUTH_REFRESH_COOLDOWN_MS,
42
+ JWT_EXPIRED_BUFFER_MS,
43
+ JWT_PROACTIVE_REFRESH_DEFER_MS,
44
+ };
@@ -0,0 +1,432 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useState, useCallback, useRef, useEffect } from "@wordpress/element";
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { generateSessionId } from "../utils/helpers";
10
+
11
+ /**
12
+ * Chat status enumeration
13
+ */
14
+ export const CHAT_STATUS = {
15
+ IDLE: "idle",
16
+ RECEIVED: "received",
17
+ GENERATING: "generating",
18
+ TOOL_CALL: "tool_call",
19
+ SUMMARIZING: "summarizing",
20
+ COMPLETED: "completed",
21
+ FAILED: "failed",
22
+ };
23
+
24
+ /**
25
+ * Default system prompt
26
+ */
27
+ const DEFAULT_SYSTEM_PROMPT = `You are a helpful AI assistant. Be concise and helpful in your responses.`;
28
+
29
+ /**
30
+ * useAIChat Hook
31
+ *
32
+ * A configurable React hook for managing AI chat conversations.
33
+ * Provides extension points for tool handling and message processing.
34
+ *
35
+ * @param {Object} options - Hook configuration options
36
+ * @param {Object} options.mcpClient - MCP client instance for tool execution
37
+ * @param {Object} options.openaiClient - OpenAI client instance for chat completions
38
+ * @param {string} options.systemPrompt - System prompt for the AI
39
+ * @param {Function} options.onToolCall - Callback before tool execution (can intercept)
40
+ * @param {Function} options.onToolResult - Callback after tool execution (for glue code)
41
+ * @param {Function} options.onMessageComplete - Callback when a message is complete
42
+ * @param {Function} options.onError - Callback for errors
43
+ * @param {boolean} options.autoInitialize - Auto-initialize MCP client (default: true)
44
+ * @return {Object} Chat state and controls
45
+ */
46
+ export const useAIChat = ({
47
+ mcpClient = null,
48
+ openaiClient = null,
49
+ systemPrompt = DEFAULT_SYSTEM_PROMPT,
50
+ onToolCall = null,
51
+ onToolResult = null,
52
+ onMessageComplete = null,
53
+ onError = null,
54
+ autoInitialize = true,
55
+ } = {}) => {
56
+ // Chat state
57
+ const [messages, setMessages] = useState([]);
58
+ const [isLoading, setIsLoading] = useState(false);
59
+ const [error, setError] = useState(null);
60
+ const [status, setStatus] = useState(CHAT_STATUS.IDLE);
61
+ const [sessionId, setSessionId] = useState(() => generateSessionId());
62
+
63
+ // Tool execution state
64
+ const [activeToolCall, setActiveToolCall] = useState(null);
65
+ const [executedTools, setExecutedTools] = useState([]);
66
+ const [pendingTools, setPendingTools] = useState([]);
67
+ const [toolProgress, setToolProgress] = useState(null);
68
+
69
+ // MCP state
70
+ const [mcpConnected, setMcpConnected] = useState(false);
71
+ const [mcpTools, setMcpTools] = useState([]);
72
+
73
+ // Refs
74
+ const isProcessingRef = useRef(false);
75
+ const abortControllerRef = useRef(null);
76
+
77
+ /**
78
+ * Initialize MCP client
79
+ */
80
+ const initializeMCP = useCallback(async () => {
81
+ if (!mcpClient) {
82
+ return false;
83
+ }
84
+
85
+ try {
86
+ if (!mcpClient.isConnected()) {
87
+ await mcpClient.connect();
88
+ }
89
+ await mcpClient.initialize();
90
+ setMcpTools(mcpClient.getTools());
91
+ setMcpConnected(true);
92
+ return true;
93
+ } catch (err) {
94
+ // eslint-disable-next-line no-console
95
+ console.error("Failed to initialize MCP:", err);
96
+ setError(`Failed to initialize MCP: ${err.message}`);
97
+ onError?.(err);
98
+ return false;
99
+ }
100
+ }, [mcpClient, onError]);
101
+
102
+ /**
103
+ * Auto-initialize MCP on mount
104
+ */
105
+ useEffect(() => {
106
+ if (autoInitialize && mcpClient) {
107
+ initializeMCP();
108
+ }
109
+
110
+ // Copy ref to variable for cleanup function
111
+ const abortController = abortControllerRef.current;
112
+
113
+ return () => {
114
+ if (abortController) {
115
+ abortController.abort();
116
+ }
117
+ };
118
+ }, [autoInitialize, mcpClient, initializeMCP]);
119
+
120
+ /**
121
+ * Create a new message object
122
+ */
123
+ const createMessage = useCallback((role, content, extras = {}) => {
124
+ return {
125
+ id: `${role}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
126
+ role,
127
+ type: role,
128
+ content,
129
+ timestamp: new Date(),
130
+ ...extras,
131
+ };
132
+ }, []);
133
+
134
+ /**
135
+ * Execute a single tool call
136
+ *
137
+ * The onToolCall callback can return an object with { intercepted: true, result: {...} }
138
+ * to handle the tool call locally instead of calling MCP.
139
+ */
140
+ const executeTool = useCallback(
141
+ async (toolCall) => {
142
+ if (!mcpClient) {
143
+ throw new Error("MCP client not available");
144
+ }
145
+
146
+ const { name, arguments: args } = toolCall;
147
+
148
+ // Call onToolCall - it can optionally intercept and return a result
149
+ if (onToolCall) {
150
+ const interceptResult = await onToolCall(toolCall);
151
+
152
+ // If the callback intercepted the call, use its result
153
+ if (interceptResult && interceptResult.intercepted) {
154
+ const result = interceptResult.result || {
155
+ content: [],
156
+ isError: false,
157
+ };
158
+
159
+ // Notify after execution (for glue code to react to changes)
160
+ if (onToolResult) {
161
+ await onToolResult(toolCall, result);
162
+ }
163
+
164
+ return {
165
+ id: toolCall.id,
166
+ name,
167
+ result,
168
+ isError: result.isError || false,
169
+ hasChanges: result.hasChanges || false,
170
+ undoData: interceptResult.undoData,
171
+ };
172
+ }
173
+ }
174
+
175
+ try {
176
+ const result = await mcpClient.callTool(name, args);
177
+
178
+ // Notify after execution (for glue code to react to changes)
179
+ if (onToolResult) {
180
+ await onToolResult(toolCall, result);
181
+ }
182
+
183
+ return {
184
+ id: toolCall.id,
185
+ name,
186
+ result,
187
+ isError: result.isError || false,
188
+ };
189
+ } catch (err) {
190
+ return {
191
+ id: toolCall.id,
192
+ name,
193
+ error: err.message,
194
+ isError: true,
195
+ };
196
+ }
197
+ },
198
+ [mcpClient, onToolCall, onToolResult]
199
+ );
200
+
201
+ /**
202
+ * Execute all tool calls
203
+ */
204
+ const executeToolCalls = useCallback(
205
+ async (toolCalls) => {
206
+ const results = [];
207
+ setPendingTools(toolCalls.slice(1));
208
+
209
+ for (let i = 0; i < toolCalls.length; i++) {
210
+ const toolCall = toolCalls[i];
211
+
212
+ setActiveToolCall({
213
+ ...toolCall,
214
+ index: i + 1,
215
+ total: toolCalls.length,
216
+ });
217
+
218
+ const result = await executeTool(toolCall);
219
+ results.push(result);
220
+
221
+ setExecutedTools((prev) => [...prev, { ...toolCall, ...result }]);
222
+ setPendingTools(toolCalls.slice(i + 2));
223
+ }
224
+
225
+ setActiveToolCall(null);
226
+ setPendingTools([]);
227
+ return results;
228
+ },
229
+ [executeTool]
230
+ );
231
+
232
+ /**
233
+ * Send a message and get AI response
234
+ */
235
+ const sendMessage = useCallback(
236
+ async (userMessage) => {
237
+ if (!openaiClient) {
238
+ setError("OpenAI client not configured");
239
+ return;
240
+ }
241
+
242
+ if (isProcessingRef.current) {
243
+ return;
244
+ }
245
+
246
+ isProcessingRef.current = true;
247
+ setIsLoading(true);
248
+ setError(null);
249
+ setStatus(CHAT_STATUS.RECEIVED);
250
+ setExecutedTools([]);
251
+
252
+ // Add user message
253
+ const userMsg = createMessage("user", userMessage);
254
+ setMessages((prev) => [...prev, userMsg]);
255
+
256
+ try {
257
+ // Build conversation history
258
+ const conversationHistory = [
259
+ { role: "system", content: systemPrompt },
260
+ ...messages.map((msg) => ({
261
+ role: msg.role === "user" ? "user" : "assistant",
262
+ content: msg.content,
263
+ toolCalls: msg.toolCalls,
264
+ toolResults: msg.toolResults,
265
+ })),
266
+ { role: "user", content: userMessage },
267
+ ];
268
+
269
+ // Get tools in OpenAI format
270
+ const tools = mcpConnected && mcpClient ? mcpClient.getToolsForOpenAI() : [];
271
+
272
+ setStatus(CHAT_STATUS.GENERATING);
273
+
274
+ let response = null;
275
+ let allToolResults = [];
276
+
277
+ // Streaming completion
278
+ await openaiClient.createStreamingCompletion(
279
+ {
280
+ messages: openaiClient.convertMessagesToOpenAI(conversationHistory),
281
+ tools: tools.length > 0 ? tools : undefined,
282
+ tool_choice: tools.length > 0 ? "auto" : undefined,
283
+ },
284
+ // onChunk - streaming content updates could be added here
285
+ () => {},
286
+ // onComplete
287
+ async (fullMessage, toolCalls) => {
288
+ if (toolCalls && toolCalls.length > 0) {
289
+ setStatus(CHAT_STATUS.TOOL_CALL);
290
+
291
+ // Execute tool calls
292
+ const toolResults = await executeToolCalls(toolCalls);
293
+ allToolResults = toolResults;
294
+
295
+ // Continue conversation with tool results
296
+ setStatus(CHAT_STATUS.SUMMARIZING);
297
+
298
+ const followUpHistory = [
299
+ ...conversationHistory,
300
+ {
301
+ role: "assistant",
302
+ content: fullMessage || null,
303
+ toolCalls,
304
+ toolResults: toolResults.map((r) => ({
305
+ id: r.id,
306
+ result: r.result,
307
+ error: r.error,
308
+ })),
309
+ },
310
+ ];
311
+
312
+ // Get follow-up response
313
+ const followUp = await openaiClient.createChatCompletion({
314
+ messages: openaiClient.convertMessagesToOpenAI(followUpHistory),
315
+ });
316
+
317
+ response = followUp.choices?.[0]?.message?.content || "";
318
+ } else {
319
+ response = fullMessage;
320
+ }
321
+ },
322
+ // onError
323
+ (err) => {
324
+ throw err;
325
+ }
326
+ );
327
+
328
+ // Add assistant message
329
+ if (response) {
330
+ const assistantMsg = createMessage("assistant", response, {
331
+ toolCalls: allToolResults.length > 0 ? allToolResults : undefined,
332
+ executedTools: allToolResults.length > 0 ? allToolResults : undefined,
333
+ });
334
+ setMessages((prev) => [...prev, assistantMsg]);
335
+ onMessageComplete?.(assistantMsg);
336
+ }
337
+
338
+ setStatus(CHAT_STATUS.COMPLETED);
339
+ } catch (err) {
340
+ // eslint-disable-next-line no-console
341
+ console.error("Chat error:", err);
342
+ setError(err.message);
343
+ setStatus(CHAT_STATUS.FAILED);
344
+ onError?.(err);
345
+ } finally {
346
+ isProcessingRef.current = false;
347
+ setIsLoading(false);
348
+ setActiveToolCall(null);
349
+ setExecutedTools([]);
350
+ setPendingTools([]);
351
+ }
352
+ },
353
+ [
354
+ openaiClient,
355
+ mcpClient,
356
+ mcpConnected,
357
+ messages,
358
+ systemPrompt,
359
+ createMessage,
360
+ executeToolCalls,
361
+ onMessageComplete,
362
+ onError,
363
+ ]
364
+ );
365
+
366
+ /**
367
+ * Stop the current request
368
+ */
369
+ const stopRequest = useCallback(() => {
370
+ if (abortControllerRef.current) {
371
+ abortControllerRef.current.abort();
372
+ }
373
+ isProcessingRef.current = false;
374
+ setIsLoading(false);
375
+ setStatus(CHAT_STATUS.IDLE);
376
+ setActiveToolCall(null);
377
+ }, []);
378
+
379
+ /**
380
+ * Clear conversation history
381
+ */
382
+ const clearMessages = useCallback(() => {
383
+ setMessages([]);
384
+ setError(null);
385
+ setStatus(CHAT_STATUS.IDLE);
386
+ setSessionId(generateSessionId());
387
+ }, []);
388
+
389
+ /**
390
+ * Add a message programmatically
391
+ */
392
+ const addMessage = useCallback(
393
+ (role, content, extras = {}) => {
394
+ const msg = createMessage(role, content, extras);
395
+ setMessages((prev) => [...prev, msg]);
396
+ return msg;
397
+ },
398
+ [createMessage]
399
+ );
400
+
401
+ return {
402
+ // State
403
+ messages,
404
+ isLoading,
405
+ error,
406
+ status,
407
+ sessionId,
408
+
409
+ // Tool execution state
410
+ activeToolCall,
411
+ executedTools,
412
+ pendingTools,
413
+ toolProgress,
414
+
415
+ // MCP state
416
+ mcpConnected,
417
+ mcpTools,
418
+
419
+ // Actions
420
+ sendMessage,
421
+ stopRequest,
422
+ clearMessages,
423
+ addMessage,
424
+ initializeMCP,
425
+
426
+ // Setters for advanced usage
427
+ setError,
428
+ setToolProgress,
429
+ };
430
+ };
431
+
432
+ export default useAIChat;