@courseecho/ai-widget-react 1.0.27 → 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.
@@ -134,6 +134,19 @@ const DEFAULT_SUGGESTIONS = [
134
134
  { id: 'd5', text: 'What are the pricing options?', icon: '', category: 'Billing', description: 'Plans and pricing' },
135
135
  { id: 'd6', text: 'Contact support team', icon: '', category: 'Support', description: 'Talk to a human' },
136
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
+ }
137
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', }) => {
138
151
  const [mergedConfig] = useState(() => {
139
152
  const auto = autoDetectPageContext();
@@ -146,6 +159,7 @@ export const AiChatWidget = ({ config, apiKey, jwtToken, title = 'AI Assistant',
146
159
  const [filteredSugs, setFilteredSugs] = useState([]);
147
160
  const [activeIdx, setActiveIdx] = useState(-1);
148
161
  const [chipsVisible, setChipsVisible] = useState(true);
162
+ const [dismissedError, setDismissedError] = useState(null);
149
163
  const messagesEndRef = useRef(null);
150
164
  const inputRef = useRef(null);
151
165
  const allSugs = (suggestions && suggestions.length > 0) ? suggestions : DEFAULT_SUGGESTIONS;
@@ -162,6 +176,9 @@ export const AiChatWidget = ({ config, apiKey, jwtToken, title = 'AI Assistant',
162
176
  }, [messages, onMessageReceived]);
163
177
  useEffect(() => { if (error && onError)
164
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
165
182
  const filterSugs = (q) => {
166
183
  const lq = q.toLowerCase();
167
184
  const filtered = q.trim()
@@ -221,6 +238,6 @@ export const AiChatWidget = ({ config, apiKey, jwtToken, title = 'AI Assistant',
221
238
  const isUser = msg.role === 'user';
222
239
  const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
223
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));
224
- }), 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 && _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 })] })] }) }));
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 })] })] }) }));
225
242
  };
226
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.27",
3
+ "version": "1.0.28",
4
4
  "description": "React AI chat widget component for CourseEcho.",
5
5
  "license": "MIT",
6
6
  "author": "CourseEcho",