@courseecho/ai-widget-react 1.0.26 → 1.0.28
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/dist/components.js +20 -4
- package/dist/hooks.d.ts +2 -2
- package/dist/hooks.js +6 -3
- package/package.json +1 -1
package/dist/components.js
CHANGED
|
@@ -4,7 +4,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
* Usage: <AiChatWidget config={...} apiKey="..." />
|
|
5
5
|
*/
|
|
6
6
|
import { useRef, useEffect, useState, useCallback } from 'react';
|
|
7
|
-
import { ICONS } from '@courseecho/ai-core-sdk';
|
|
8
7
|
import { useAiWidget } from './hooks';
|
|
9
8
|
import { ShadowWrapper } from './ShadowWrapper';
|
|
10
9
|
// Chat sound — Web Audio API (no external files)
|
|
@@ -135,6 +134,19 @@ const DEFAULT_SUGGESTIONS = [
|
|
|
135
134
|
{ id: 'd5', text: 'What are the pricing options?', icon: '', category: 'Billing', description: 'Plans and pricing' },
|
|
136
135
|
{ id: 'd6', text: 'Contact support team', icon: '', category: 'Support', description: 'Talk to a human' },
|
|
137
136
|
];
|
|
137
|
+
function humanizeError(raw) {
|
|
138
|
+
if (/401|unauthorized/i.test(raw))
|
|
139
|
+
return '⚠️ Authentication failed. Please check your API key or token.';
|
|
140
|
+
if (/403|forbidden/i.test(raw))
|
|
141
|
+
return '⚠️ Access denied. You don\'t have permission to use this service.';
|
|
142
|
+
if (/429|rate.?limit/i.test(raw))
|
|
143
|
+
return '⚠️ Too many requests. Please wait a moment and try again.';
|
|
144
|
+
if (/5\d\d|server|internal/i.test(raw))
|
|
145
|
+
return '⚠️ The AI service is temporarily unavailable. Please try again shortly.';
|
|
146
|
+
if (/network|fetch|failed to fetch/i.test(raw))
|
|
147
|
+
return '⚠️ Network error. Please check your connection.';
|
|
148
|
+
return `⚠️ ${raw}`;
|
|
149
|
+
}
|
|
138
150
|
export const AiChatWidget = ({ config, apiKey, jwtToken, title = 'AI Assistant', subtitle = 'Online Ready to help', placeholder = 'Type a message', poweredBy, poweredByUrl = 'https://courseecho.com', onError, onMessageReceived, className, defaultMinimized = false, expandedHeight = '580px', }) => {
|
|
139
151
|
const [mergedConfig] = useState(() => {
|
|
140
152
|
const auto = autoDetectPageContext();
|
|
@@ -147,6 +159,7 @@ export const AiChatWidget = ({ config, apiKey, jwtToken, title = 'AI Assistant',
|
|
|
147
159
|
const [filteredSugs, setFilteredSugs] = useState([]);
|
|
148
160
|
const [activeIdx, setActiveIdx] = useState(-1);
|
|
149
161
|
const [chipsVisible, setChipsVisible] = useState(true);
|
|
162
|
+
const [dismissedError, setDismissedError] = useState(null);
|
|
150
163
|
const messagesEndRef = useRef(null);
|
|
151
164
|
const inputRef = useRef(null);
|
|
152
165
|
const allSugs = (suggestions && suggestions.length > 0) ? suggestions : DEFAULT_SUGGESTIONS;
|
|
@@ -163,6 +176,9 @@ export const AiChatWidget = ({ config, apiKey, jwtToken, title = 'AI Assistant',
|
|
|
163
176
|
}, [messages, onMessageReceived]);
|
|
164
177
|
useEffect(() => { if (error && onError)
|
|
165
178
|
onError(error); }, [error, onError]);
|
|
179
|
+
// Reset dismissed state when a new different error arrives
|
|
180
|
+
useEffect(() => { if (error !== dismissedError)
|
|
181
|
+
setDismissedError(null); }, [error]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
166
182
|
const filterSugs = (q) => {
|
|
167
183
|
const lq = q.toLowerCase();
|
|
168
184
|
const filtered = q.trim()
|
|
@@ -218,10 +234,10 @@ export const AiChatWidget = ({ config, apiKey, jwtToken, title = 'AI Assistant',
|
|
|
218
234
|
const themeClass = mergedConfig.theme === 'dark' ? 'aiwg-dark' : '';
|
|
219
235
|
const minimizedClass = isMinimized ? 'aiwg-minimized' : '';
|
|
220
236
|
const poweredByLabel = poweredBy || 'CourseEcho.com';
|
|
221
|
-
return (_jsx(ShadowWrapper, { children: _jsxs("div", { className: `aiwg-root ${themeClass} ${minimizedClass} ${className || ''}`, style: { height: isMinimized ? '72px' : expandedHeight }, children: [_jsxs("div", { className: "aiwg-header", onClick: () => isMinimized && setIsMinimized(false), children: [_jsx("div", { className: "aiwg-avatar",
|
|
237
|
+
return (_jsx(ShadowWrapper, { children: _jsxs("div", { className: `aiwg-root ${themeClass} ${minimizedClass} ${className || ''}`, style: { height: isMinimized ? '72px' : expandedHeight }, children: [_jsxs("div", { className: "aiwg-header", onClick: () => isMinimized && setIsMinimized(false), children: [_jsx("div", { className: "aiwg-avatar", children: "\uD83E\uDD16" }), _jsxs("div", { className: "aiwg-header-info", children: [_jsx("div", { className: "aiwg-title", children: title }), _jsxs("div", { className: "aiwg-subtitle", children: [_jsx("span", { className: "aiwg-online-dot" }), subtitle] })] }), _jsx("button", { className: "aiwg-minimize-btn", "aria-label": "Minimize", onClick: (e) => { e.stopPropagation(); setIsMinimized(true); }, children: "\u2715" })] }), _jsxs("div", { className: "aiwg-messages", children: [messages.length === 0 && (_jsxs("div", { className: "aiwg-welcome", children: [_jsx("div", { className: "aiwg-welcome-icon", children: "\uD83D\uDC4B" }), _jsx("h4", { children: "Hi there! I'm your AI assistant." }), _jsx("p", { children: "Ask me anything or pick a suggestion below." })] })), messages.map((msg) => {
|
|
222
238
|
const isUser = msg.role === 'user';
|
|
223
239
|
const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
224
|
-
return (_jsxs("div", { className: `aiwg-msg ${isUser ? 'aiwg-msg--user' : 'aiwg-msg--bot'}`, children: [_jsx("div", { className: "aiwg-msg-avatar",
|
|
225
|
-
}), isLoading && (_jsxs("div", { className: "aiwg-msg aiwg-msg--bot", children: [_jsx("div", { className: "aiwg-msg-avatar",
|
|
240
|
+
return (_jsxs("div", { className: `aiwg-msg ${isUser ? 'aiwg-msg--user' : 'aiwg-msg--bot'}`, children: [_jsx("div", { className: "aiwg-msg-avatar", children: isUser ? '👤' : '🤖' }), _jsxs("div", { className: "aiwg-msg-body", children: [_jsx("div", { className: "aiwg-msg-bubble", children: isUser ? msg.content : renderMarkdown(msg.content) }), _jsx("div", { className: "aiwg-msg-time", children: time })] })] }, msg.id));
|
|
241
|
+
}), isLoading && (_jsxs("div", { className: "aiwg-msg aiwg-msg--bot", children: [_jsx("div", { className: "aiwg-msg-avatar", children: "\uD83E\uDD16" }), _jsx("div", { className: "aiwg-msg-body", children: _jsx("div", { className: "aiwg-msg-bubble aiwg-typing", children: _jsxs("div", { className: "aiwg-typing-dots", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }) }) })] })), error && error !== dismissedError && (_jsxs("div", { className: "aiwg-error", style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, children: [_jsx("span", { children: humanizeError(error) }), _jsx("button", { onClick: () => setDismissedError(error), style: { background: 'none', border: 'none', color: 'inherit', cursor: 'pointer', fontSize: '16px', lineHeight: 1, padding: '0 0 0 8px' }, "aria-label": "Dismiss", children: "\u2715" })] })), _jsx("div", { ref: messagesEndRef })] }), chipsVisible && messages.length === 0 && (_jsx("div", { className: "aiwg-chip-row", children: allSugs.slice(0, 4).map(s => (_jsxs("button", { className: "aiwg-chip", onClick: () => selectSuggestion(s.text), children: [s.icon, " ", s.text] }, s.id))) })), _jsxs("div", { className: "aiwg-input-area", children: [showSuggestions && filteredSugs.length > 0 && (_jsxs("div", { className: "aiwg-suggestions", children: [_jsx("div", { className: "aiwg-suggestions-header", children: "Suggestions" }), _jsx("div", { className: "aiwg-suggestions-list", children: filteredSugs.map((s, i) => (_jsxs("div", { className: `aiwg-suggestion-item${i === activeIdx ? ' aiwg-active' : ''}`, onMouseEnter: () => setActiveIdx(i), onClick: () => selectSuggestion(s.text), children: [s.icon && _jsx("span", { className: "aiwg-suggestion-icon", children: s.icon }), _jsxs("div", { className: "aiwg-suggestion-main", children: [_jsx("div", { className: "aiwg-suggestion-text", children: s.text }), s.description && _jsx("div", { className: "aiwg-suggestion-desc", children: s.description })] }), s.category && _jsx("span", { className: "aiwg-suggestion-badge", children: s.category })] }, s.id))) }), _jsxs("div", { className: "aiwg-kbd-hint", children: [_jsxs("span", { children: [_jsx("kbd", {}), " navigate"] }), _jsxs("span", { children: [_jsx("kbd", {}), " select"] }), _jsxs("span", { children: [_jsx("kbd", { children: "Esc" }), " close"] })] })] })), _jsxs("div", { className: "aiwg-input-row", children: [_jsx("textarea", { ref: inputRef, className: "aiwg-input", placeholder: placeholder, value: userInput, rows: 1, onChange: handleInputChange, onFocus: () => filterSugs(userInput), onKeyDown: handleKeyDown, disabled: isLoading, "aria-label": "Chat message input" }), _jsx("button", { className: "aiwg-send-btn", type: "button", disabled: isLoading || !userInput.trim(), onClick: () => doSend(userInput), "aria-label": "Send", children: _jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }), _jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })] }) })] })] }), _jsxs("div", { className: "aiwg-powered", children: ["Powered by", ' ', _jsx("a", { href: poweredByUrl, target: "_blank", rel: "noopener noreferrer", children: poweredByLabel })] })] }) }));
|
|
226
242
|
};
|
|
227
243
|
export default AiChatWidget;
|
package/dist/hooks.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { AiWidgetSDK, AiContextConfig, AiMessage, AiContext, AiSuggestion } from
|
|
|
11
11
|
*/
|
|
12
12
|
export declare function useAiChat(config: AiContextConfig, jwtToken?: string): {
|
|
13
13
|
sdk: AiWidgetSDK;
|
|
14
|
-
sendQuery: (query: string) => Promise<AiMessage>;
|
|
14
|
+
sendQuery: (query: string) => Promise<AiMessage | undefined>;
|
|
15
15
|
setJwt: (token: string) => void;
|
|
16
16
|
checkHealth: () => Promise<boolean>;
|
|
17
17
|
exportChats: (format?: "json" | "csv") => string;
|
|
@@ -54,7 +54,7 @@ export declare function useAiWidget(config: AiContextConfig, jwtToken?: string):
|
|
|
54
54
|
isLoading: boolean;
|
|
55
55
|
error: string | null;
|
|
56
56
|
context: AiContext;
|
|
57
|
-
sendQuery: (query: string) => Promise<AiMessage>;
|
|
57
|
+
sendQuery: (query: string) => Promise<AiMessage | undefined>;
|
|
58
58
|
updateContext: (newContext: Partial<AiContext>) => void;
|
|
59
59
|
updateContextProperty: (key: string, value: any) => void;
|
|
60
60
|
initialized: boolean;
|
package/dist/hooks.js
CHANGED
|
@@ -40,15 +40,18 @@ export function useAiChat(config, jwtToken) {
|
|
|
40
40
|
}, [jwtToken]);
|
|
41
41
|
const sdk = sdkRef.current;
|
|
42
42
|
const sendQuery = useCallback(async (query) => {
|
|
43
|
-
if (!sdk)
|
|
44
|
-
|
|
43
|
+
if (!sdk) {
|
|
44
|
+
setError('SDK not initialized');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
45
47
|
try {
|
|
46
48
|
setError(null);
|
|
47
49
|
return await sdk.sendQuery(query);
|
|
48
50
|
}
|
|
49
51
|
catch (err) {
|
|
50
52
|
setError(String(err));
|
|
51
|
-
throw
|
|
53
|
+
// Do NOT re-throw — error is captured in state and shown in the UI.
|
|
54
|
+
// Re-throwing would cause an unhandled promise rejection that crashes the app.
|
|
52
55
|
}
|
|
53
56
|
}, [sdk]);
|
|
54
57
|
const setJwt = useCallback((token) => {
|