@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.
- package/README.md +98 -0
- package/package.json +51 -0
- package/src/components/chat/ChatHeader.jsx +63 -0
- package/src/components/chat/ChatHistoryDropdown.jsx +182 -0
- package/src/components/chat/ChatHistoryList.jsx +257 -0
- package/src/components/chat/ChatInput.jsx +157 -0
- package/src/components/chat/ChatMessage.jsx +157 -0
- package/src/components/chat/ChatMessages.jsx +137 -0
- package/src/components/chat/WelcomeScreen.jsx +115 -0
- package/src/components/icons/CloseIcon.jsx +27 -0
- package/src/components/icons/SparklesOutlineIcon.jsx +30 -0
- package/src/components/icons/index.js +5 -0
- package/src/components/ui/AILogo.jsx +47 -0
- package/src/components/ui/BluBetaHeading.jsx +18 -0
- package/src/components/ui/ErrorAlert.jsx +30 -0
- package/src/components/ui/HeaderBar.jsx +34 -0
- package/src/components/ui/SuggestionButton.jsx +28 -0
- package/src/components/ui/ToolExecutionList.jsx +264 -0
- package/src/components/ui/TypingIndicator.jsx +268 -0
- package/src/constants/nfdAgents/input.js +13 -0
- package/src/constants/nfdAgents/storageKeys.js +102 -0
- package/src/constants/nfdAgents/typingStatus.js +40 -0
- package/src/constants/nfdAgents/websocket.js +44 -0
- package/src/hooks/useAIChat.js +432 -0
- package/src/hooks/useNfdAgentsWebSocket.js +964 -0
- package/src/index.js +66 -0
- package/src/services/mcpClient.js +433 -0
- package/src/services/openaiClient.js +416 -0
- package/src/styles/_branding.scss +151 -0
- package/src/styles/_history.scss +180 -0
- package/src/styles/_input.scss +170 -0
- package/src/styles/_messages.scss +272 -0
- package/src/styles/_mixins.scss +21 -0
- package/src/styles/_typing-indicator.scss +162 -0
- package/src/styles/_ui.scss +173 -0
- package/src/styles/_vars.scss +103 -0
- package/src/styles/_welcome.scss +81 -0
- package/src/styles/app.scss +10 -0
- package/src/utils/helpers.js +75 -0
- package/src/utils/markdownParser.js +319 -0
- package/src/utils/nfdAgents/archiveConversation.js +82 -0
- package/src/utils/nfdAgents/chatHistoryList.js +130 -0
- package/src/utils/nfdAgents/configFetcher.js +137 -0
- package/src/utils/nfdAgents/greeting.js +55 -0
- package/src/utils/nfdAgents/jwtUtils.js +59 -0
- package/src/utils/nfdAgents/messageHandler.js +328 -0
- package/src/utils/nfdAgents/storage.js +112 -0
- package/src/utils/nfdAgents/typingIndicatorToolDisplay.js +180 -0
- package/src/utils/nfdAgents/url.js +101 -0
- package/src/utils/restApi.js +87 -0
- 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
|
+
};
|