@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,18 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { __ } from "@wordpress/i18n";
5
+
6
+ /**
7
+ * BluBetaHeading Component
8
+ *
9
+ * Single solid dark blue BETA badge for the chat header (matches screenshot and
10
+ * editor-chat: AILogo + "Blu Chat" plain text + this BETA pill).
11
+ *
12
+ * @return {JSX.Element} BETA badge span for the chat header.
13
+ */
14
+ const BluBetaHeading = () => (
15
+ <span className="nfd-ai-chat-blu-beta-badge">{__("BETA", "wp-module-ai-chat")}</span>
16
+ );
17
+
18
+ export default BluBetaHeading;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { CircleX } from "lucide-react";
5
+
6
+ /**
7
+ * ErrorAlert Component
8
+ *
9
+ * A reusable error alert component that displays error messages
10
+ * in a red box with an exclamation mark icon.
11
+ *
12
+ * @param {Object} props - The component props.
13
+ * @param {string} props.message - The error message to display.
14
+ * @param {string} props.className - Additional CSS classes (optional).
15
+ * @return {JSX.Element} The ErrorAlert component.
16
+ */
17
+ const ErrorAlert = ({ message, className = "" }) => {
18
+ return (
19
+ <div className={`nfd-ai-chat-error-alert ${className}`}>
20
+ <div className="nfd-ai-chat-error-alert__icon">
21
+ <CircleX width={16} height={16} />
22
+ </div>
23
+ <div className="nfd-ai-chat-error-alert__content">
24
+ <div className="nfd-ai-chat-error-alert__message">{message}</div>
25
+ </div>
26
+ </div>
27
+ );
28
+ };
29
+
30
+ export default ErrorAlert;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * HeaderBar Component
3
+ *
4
+ * Shared header bar layout with configurable title, badge, and action slots.
5
+ * Used by ChatHeader (help center / modal) and SidebarHeader (editor chat).
6
+ *
7
+ * @param {Object} props - Component props.
8
+ * @param {string|import('react').ReactNode} [props.title] - Title content (e.g. "Blu Chat").
9
+ * @param {import('react').ReactNode} [props.badge] - Badge node (e.g. BETA pill).
10
+ * @param {import('react').ReactNode} [props.logo] - Optional logo/icon left of title.
11
+ * @param {import('react').ReactNode} [props.leftActions] - Optional actions on the left side of the actions area.
12
+ * @param {import('react').ReactNode} [props.rightActions] - Actions on the right (e.g. New chat, Close).
13
+ * @param {string} [props.className] - Optional extra class for the root.
14
+ * @return {JSX.Element} Shared header bar with configurable title, badge, and action slots.
15
+ */
16
+ const HeaderBar = ({ title, badge, logo, leftActions, rightActions, className = "" }) => (
17
+ <div className={`nfd-ai-chat-header ${className}`.trim()} role="banner">
18
+ <div className="nfd-ai-chat-header__brand">
19
+ {logo}
20
+ {title !== undefined && title !== null && (
21
+ <span className="nfd-ai-chat-header__title">{title}</span>
22
+ )}
23
+ {badge}
24
+ </div>
25
+ {(leftActions || rightActions) && (
26
+ <div className="nfd-ai-chat-header__actions">
27
+ {leftActions}
28
+ {rightActions}
29
+ </div>
30
+ )}
31
+ </div>
32
+ );
33
+
34
+ export default HeaderBar;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { Button } from "@wordpress/components";
5
+
6
+ /**
7
+ * SuggestionButton Component
8
+ *
9
+ * A reusable suggestion button component that can be used in various contexts.
10
+ * Takes an icon, text, and onClick action as parameters.
11
+ *
12
+ * @param {Object} props - The component props.
13
+ * @param {JSX.Element} props.icon - The icon element to display.
14
+ * @param {string} props.text - The text to display.
15
+ * @param {Function} props.onClick - The function to call when clicked.
16
+ * @param {string} props.className - Additional CSS classes (optional).
17
+ * @return {JSX.Element} The SuggestionButton component.
18
+ */
19
+ const SuggestionButton = ({ icon, text, onClick, className = "" }) => {
20
+ return (
21
+ <Button className={`nfd-ai-chat-suggestion ${className}`} onClick={onClick}>
22
+ <div className="nfd-ai-chat-suggestion__icon">{icon}</div>
23
+ <div className="nfd-ai-chat-suggestion__text">{text}</div>
24
+ </Button>
25
+ );
26
+ };
27
+
28
+ export default SuggestionButton;
@@ -0,0 +1,264 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useState } from "@wordpress/element";
5
+ import { __ } from "@wordpress/i18n";
6
+
7
+ /**
8
+ * External dependencies
9
+ */
10
+ import { CheckCircle, ChevronDown, ChevronRight, XCircle } from "lucide-react";
11
+ import classnames from "classnames";
12
+
13
+ /**
14
+ * Internal dependencies
15
+ */
16
+ import { getToolDetails } from "../../utils/nfdAgents/typingIndicatorToolDisplay";
17
+
18
+ /**
19
+ * Safely convert a value to a string for display
20
+ *
21
+ * @param {*} value The value to convert
22
+ * @return {string|null} String representation or null
23
+ */
24
+ const safeString = (value) => {
25
+ if (value === null || value === undefined) {
26
+ return null;
27
+ }
28
+ if (typeof value === "string") {
29
+ return value;
30
+ }
31
+ if (typeof value === "number" || typeof value === "boolean") {
32
+ return String(value);
33
+ }
34
+ // Don't render objects - return null instead
35
+ return null;
36
+ };
37
+
38
+ /**
39
+ * Parse tool result to get a human-readable summary
40
+ *
41
+ * @param {Object} result The tool result object
42
+ * @param {string} toolName The tool name
43
+ * @return {string|null} Summary string or null
44
+ */
45
+ const getResultSummary = (result, toolName) => {
46
+ if (!result || result.isError) {
47
+ return safeString(result?.error);
48
+ }
49
+
50
+ try {
51
+ // Result is typically an array with { type: "text", text: "..." }
52
+ let data = result.result;
53
+ if (Array.isArray(data) && data.length > 0 && data[0].text) {
54
+ data = JSON.parse(data[0].text);
55
+ } else if (typeof data === "string") {
56
+ data = JSON.parse(data);
57
+ }
58
+
59
+ // If data is not an object at this point, we can't process it
60
+ if (!data || typeof data !== "object") {
61
+ return null;
62
+ }
63
+
64
+ // Handle update results
65
+ if (toolName?.includes("update")) {
66
+ if (data.updatedColors && Array.isArray(data.updatedColors)) {
67
+ const colors = data.updatedColors;
68
+ if (colors.length <= 3) {
69
+ return colors.map((c) => `${c.name || c.slug}: ${c.color}`).join(", ");
70
+ }
71
+ return `${colors.length} colors updated`;
72
+ }
73
+ if (data.message && typeof data.message === "string") {
74
+ return data.message;
75
+ }
76
+ }
77
+
78
+ // Handle block editor tool results
79
+ if (toolName?.includes("edit-block") || toolName?.includes("edit_block")) {
80
+ if (data.message && typeof data.message === "string") {
81
+ return data.message;
82
+ }
83
+ return data.success ? "Block updated" : "Update failed";
84
+ }
85
+
86
+ if (toolName?.includes("add-section") || toolName?.includes("add_section")) {
87
+ if (data.message && typeof data.message === "string") {
88
+ return data.message;
89
+ }
90
+ return data.success ? "Section added" : "Add failed";
91
+ }
92
+
93
+ if (toolName?.includes("delete-block") || toolName?.includes("delete_block")) {
94
+ if (data.message && typeof data.message === "string") {
95
+ return data.message;
96
+ }
97
+ return data.success ? "Block removed" : "Delete failed";
98
+ }
99
+
100
+ if (toolName?.includes("move-block") || toolName?.includes("move_block")) {
101
+ if (data.message && typeof data.message === "string") {
102
+ return data.message;
103
+ }
104
+ return data.success ? "Block moved" : "Move failed";
105
+ }
106
+
107
+ // Handle get/read results
108
+ if (toolName?.includes("get") || toolName?.includes("read")) {
109
+ // Check for palette data
110
+ if (data.color?.palette) {
111
+ const palette = data.color.palette;
112
+ const customCount = palette.custom?.length || 0;
113
+ const themeCount = palette.theme?.length || 0;
114
+ if (customCount || themeCount) {
115
+ return `Found ${customCount + themeCount} colors`;
116
+ }
117
+ }
118
+ // Check for typography
119
+ if (data.typography) {
120
+ const fontFamilies = data.typography.fontFamilies?.length || 0;
121
+ const fontSizes = data.typography.fontSizes?.length || 0;
122
+ const parts = [];
123
+ if (fontFamilies) {
124
+ parts.push(`${fontFamilies} font families`);
125
+ }
126
+ if (fontSizes) {
127
+ parts.push(`${fontSizes} sizes`);
128
+ }
129
+ if (parts.length) {
130
+ return parts.join(", ");
131
+ }
132
+ }
133
+ // Generic message - only if it's a string
134
+ if (data.message && typeof data.message === "string") {
135
+ return data.message;
136
+ }
137
+ }
138
+
139
+ // Fallback for styles ID
140
+ if (data.id && toolName?.includes("id")) {
141
+ return `ID: ${data.id}`;
142
+ }
143
+
144
+ return null;
145
+ } catch {
146
+ return null;
147
+ }
148
+ };
149
+
150
+ /**
151
+ * Single tool execution item
152
+ *
153
+ * @param {Object} props - The component props.
154
+ * @param {Object} props.tool - The tool object.
155
+ * @param {boolean} props.isError - Whether the tool had an error.
156
+ * @param {Object|null} props.result - The tool result.
157
+ * @return {JSX.Element} The item component.
158
+ */
159
+ const ToolExecutionItem = ({ tool, isError, result }) => {
160
+ const details = getToolDetails(tool.name, tool.arguments);
161
+ const summary = getResultSummary(result, tool.name);
162
+
163
+ return (
164
+ <div
165
+ className={classnames("nfd-ai-chat-tool-execution__item", {
166
+ "nfd-ai-chat-tool-execution__item--complete": !isError,
167
+ "nfd-ai-chat-tool-execution__item--error": isError,
168
+ })}
169
+ >
170
+ <div className="nfd-ai-chat-tool-execution__item-header">
171
+ {isError ? (
172
+ <XCircle
173
+ className="nfd-ai-chat-tool-execution__icon nfd-ai-chat-tool-execution__icon--error"
174
+ size={12}
175
+ />
176
+ ) : (
177
+ <CheckCircle
178
+ className="nfd-ai-chat-tool-execution__icon nfd-ai-chat-tool-execution__icon--success"
179
+ size={12}
180
+ />
181
+ )}
182
+ <span className="nfd-ai-chat-tool-execution__item-title">{details.title}</span>
183
+ {details.params && (
184
+ <span className="nfd-ai-chat-tool-execution__item-params">{details.params}</span>
185
+ )}
186
+ </div>
187
+ {summary && <div className="nfd-ai-chat-tool-execution__item-summary">{summary}</div>}
188
+ </div>
189
+ );
190
+ };
191
+
192
+ /**
193
+ * ToolExecutionList Component
194
+ *
195
+ * Displays a collapsible list of executed tools using the same styling
196
+ * as the typing indicator's tool execution view.
197
+ *
198
+ * @param {Object} props - The component props.
199
+ * @param {Array} props.executedTools - List of executed tools.
200
+ * @param {Array} props.toolResults - Results from tool executions.
201
+ * @return {JSX.Element} The ToolExecutionList component.
202
+ */
203
+ const ToolExecutionList = ({ executedTools = [], toolResults = [] }) => {
204
+ const [isExpanded, setIsExpanded] = useState(false);
205
+
206
+ if (!executedTools || executedTools.length === 0) {
207
+ return null;
208
+ }
209
+
210
+ // Create a map of results by tool ID for quick lookup
211
+ const resultsMap = new Map();
212
+ if (toolResults && Array.isArray(toolResults)) {
213
+ toolResults.forEach((result) => {
214
+ if (result.id) {
215
+ resultsMap.set(result.id, result);
216
+ }
217
+ });
218
+ }
219
+
220
+ const hasErrors = executedTools.some((tool) => tool.isError);
221
+ const totalTools = executedTools.length;
222
+
223
+ return (
224
+ <div
225
+ className={classnames("nfd-ai-chat-tool-execution", {
226
+ "nfd-ai-chat-tool-execution--collapsed": !isExpanded,
227
+ })}
228
+ >
229
+ <button
230
+ type="button"
231
+ className="nfd-ai-chat-tool-execution__header"
232
+ onClick={() => setIsExpanded(!isExpanded)}
233
+ aria-expanded={isExpanded ? "true" : "false"}
234
+ >
235
+ {isExpanded ? (
236
+ <ChevronDown className="nfd-ai-chat-tool-execution__chevron" size={12} />
237
+ ) : (
238
+ <ChevronRight className="nfd-ai-chat-tool-execution__chevron" size={12} />
239
+ )}
240
+ <span>
241
+ {hasErrors
242
+ ? __("Some actions failed", "wp-module-ai-chat")
243
+ : __("Actions completed", "wp-module-ai-chat")}
244
+ </span>
245
+ <span className="nfd-ai-chat-tool-execution__header-count">({totalTools})</span>
246
+ </button>
247
+
248
+ {isExpanded && (
249
+ <div className="nfd-ai-chat-tool-execution__list">
250
+ {executedTools.map((tool, index) => (
251
+ <ToolExecutionItem
252
+ key={tool.id || `tool-${index}`}
253
+ tool={tool}
254
+ isError={tool.isError}
255
+ result={resultsMap.get(tool.id)}
256
+ />
257
+ ))}
258
+ </div>
259
+ )}
260
+ </div>
261
+ );
262
+ };
263
+
264
+ export default ToolExecutionList;
@@ -0,0 +1,268 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useState, useEffect } from "@wordpress/element";
5
+ import { __ } from "@wordpress/i18n";
6
+ import { TYPING_STATUS } from "../../constants/nfdAgents/typingStatus";
7
+
8
+ /**
9
+ * Internal dependencies
10
+ */
11
+ import { getToolDetails } from "../../utils/nfdAgents/typingIndicatorToolDisplay";
12
+
13
+ /**
14
+ * External dependencies
15
+ */
16
+ import { Loader2, CheckCircle, XCircle, Sparkles, ChevronDown, ChevronRight } from "lucide-react";
17
+ import classnames from "classnames";
18
+
19
+ /** Status key → user-facing label for the simple typing state (single place for copy; i18n-ready). */
20
+ const STATUS_LABELS = {
21
+ [TYPING_STATUS.PROCESSING]: __("Processing…", "wp-module-ai-chat"),
22
+ [TYPING_STATUS.CONNECTING]: __("Getting your site ready…", "wp-module-ai-chat"),
23
+ [TYPING_STATUS.WS_CONNECTING]: __("Connecting…", "wp-module-ai-chat"),
24
+ [TYPING_STATUS.TOOL_CALL]: __("Looking this up…", "wp-module-ai-chat"),
25
+ [TYPING_STATUS.WORKING]: __("Almost there…", "wp-module-ai-chat"),
26
+ [TYPING_STATUS.RECEIVED]: __("Message received", "wp-module-ai-chat"),
27
+ [TYPING_STATUS.GENERATING]: __("Thinking…", "wp-module-ai-chat"),
28
+ [TYPING_STATUS.SUMMARIZING]: __("Summarizing results", "wp-module-ai-chat"),
29
+ [TYPING_STATUS.COMPLETED]: __("Processing", "wp-module-ai-chat"),
30
+ [TYPING_STATUS.FAILED]: __("Error occurred", "wp-module-ai-chat"),
31
+ };
32
+
33
+ /**
34
+ * Single tool execution item in the list
35
+ *
36
+ * @param {Object} props - The component props.
37
+ * @param {Object} props.tool - The tool object with name and arguments.
38
+ * @param {boolean} props.isActive - Whether the tool is active.
39
+ * @param {string} props.progress - The progress message.
40
+ * @param {boolean} props.isComplete - Whether the tool is complete.
41
+ * @param {boolean} props.isError - Whether the tool is in error.
42
+ * @return {JSX.Element} The ToolExecutionItem component.
43
+ */
44
+ const ToolExecutionItem = ({ tool, isActive, progress, isComplete, isError }) => {
45
+ const details = getToolDetails(tool.name, tool.arguments);
46
+
47
+ const getIcon = () => {
48
+ if (isError) {
49
+ return (
50
+ <XCircle
51
+ className="nfd-ai-chat-tool-execution__icon nfd-ai-chat-tool-execution__icon--error"
52
+ size={12}
53
+ />
54
+ );
55
+ }
56
+ if (isComplete) {
57
+ return (
58
+ <CheckCircle
59
+ className="nfd-ai-chat-tool-execution__icon nfd-ai-chat-tool-execution__icon--success"
60
+ size={12}
61
+ />
62
+ );
63
+ }
64
+ if (isActive) {
65
+ return (
66
+ <Loader2
67
+ className="nfd-ai-chat-tool-execution__icon nfd-ai-chat-tool-execution__icon--active"
68
+ size={12}
69
+ />
70
+ );
71
+ }
72
+ return (
73
+ <Sparkles
74
+ className="nfd-ai-chat-tool-execution__icon nfd-ai-chat-tool-execution__icon--pending"
75
+ size={12}
76
+ />
77
+ );
78
+ };
79
+
80
+ return (
81
+ <div
82
+ className={classnames("nfd-ai-chat-tool-execution__item", {
83
+ "nfd-ai-chat-tool-execution__item--active": isActive,
84
+ "nfd-ai-chat-tool-execution__item--complete": isComplete,
85
+ "nfd-ai-chat-tool-execution__item--error": isError,
86
+ })}
87
+ >
88
+ <div className="nfd-ai-chat-tool-execution__item-header">
89
+ {getIcon()}
90
+ <span className="nfd-ai-chat-tool-execution__item-title">{details.title}</span>
91
+ {details.params && (
92
+ <span className="nfd-ai-chat-tool-execution__item-params">{details.params}</span>
93
+ )}
94
+ </div>
95
+ {isActive && progress && (
96
+ <div className="nfd-ai-chat-tool-execution__item-progress">{progress}</div>
97
+ )}
98
+ </div>
99
+ );
100
+ };
101
+
102
+ /**
103
+ * TypingIndicator Component
104
+ *
105
+ * Displays an animated typing indicator with spinner and real-time progress.
106
+ *
107
+ * @param {Object} props - The component props.
108
+ * @param {string} props.status - The current status.
109
+ * @param {Object} props.activeToolCall - The currently executing tool call.
110
+ * @param {string} props.toolProgress - Real-time progress message.
111
+ * @param {Array} props.executedTools - List of already executed tools.
112
+ * @param {Array} props.pendingTools - List of pending tools to execute.
113
+ * @return {JSX.Element} The TypingIndicator component.
114
+ */
115
+ const TypingIndicator = ({
116
+ status = null,
117
+ activeToolCall = null,
118
+ toolProgress = null,
119
+ executedTools = [],
120
+ pendingTools = [],
121
+ }) => {
122
+ const [isExpanded, setIsExpanded] = useState(true);
123
+ const isExecuting = !!activeToolCall;
124
+ // Show "summarizing" state when waiting between tool batch and final response.
125
+ const isBetweenBatches =
126
+ !isExecuting && status === TYPING_STATUS.SUMMARIZING && executedTools.length > 0;
127
+
128
+ useEffect(() => {
129
+ if (isExecuting || isBetweenBatches) {
130
+ setIsExpanded(true);
131
+ }
132
+ }, [isExecuting, isBetweenBatches]);
133
+
134
+ const getStatusText = () => {
135
+ return STATUS_LABELS[status] ?? __("Thinking…", "wp-module-ai-chat");
136
+ };
137
+
138
+ // Show expandable tool list when any tools are active, done, or queued.
139
+ const hasToolActivity = activeToolCall || executedTools.length > 0 || pendingTools.length > 0;
140
+ const totalTools = executedTools.length + (activeToolCall ? 1 : 0) + pendingTools.length;
141
+
142
+ const renderHeaderLabel = () => {
143
+ if (isExecuting) {
144
+ return (
145
+ <>
146
+ <span>{__("Executing actions", "wp-module-ai-chat")}</span>
147
+ {activeToolCall.total > 1 && (
148
+ <span className="nfd-ai-chat-tool-execution__header-count">
149
+ ({activeToolCall.index}/{activeToolCall.total})
150
+ </span>
151
+ )}
152
+ </>
153
+ );
154
+ }
155
+ if (isBetweenBatches) {
156
+ return (
157
+ <>
158
+ <Loader2
159
+ className="nfd-ai-chat-tool-execution__icon nfd-ai-chat-tool-execution__icon--active"
160
+ size={12}
161
+ />
162
+ <span>{__("Processing", "wp-module-ai-chat")}</span>
163
+ <span className="nfd-ai-chat-tool-execution__header-count">({executedTools.length})</span>
164
+ </>
165
+ );
166
+ }
167
+ return (
168
+ <>
169
+ <span>{__("Actions completed", "wp-module-ai-chat")}</span>
170
+ <span className="nfd-ai-chat-tool-execution__header-count">({totalTools})</span>
171
+ </>
172
+ );
173
+ };
174
+
175
+ if (hasToolActivity) {
176
+ return (
177
+ <div className="nfd-ai-chat-message nfd-ai-chat-message--assistant">
178
+ <div className="nfd-ai-chat-message__content">
179
+ <div
180
+ className={classnames("nfd-ai-chat-tool-execution", {
181
+ "nfd-ai-chat-tool-execution--collapsed": !isExpanded,
182
+ })}
183
+ >
184
+ <button
185
+ type="button"
186
+ className="nfd-ai-chat-tool-execution__header"
187
+ onClick={() => setIsExpanded(!isExpanded)}
188
+ aria-expanded={isExpanded ? "true" : "false"}
189
+ >
190
+ {isExpanded ? (
191
+ <ChevronDown className="nfd-ai-chat-tool-execution__chevron" size={12} />
192
+ ) : (
193
+ <ChevronRight className="nfd-ai-chat-tool-execution__chevron" size={12} />
194
+ )}
195
+
196
+ {renderHeaderLabel()}
197
+ </button>
198
+
199
+ {isExpanded && (
200
+ <div className="nfd-ai-chat-tool-execution__list">
201
+ {executedTools.map((tool, index) => (
202
+ <ToolExecutionItem
203
+ key={tool.id || `executed-${index}`}
204
+ tool={tool}
205
+ isActive={false}
206
+ isComplete={!tool.isError}
207
+ isError={tool.isError}
208
+ progress={null}
209
+ />
210
+ ))}
211
+
212
+ {activeToolCall && (
213
+ <ToolExecutionItem
214
+ key={activeToolCall.id || "active"}
215
+ tool={activeToolCall}
216
+ isActive={true}
217
+ isComplete={false}
218
+ isError={false}
219
+ progress={toolProgress}
220
+ />
221
+ )}
222
+
223
+ {isBetweenBatches && (
224
+ <ToolExecutionItem
225
+ key="preparing"
226
+ tool={{ name: "preparing-changes" }}
227
+ isActive={true}
228
+ isComplete={false}
229
+ isError={false}
230
+ progress={null}
231
+ />
232
+ )}
233
+
234
+ {pendingTools.map((tool, index) => (
235
+ <ToolExecutionItem
236
+ key={tool.id || `pending-${index}`}
237
+ tool={tool}
238
+ isActive={false}
239
+ isComplete={false}
240
+ isError={false}
241
+ progress={null}
242
+ />
243
+ ))}
244
+ </div>
245
+ )}
246
+ </div>
247
+ </div>
248
+ </div>
249
+ );
250
+ }
251
+
252
+ return (
253
+ <div className="nfd-ai-chat-message nfd-ai-chat-message--assistant">
254
+ <div className="nfd-ai-chat-message__content">
255
+ <div className="nfd-ai-chat-typing-indicator">
256
+ <span className="nfd-ai-chat-typing-indicator__dots" aria-hidden="true">
257
+ <span></span>
258
+ <span></span>
259
+ <span></span>
260
+ </span>
261
+ <span className="nfd-ai-chat-typing-indicator__text">{getStatusText()}</span>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ );
266
+ };
267
+
268
+ export default TypingIndicator;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * NFD Agents chat input constants.
3
+ *
4
+ * Central place for textarea and input behavior used by the chat UI (e.g. ChatInput).
5
+ * Add timeouts and dimension limits here to avoid magic numbers in components.
6
+ */
7
+
8
+ /** Chat input configuration: dimensions, focus delay, and debounce timings. */
9
+ export const INPUT = {
10
+ MAX_HEIGHT: 200, // Textarea max height (px) before scrolling
11
+ FOCUS_DELAY: 100, // Delay before focusing input after mount or panel open (ms)
12
+ STOP_DEBOUNCE: 500, // Debounce for stop-generation button to avoid double-firing (ms)
13
+ };