@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.
Files changed (54) hide show
  1. package/build/index-rtl.css +1 -0
  2. package/build/index.asset.php +1 -0
  3. package/build/index.css +1 -0
  4. package/build/index.js +8 -0
  5. package/package.json +11 -9
  6. package/src/components/chat/ChatHeader.jsx +0 -63
  7. package/src/components/chat/ChatHistoryDropdown.jsx +0 -182
  8. package/src/components/chat/ChatHistoryList.jsx +0 -257
  9. package/src/components/chat/ChatInput.jsx +0 -157
  10. package/src/components/chat/ChatMessage.jsx +0 -157
  11. package/src/components/chat/ChatMessages.jsx +0 -137
  12. package/src/components/chat/WelcomeScreen.jsx +0 -115
  13. package/src/components/icons/CloseIcon.jsx +0 -27
  14. package/src/components/icons/SparklesOutlineIcon.jsx +0 -30
  15. package/src/components/icons/index.js +0 -5
  16. package/src/components/ui/AILogo.jsx +0 -47
  17. package/src/components/ui/BluBetaHeading.jsx +0 -18
  18. package/src/components/ui/ErrorAlert.jsx +0 -30
  19. package/src/components/ui/HeaderBar.jsx +0 -34
  20. package/src/components/ui/SuggestionButton.jsx +0 -28
  21. package/src/components/ui/ToolExecutionList.jsx +0 -264
  22. package/src/components/ui/TypingIndicator.jsx +0 -268
  23. package/src/constants/nfdAgents/input.js +0 -13
  24. package/src/constants/nfdAgents/storageKeys.js +0 -102
  25. package/src/constants/nfdAgents/typingStatus.js +0 -40
  26. package/src/constants/nfdAgents/websocket.js +0 -44
  27. package/src/hooks/useAIChat.js +0 -432
  28. package/src/hooks/useNfdAgentsWebSocket.js +0 -964
  29. package/src/index.js +0 -66
  30. package/src/services/mcpClient.js +0 -433
  31. package/src/services/openaiClient.js +0 -416
  32. package/src/styles/_branding.scss +0 -151
  33. package/src/styles/_history.scss +0 -180
  34. package/src/styles/_input.scss +0 -170
  35. package/src/styles/_messages.scss +0 -272
  36. package/src/styles/_mixins.scss +0 -21
  37. package/src/styles/_typing-indicator.scss +0 -162
  38. package/src/styles/_ui.scss +0 -173
  39. package/src/styles/_vars.scss +0 -103
  40. package/src/styles/_welcome.scss +0 -81
  41. package/src/styles/app.scss +0 -10
  42. package/src/utils/helpers.js +0 -75
  43. package/src/utils/markdownParser.js +0 -319
  44. package/src/utils/nfdAgents/archiveConversation.js +0 -82
  45. package/src/utils/nfdAgents/chatHistoryList.js +0 -130
  46. package/src/utils/nfdAgents/configFetcher.js +0 -137
  47. package/src/utils/nfdAgents/greeting.js +0 -55
  48. package/src/utils/nfdAgents/jwtUtils.js +0 -59
  49. package/src/utils/nfdAgents/messageHandler.js +0 -328
  50. package/src/utils/nfdAgents/storage.js +0 -112
  51. package/src/utils/nfdAgents/typingIndicatorToolDisplay.js +0 -180
  52. package/src/utils/nfdAgents/url.js +0 -101
  53. package/src/utils/restApi.js +0 -87
  54. 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
- }