@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
package/src/index.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * AI Chat Module - Main Entry Point
3
+ *
4
+ * This module provides reusable AI chat functionality for WordPress.
5
+ * Use this as a foundation for editor chat, help center chat, and other AI interfaces.
6
+ */
7
+
8
+ import "./styles/app.scss";
9
+
10
+ // Services
11
+ export { WordPressMCPClient, createMCPClient, mcpClient, MCPError } from "./services/mcpClient";
12
+
13
+ export {
14
+ CloudflareOpenAIClient,
15
+ createOpenAIClient,
16
+ openaiClient,
17
+ OpenAIError,
18
+ } from "./services/openaiClient";
19
+
20
+ // Hooks
21
+ export { useAIChat, CHAT_STATUS } from "./hooks/useAIChat";
22
+ export { default as useNfdAgentsWebSocket } from "./hooks/useNfdAgentsWebSocket";
23
+
24
+ // Utils
25
+ export { simpleHash, generateSessionId, debounce } from "./utils/helpers";
26
+ export { containsMarkdown, parseMarkdown } from "./utils/markdownParser";
27
+ export { sanitizeHtml, containsHtml } from "./utils/sanitizeHtml";
28
+
29
+ // NFD Agents Utilities
30
+ export {
31
+ convertToWebSocketUrl,
32
+ normalizeUrl,
33
+ isLocalhost,
34
+ buildWebSocketUrl,
35
+ } from "./utils/nfdAgents/url";
36
+ export { isInitialGreeting } from "./utils/nfdAgents/greeting";
37
+
38
+ // Constants
39
+ export { NFD_AGENTS_WEBSOCKET } from "./constants/nfdAgents/websocket";
40
+ export { getChatHistoryStorageKeys } from "./constants/nfdAgents/storageKeys";
41
+ export { TYPING_STATUS } from "./constants/nfdAgents/typingStatus";
42
+ export { INPUT } from "./constants/nfdAgents/input";
43
+
44
+ // Chat Components
45
+ export { default as ChatMessage } from "./components/chat/ChatMessage";
46
+ export { default as ChatMessages } from "./components/chat/ChatMessages";
47
+ export { default as ChatInput } from "./components/chat/ChatInput";
48
+ export { default as ChatHeader } from "./components/chat/ChatHeader";
49
+ export { default as WelcomeScreen } from "./components/chat/WelcomeScreen";
50
+
51
+ // Chat history (consumer must match useNfdAgentsWebSocket for same consumer)
52
+ export {
53
+ archiveConversation,
54
+ removeConversationFromArchive,
55
+ } from "./utils/nfdAgents/archiveConversation";
56
+ export { default as ChatHistoryList } from "./components/chat/ChatHistoryList";
57
+ export { default as ChatHistoryDropdown } from "./components/chat/ChatHistoryDropdown";
58
+
59
+ // UI Components
60
+ export { default as AILogo } from "./components/ui/AILogo";
61
+ export { default as BluBetaHeading } from "./components/ui/BluBetaHeading";
62
+ export { default as HeaderBar } from "./components/ui/HeaderBar";
63
+ export { default as ErrorAlert } from "./components/ui/ErrorAlert";
64
+ export { default as SuggestionButton } from "./components/ui/SuggestionButton";
65
+ export { default as ToolExecutionList } from "./components/ui/ToolExecutionList";
66
+ export { default as TypingIndicator } from "./components/ui/TypingIndicator";
@@ -0,0 +1,433 @@
1
+ /**
2
+ * WordPress MCP Client
3
+ *
4
+ * MCP client implementation using the official TypeScript SDK
5
+ * for WordPress integration with nonce-based authentication.
6
+ * Configurable for use across different modules.
7
+ */
8
+
9
+ /* eslint-disable no-undef, no-console */
10
+
11
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
12
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
13
+
14
+ /**
15
+ * Custom error class for MCP operations
16
+ */
17
+ export class MCPError extends Error {
18
+ constructor(message, code = null, details = null) {
19
+ super(message);
20
+ this.name = "MCPError";
21
+ this.code = code;
22
+ this.details = details;
23
+ }
24
+ }
25
+
26
+ /**
27
+ * WordPress MCP Client class
28
+ *
29
+ * @param {Object} options Configuration options
30
+ * @param {string} options.configKey - Window config object name (default: 'nfdAIChat')
31
+ */
32
+ export class WordPressMCPClient {
33
+ constructor(options = {}) {
34
+ this.configKey = options.configKey || "nfdAIChat";
35
+ this.client = null;
36
+ this.transport = null;
37
+ this.connected = false;
38
+ this.tools = [];
39
+ this.resources = [];
40
+ this.eventListeners = new Map();
41
+ }
42
+
43
+ /**
44
+ * Get WordPress configuration from global variable
45
+ *
46
+ * @return {Object} WordPress config
47
+ */
48
+ getConfig() {
49
+ const config = (typeof window !== "undefined" && window[this.configKey]) || {};
50
+ return {
51
+ nonce: config.nonce || "",
52
+ restUrl: config.restUrl || "/wp-json/",
53
+ mcpUrl: config.mcpUrl || `${config.restUrl || "/wp-json/"}mcp/mcp-adapter-default-server`,
54
+ homeUrl: config.homeUrl || "",
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Add event listener
60
+ *
61
+ * @param {string} event Event name
62
+ * @param {Function} listener Callback function
63
+ */
64
+ on(event, listener) {
65
+ if (!this.eventListeners.has(event)) {
66
+ this.eventListeners.set(event, new Set());
67
+ }
68
+ this.eventListeners.get(event).add(listener);
69
+ }
70
+
71
+ /**
72
+ * Remove event listener
73
+ *
74
+ * @param {string} event Event name
75
+ * @param {Function} listener Callback function
76
+ */
77
+ off(event, listener) {
78
+ const listeners = this.eventListeners.get(event);
79
+ if (listeners) {
80
+ listeners.delete(listener);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Emit event to listeners
86
+ *
87
+ * @param {Object} event Event object with type and optional data
88
+ */
89
+ emit(event) {
90
+ const listeners = this.eventListeners.get(event.type);
91
+ if (listeners) {
92
+ listeners.forEach((listener) => {
93
+ try {
94
+ listener(event);
95
+ } catch (error) {
96
+ console.error("Error in MCP event listener:", error);
97
+ }
98
+ });
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Connect to the MCP server
104
+ *
105
+ * @param {string} serverUrl Optional server URL override
106
+ * @return {Promise<void>}
107
+ */
108
+ async connect(serverUrl = null) {
109
+ try {
110
+ const config = this.getConfig();
111
+ const mcpEndpoint = serverUrl || config.mcpUrl;
112
+
113
+ if (!mcpEndpoint) {
114
+ throw new MCPError("MCP endpoint URL not configured");
115
+ }
116
+
117
+ // Initialize the MCP Client
118
+ this.client = new Client(
119
+ {
120
+ name: "nfd-ai-chat-client",
121
+ version: "1.0.0",
122
+ },
123
+ {
124
+ capabilities: {},
125
+ }
126
+ );
127
+
128
+ // Create HTTP transport with WordPress authentication headers
129
+ this.transport = new StreamableHTTPClientTransport(new URL(mcpEndpoint), {
130
+ requestInit: {
131
+ headers: {
132
+ "X-WP-Nonce": config.nonce,
133
+ "Content-Type": "application/json",
134
+ },
135
+ },
136
+ });
137
+
138
+ // Connect using the SDK
139
+ await this.client.connect(this.transport);
140
+
141
+ this.connected = true;
142
+ this.emit({ type: "connected" });
143
+ } catch (error) {
144
+ const mcpError =
145
+ error instanceof MCPError ? error : new MCPError(`Connection failed: ${error.message}`);
146
+ this.emit({ type: "error", data: mcpError });
147
+ throw mcpError;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Initialize the MCP session and load tools/resources
153
+ *
154
+ * @return {Promise<Object>} Initialization result
155
+ */
156
+ async initialize() {
157
+ if (!this.connected) {
158
+ throw new MCPError("Not connected to MCP server");
159
+ }
160
+
161
+ try {
162
+ // Load tools and resources
163
+ await Promise.all([this.loadTools(), this.loadResources()]);
164
+
165
+ const initResult = {
166
+ protocolVersion: "2025-06-18",
167
+ capabilities: {
168
+ tools: {},
169
+ resources: {},
170
+ prompts: {},
171
+ },
172
+ serverInfo: {
173
+ name: "WordPress MCP Server",
174
+ version: "1.0.0",
175
+ },
176
+ };
177
+
178
+ this.emit({ type: "initialized", data: initResult });
179
+
180
+ return initResult;
181
+ } catch (error) {
182
+ const mcpError =
183
+ error instanceof MCPError ? error : new MCPError(`Initialization failed: ${error.message}`);
184
+ this.emit({ type: "error", data: mcpError });
185
+ throw mcpError;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Normalize input schema to valid JSON Schema object
191
+ *
192
+ * @param {any} schema Raw input schema from MCP
193
+ * @return {Object} Valid JSON Schema object
194
+ */
195
+ normalizeInputSchema(schema) {
196
+ if (
197
+ !schema ||
198
+ Array.isArray(schema) ||
199
+ typeof schema !== "object" ||
200
+ Object.keys(schema).length === 0
201
+ ) {
202
+ return {
203
+ type: "object",
204
+ properties: {},
205
+ required: [],
206
+ };
207
+ }
208
+
209
+ return {
210
+ type: schema.type || "object",
211
+ properties: schema.properties || {},
212
+ required: Array.isArray(schema.required) ? schema.required : [],
213
+ };
214
+ }
215
+
216
+ /**
217
+ * Load available tools from the MCP server
218
+ *
219
+ * @return {Promise<void>}
220
+ */
221
+ async loadTools() {
222
+ try {
223
+ const result = await this.client.listTools();
224
+
225
+ this.tools = result.tools.map((tool) => ({
226
+ name: tool.name,
227
+ description: tool.description || "",
228
+ inputSchema: this.normalizeInputSchema(tool.inputSchema),
229
+ annotations: tool.annotations || {},
230
+ }));
231
+
232
+ this.emit({ type: "tools_updated", data: this.tools });
233
+ } catch (error) {
234
+ console.error("Failed to load tools via SDK:", error);
235
+ this.tools = [];
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Load available resources from the MCP server
241
+ *
242
+ * @return {Promise<void>}
243
+ */
244
+ async loadResources() {
245
+ try {
246
+ const result = await this.client.listResources();
247
+
248
+ this.resources = result.resources.map((resource) => ({
249
+ uri: resource.uri,
250
+ name: resource.name || "",
251
+ description: resource.description,
252
+ mimeType: resource.mimeType,
253
+ }));
254
+
255
+ this.emit({ type: "resources_updated", data: this.resources });
256
+ } catch (error) {
257
+ console.error("Failed to load resources via SDK:", error);
258
+ this.resources = [];
259
+ }
260
+ }
261
+
262
+ /**
263
+ * List available tools
264
+ *
265
+ * @return {Promise<Array>} List of tools
266
+ */
267
+ async listTools() {
268
+ if (!this.connected) {
269
+ throw new MCPError("Not connected to MCP server");
270
+ }
271
+ return this.tools;
272
+ }
273
+
274
+ /**
275
+ * Call a tool on the MCP server
276
+ *
277
+ * @param {string} name Tool name
278
+ * @param {Object} args Tool arguments
279
+ * @return {Promise<Object>} Tool result
280
+ */
281
+ async callTool(name, args = {}) {
282
+ if (!this.connected) {
283
+ throw new MCPError("Not connected to MCP server");
284
+ }
285
+
286
+ try {
287
+ const result = await this.client.callTool({ name, arguments: args });
288
+
289
+ return {
290
+ content: Array.isArray(result.content) ? result.content : [],
291
+ isError: Boolean(result.isError),
292
+ meta: result.meta || {},
293
+ };
294
+ } catch (error) {
295
+ console.error(`Tool "${name}" call failed:`, error);
296
+ const mcpError =
297
+ error instanceof MCPError ? error : new MCPError(`Tool call failed: ${error.message}`);
298
+ this.emit({ type: "error", data: mcpError });
299
+ throw mcpError;
300
+ }
301
+ }
302
+
303
+ /**
304
+ * List available resources
305
+ *
306
+ * @return {Promise<Array>} List of resources
307
+ */
308
+ async listResources() {
309
+ if (!this.connected) {
310
+ throw new MCPError("Not connected to MCP server");
311
+ }
312
+ return this.resources;
313
+ }
314
+
315
+ /**
316
+ * Read a resource from the MCP server
317
+ *
318
+ * @param {string} uri Resource URI
319
+ * @return {Promise<Object>} Resource content
320
+ */
321
+ async readResource(uri) {
322
+ if (!this.connected) {
323
+ throw new MCPError("Not connected to MCP server");
324
+ }
325
+
326
+ try {
327
+ return await this.client.readResource({ uri });
328
+ } catch (error) {
329
+ console.error(`Resource "${uri}" read failed:`, error);
330
+ const mcpError =
331
+ error instanceof MCPError ? error : new MCPError(`Resource read failed: ${error.message}`);
332
+ this.emit({ type: "error", data: mcpError });
333
+ throw mcpError;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Disconnect from the MCP server
339
+ *
340
+ * @return {Promise<void>}
341
+ */
342
+ async disconnect() {
343
+ try {
344
+ if (this.transport) {
345
+ await this.client.close();
346
+ this.transport = null;
347
+ }
348
+
349
+ this.connected = false;
350
+ this.tools = [];
351
+ this.resources = [];
352
+ this.emit({ type: "disconnected" });
353
+ } catch (error) {
354
+ console.error("Error during SDK disconnect:", error);
355
+ }
356
+ }
357
+
358
+ /**
359
+ * Check if connected to MCP server
360
+ *
361
+ * @return {boolean} Connection status
362
+ */
363
+ isConnected() {
364
+ return this.connected;
365
+ }
366
+
367
+ /**
368
+ * Get cached tools
369
+ *
370
+ * @return {Array} List of tools
371
+ */
372
+ getTools() {
373
+ return [...this.tools];
374
+ }
375
+
376
+ /**
377
+ * Get cached resources
378
+ *
379
+ * @return {Array} List of resources
380
+ */
381
+ getResources() {
382
+ return [...this.resources];
383
+ }
384
+
385
+ /**
386
+ * Check if a tool is read-only
387
+ *
388
+ * @param {string} toolName Tool name to check
389
+ * @return {boolean} True if read-only
390
+ */
391
+ isToolReadOnly(toolName) {
392
+ const tool = this.tools.find((t) => t.name === toolName);
393
+ if (!tool) {
394
+ return true;
395
+ }
396
+ return tool.annotations?.readonly === true || tool.annotations?.readOnlyHint === true;
397
+ }
398
+
399
+ /**
400
+ * Convert all tools to OpenAI functions format
401
+ *
402
+ * @return {Array} OpenAI tools array
403
+ */
404
+ getToolsForOpenAI() {
405
+ return this.tools.map((tool) => {
406
+ const parameters = this.normalizeInputSchema(tool.inputSchema);
407
+
408
+ return {
409
+ type: "function",
410
+ function: {
411
+ name: tool.name,
412
+ description: tool.description || "",
413
+ parameters,
414
+ },
415
+ };
416
+ });
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Create a new MCP client instance
422
+ *
423
+ * @param {Object} options Configuration options
424
+ * @return {WordPressMCPClient} New client instance
425
+ */
426
+ export const createMCPClient = (options = {}) => {
427
+ return new WordPressMCPClient(options);
428
+ };
429
+
430
+ // Default singleton instance for backwards compatibility
431
+ export const mcpClient = new WordPressMCPClient();
432
+
433
+ export default mcpClient;