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