@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.
@@ -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", dangerouslySetInnerHTML: { __html: ICONS.BOT } }), _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); }, dangerouslySetInnerHTML: { __html: ICONS.CLOSE } })] }), _jsxs("div", { className: "aiwg-messages", children: [messages.length === 0 && (_jsxs("div", { className: "aiwg-welcome", children: [_jsx("div", { className: "aiwg-welcome-icon", dangerouslySetInnerHTML: { __html: ICONS.WAVE } }), _jsx("h4", { children: "Hi there! I'm your AI assistant." }), _jsx("p", { children: "Ask me anything or pick a suggestion below." })] })), messages.map((msg) => {
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", dangerouslySetInnerHTML: { __html: isUser ? ICONS.USER : ICONS.BOT } }), _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));
225
- }), isLoading && (_jsxs("div", { className: "aiwg-msg aiwg-msg--bot", children: [_jsx("div", { className: "aiwg-msg-avatar", dangerouslySetInnerHTML: { __html: ICONS.BOT } }), _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 && _jsxs("div", { className: "aiwg-error", style: { display: 'block' }, children: [" ", error] }), _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 })] })] }) }));
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
- throw new Error('SDK not initialized');
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 err;
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@courseecho/ai-widget-react",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "React AI chat widget component for CourseEcho.",
5
5
  "license": "MIT",
6
6
  "author": "CourseEcho",