@courseecho/ai-widget-react 1.0.24 → 1.0.26

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.
@@ -1,12 +1,8 @@
1
1
  /**
2
- * ShadowWrapper — renders children inside a Shadow DOM so the widget's CSS
3
- * is completely isolated from the host application's styles.
2
+ * ShadowWrapper — renders children inside a Shadow DOM.
4
3
  *
5
- * Key isolation techniques used:
6
- * 1. :host { all: initial } — kills every inherited CSS property at the boundary
7
- * 2. Full element reset inside the shadow — counters any UA-sheet or inherited font changes
8
- * 3. Google Fonts injected into document <head> (fonts are shared across shadow trees)
9
- * 4. React portal into a container that lives inside the shadow root
4
+ * CSS lives in ONE place: packages/core/src/widget-css.ts
5
+ * Resolved via the npm workspace symlink no install needed.
10
6
  */
11
7
  import React from 'react';
12
8
  export declare const ShadowWrapper: React.FC<{
@@ -1,17 +1,14 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * ShadowWrapper — renders children inside a Shadow DOM so the widget's CSS
4
- * is completely isolated from the host application's styles.
3
+ * ShadowWrapper — renders children inside a Shadow DOM.
5
4
  *
6
- * Key isolation techniques used:
7
- * 1. :host { all: initial } — kills every inherited CSS property at the boundary
8
- * 2. Full element reset inside the shadow — counters any UA-sheet or inherited font changes
9
- * 3. Google Fonts injected into document <head> (fonts are shared across shadow trees)
10
- * 4. React portal into a container that lives inside the shadow root
5
+ * CSS lives in ONE place: packages/core/src/widget-css.ts
6
+ * Resolved via the npm workspace symlink no install needed.
11
7
  */
12
8
  import { useRef, useEffect, useState } from 'react';
13
9
  import { createPortal } from 'react-dom';
14
- // Inject Inter font into the document <head> once (fonts ARE shared across shadow DOM boundaries)
10
+ import { WIDGET_CSS } from '@courseecho/ai-core-sdk';
11
+ // Inter font must be in document <head> — fonts are shared across shadow DOM boundaries
15
12
  function ensureFont() {
16
13
  if (typeof document === 'undefined')
17
14
  return;
@@ -23,186 +20,18 @@ function ensureFont() {
23
20
  link.href = 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap';
24
21
  document.head.appendChild(link);
25
22
  }
26
- const WIDGET_CSS = `
27
- /* ── Total isolation from host app ───────────────────────────────────────── */
28
- :host {
29
- all: initial !important;
30
- display: block !important;
31
- }
32
-
33
- /* Guarantee every element inside the shadow starts from a known baseline */
34
- *, *::before, *::after {
35
- box-sizing: border-box !important;
36
- margin: 0 !important;
37
- padding: 0 !important;
38
- border: 0 !important;
39
- font-size: 100% !important;
40
- font: inherit !important;
41
- vertical-align: baseline !important;
42
- line-height: inherit !important;
43
- color: inherit !important;
44
- background: transparent !important;
45
- text-decoration: none !important;
46
- list-style: none !important;
47
- }
48
-
49
- /* Restore sensible block/inline defaults that the above stripped */
50
- div, section, article, header, footer, main, nav, aside { display: block !important; }
51
- span, em, strong, small, code, kbd { display: inline !important; }
52
- button { display: inline-flex !important; cursor: pointer !important; }
53
- textarea, input { display: block !important; }
54
- ul, ol { display: flex !important; flex-direction: column !important; }
55
- a { cursor: pointer !important; }
56
-
57
- /* ── Widget component styles ─────────────────────────────────────────────── */
58
- .aiwg-root *, .aiwg-root *::before, .aiwg-root *::after { box-sizing: border-box; margin: 0; padding: 0; }
59
-
60
- .aiwg-root {
61
- --aiwg-primary: #6366f1;
62
- --aiwg-primary-dark: #8b5cf6;
63
- --aiwg-fg-color: #ffffff;
64
- --aiwg-bg-color: #ffffff;
65
- --aiwg-chat-bg: #f1f5f9;
66
-
67
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
68
- font-size: 14px; line-height: 1.5; color: #1a1a2e;
69
- display: flex; flex-direction: column; overflow: hidden;
70
- border-radius: 16px; background: var(--aiwg-bg-color);
71
- position: fixed; bottom: 24px; right: 24px;
72
- width: 380px !important; max-width: calc(100vw - 48px) !important;
73
- z-index: 99999;
74
- transition: height 0.25s ease;
75
- box-shadow: 0 24px 64px rgba(0,0,0,0.18);
76
- }
77
-
78
- .aiwg-header { background: linear-gradient(135deg,#6366f1 0%,#8b5cf6 100%); padding: 16px 20px; display: flex; align-items: center; gap: 12px; flex-shrink: 0; position: relative; overflow: hidden; }
79
- .aiwg-header::before { content:''; position:absolute; width:140px; height:140px; background:rgba(255,255,255,.08); border-radius:50%; top:-50px; right:-30px; }
80
- .aiwg-avatar { width:40px; height:40px; background:rgba(255,255,255,.25); border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:18px; flex-shrink:0; border:2px solid rgba(255,255,255,.4); }
81
- .aiwg-header-info { flex:1; }
82
- .aiwg-title { color:#fff; font-weight:600; font-size:15px; }
83
- .aiwg-subtitle { color:rgba(255,255,255,.75); font-size:12px; display:flex; align-items:center; gap:5px; }
84
- .aiwg-online-dot { width:7px; height:7px; background:#4ade80; border-radius:50%; display:inline-block; animation:aiwg-pulse 2s ease-in-out infinite; }
85
- @keyframes aiwg-pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:.6;transform:scale(.85)} }
86
- .aiwg-minimize-btn { width:36px; height:36px; background:rgba(255,255,255,.18); border:1.5px solid rgba(255,255,255,.3); border-radius:50%; color:#fff; cursor:pointer !important; display:flex; align-items:center; justify-content:center; font-size:18px; line-height:1; transition:background .15s,transform .15s; flex-shrink:0; position:relative; z-index:2; }
87
- .aiwg-minimize-btn * { pointer-events:none; }
88
- .aiwg-minimize-btn:hover { background:rgba(255,255,255,.32); transform:scale(1.08); }
89
-
90
- .aiwg-messages { flex:1; overflow-y:auto; padding:20px 16px 8px; scroll-behavior:smooth; display:flex; flex-direction:column; gap:12px; }
91
- .aiwg-messages::-webkit-scrollbar { width:4px; }
92
- .aiwg-messages::-webkit-scrollbar-thumb { background:#e2e8f0; border-radius:99px; }
93
-
94
- .aiwg-welcome { text-align:center; padding:24px 16px; display:flex; flex-direction:column; align-items:center; gap:8px; }
95
- .aiwg-welcome-icon { font-size:36px; margin-bottom:4px; }
96
- .aiwg-welcome h4 { font-size:15px; font-weight:600; color:#1a1a2e; }
97
- .aiwg-welcome p { font-size:13px; color:#64748b; }
98
-
99
- .aiwg-msg { display:flex; gap:8px; align-items:flex-end; animation:aiwg-fade-up .2s ease both; }
100
- @keyframes aiwg-fade-up { from{opacity:0;transform:translateY(8px)} to{opacity:1;transform:translateY(0)} }
101
- .aiwg-msg--user { flex-direction:row-reverse; }
102
- .aiwg-msg-avatar { width:28px; height:28px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:13px; flex-shrink:0; margin-bottom:2px; }
103
- .aiwg-msg--bot .aiwg-msg-avatar { background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; }
104
- .aiwg-msg--user .aiwg-msg-avatar { background:#e2e8f0; color:#64748b; }
105
- .aiwg-msg-body { max-width:78%; }
106
- .aiwg-msg-bubble { padding:10px 14px; border-radius:18px; font-size:14px; line-height:1.55; word-break:break-word; }
107
- .aiwg-msg--bot .aiwg-msg-bubble { background:#f1f5f9; color:#1a1a2e; border-bottom-left-radius:4px; }
108
- .aiwg-msg--user .aiwg-msg-bubble { background:linear-gradient(135deg,#6366f1,#8b5cf6); color:#fff; border-bottom-right-radius:4px; }
109
- .aiwg-msg-time { font-size:10px; color:#94a3b8; margin-top:4px; text-align:right; padding:0 4px; }
110
- .aiwg-msg--bot .aiwg-msg-time { text-align:left; }
111
-
112
- .aiwg-msg-bubble strong { font-weight:600; }
113
- .aiwg-msg-bubble em { font-style:italic; }
114
- .aiwg-msg-bubble code { background:rgba(0,0,0,.08); padding:1px 5px; border-radius:4px; font-family:'Fira Code','Consolas',monospace; font-size:.9em; }
115
- .aiwg-msg--user .aiwg-msg-bubble code { background:rgba(255,255,255,.2); }
116
- .aiwg-msg-bubble ol, .aiwg-msg-bubble ul { padding-left:18px; margin:6px 0; display:flex; flex-direction:column; gap:3px; }
117
- .aiwg-msg-bubble ol { list-style:decimal; }
118
- .aiwg-msg-bubble ul { list-style:disc; }
119
- .aiwg-msg-bubble li { line-height:1.5; }
120
- .aiwg-msg-bubble h3,.aiwg-msg-bubble h4 { font-weight:600; margin:8px 0 4px; }
121
- .aiwg-msg-bubble p { margin:2px 0; }
122
-
123
- .aiwg-typing { padding:10px 14px; }
124
- .aiwg-typing-dots { display:flex; gap:4px; align-items:center; }
125
- .aiwg-typing-dots span { width:7px; height:7px; background:#94a3b8; border-radius:50%; animation:aiwg-bounce 1.2s ease-in-out infinite; }
126
- .aiwg-typing-dots span:nth-child(2) { animation-delay:.2s; }
127
- .aiwg-typing-dots span:nth-child(3) { animation-delay:.4s; }
128
- @keyframes aiwg-bounce { 0%,80%,100%{transform:translateY(0)} 40%{transform:translateY(-6px)} }
129
-
130
- .aiwg-chip-row { padding:6px 16px 2px; display:flex; flex-wrap:wrap; gap:6px; flex-shrink:0; }
131
- .aiwg-chip { background:#f1f5f9; border:1px solid #e2e8f0; border-radius:99px; padding:4px 12px; font-size:12px; color:#4f46e5; cursor:pointer; transition:all .15s; white-space:nowrap; font-weight:500; }
132
- .aiwg-chip:hover { background:#ede9fe; border-color:#a5b4fc; }
133
-
134
- .aiwg-input-area { padding:12px 16px 16px; flex-shrink:0; background:#fff; border-top:1px solid #f1f5f9; position:relative; }
135
-
136
- .aiwg-suggestions { position:absolute; bottom:calc(100% + 4px); left:16px; right:16px; background:#fff; border:1px solid #e2e8f0; border-radius:12px; box-shadow:0 8px 24px rgba(0,0,0,.12); overflow:hidden; z-index:100; animation:aiwg-fade-up .15s ease both; }
137
- .aiwg-suggestions-header { padding:8px 14px 4px; font-size:11px; font-weight:600; color:#94a3b8; text-transform:uppercase; letter-spacing:.06em; border-bottom:1px solid #f1f5f9; }
138
- .aiwg-suggestion-item { display:flex; align-items:center; gap:10px; padding:10px 14px; cursor:pointer; transition:background .1s; border-bottom:1px solid #f8fafc; }
139
- .aiwg-suggestion-item:last-child { border-bottom:none; }
140
- .aiwg-suggestion-item:hover,.aiwg-suggestion-item.aiwg-active { background:#f5f3ff; }
141
- .aiwg-suggestion-icon { font-size:16px; flex-shrink:0; }
142
- .aiwg-suggestion-main { flex:1; min-width:0; }
143
- .aiwg-suggestion-text { font-size:13px; font-weight:500; color:#1a1a2e; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
144
- .aiwg-suggestion-text mark { background:none; color:#6366f1; font-weight:600; }
145
- .aiwg-suggestion-desc { font-size:11px; color:#94a3b8; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
146
- .aiwg-suggestion-badge { font-size:10px; font-weight:600; padding:2px 8px; border-radius:99px; background:#ede9fe; color:#6366f1; flex-shrink:0; }
147
- .aiwg-kbd-hint { padding:6px 14px; font-size:11px; color:#cbd5e1; background:#f8fafc; display:flex; justify-content:flex-end; gap:10px; }
148
- .aiwg-kbd-hint kbd { font-family:inherit; background:#e2e8f0; border-radius:4px; padding:1px 5px; font-size:10px; color:#64748b; }
149
-
150
- .aiwg-input-row { display:flex; gap:8px; align-items:flex-end; }
151
- .aiwg-input { flex:1; border:1.5px solid #e2e8f0; border-radius:12px; padding:10px 14px; font-size:14px; font-family:inherit; outline:none; resize:none; background:#f8fafc; color:#1a1a2e; transition:border-color .15s,box-shadow .15s,background .15s; line-height:1.4; max-height:120px; overflow-y:auto; scrollbar-width:none; -ms-overflow-style:none; }
152
- .aiwg-input::-webkit-scrollbar { display:none; }
153
- .aiwg-input::placeholder { color:#94a3b8; }
154
- .aiwg-input:focus { border-color:#8b5cf6; background:#fff; box-shadow:0 0 0 3px rgba(139,92,246,.1); }
155
- .aiwg-send-btn { width:42px; height:42px; background:linear-gradient(135deg,#6366f1,#8b5cf6); border:none; border-radius:12px; color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0; transition:transform .15s,box-shadow .15s,opacity .15s; box-shadow:0 4px 12px rgba(99,102,241,.35); }
156
- .aiwg-send-btn svg { width:18px; height:18px; }
157
- .aiwg-send-btn:hover { transform:translateY(-1px); box-shadow:0 6px 16px rgba(99,102,241,.4); }
158
- .aiwg-send-btn:active { transform:translateY(0); }
159
- .aiwg-send-btn:disabled { opacity:.5; cursor:not-allowed; transform:none; }
160
-
161
- .aiwg-error { margin:0 16px 8px; padding:8px 12px; background:#fef2f2; border:1px solid #fecaca; border-radius:8px; font-size:12px; color:#dc2626; }
162
-
163
- .aiwg-root.aiwg-minimized .aiwg-messages,
164
- .aiwg-root.aiwg-minimized .aiwg-chip-row,
165
- .aiwg-root.aiwg-minimized .aiwg-error,
166
- .aiwg-root.aiwg-minimized .aiwg-input-area,
167
- .aiwg-root.aiwg-minimized .aiwg-powered { display:none !important; }
168
- .aiwg-root.aiwg-minimized .aiwg-minimize-btn { display:none !important; }
169
- .aiwg-root.aiwg-minimized { cursor:pointer; }
170
- .aiwg-root.aiwg-minimized .aiwg-header { cursor:pointer !important; }
171
-
172
- .aiwg-powered { text-align:center; padding:6px 16px 10px; font-size:11px; color:#94a3b8; flex-shrink:0; border-top:1px solid #f1f5f9; background:#fff; }
173
- .aiwg-powered a { color:#6366f1; text-decoration:none; font-weight:500; }
174
- .aiwg-powered a:hover { text-decoration:underline; }
175
-
176
- .aiwg-dark { background:#0f172a; color:#e2e8f0; }
177
- .aiwg-dark .aiwg-msg--bot .aiwg-msg-bubble { background:#1e293b; color:#e2e8f0; }
178
- .aiwg-dark .aiwg-input-area { background:#0f172a; border-top-color:#1e293b; }
179
- .aiwg-dark .aiwg-input { background:#1e293b; border-color:#334155; color:#e2e8f0; }
180
- .aiwg-dark .aiwg-input:focus { border-color:#8b5cf6; background:#1e293b; }
181
- .aiwg-dark .aiwg-suggestions { background:#1e293b; border-color:#334155; }
182
- .aiwg-dark .aiwg-suggestions-header { color:#64748b; border-bottom-color:#334155; }
183
- .aiwg-dark .aiwg-suggestion-item { border-bottom-color:#0f172a; }
184
- .aiwg-dark .aiwg-suggestion-item:hover,.aiwg-dark .aiwg-suggestion-item.aiwg-active { background:#2d1b69; }
185
- .aiwg-dark .aiwg-suggestion-text { color:#e2e8f0; }
186
- .aiwg-dark .aiwg-chip { background:#1e293b; border-color:#334155; }
187
- .aiwg-dark .aiwg-chip:hover { background:#2d1b69; }
188
- .aiwg-dark .aiwg-kbd-hint { background:#1e293b; }
189
- .aiwg-dark .aiwg-welcome h4 { color:#e2e8f0; }
190
- .aiwg-dark .aiwg-powered { background:#0f172a; border-top-color:#1e293b; }
191
- `;
192
23
  export const ShadowWrapper = ({ children }) => {
193
24
  const hostRef = useRef(null);
194
25
  const [portalTarget, setPortalTarget] = useState(null);
195
26
  useEffect(() => {
196
- ensureFont(); // inject Inter into <head> — fonts are shared across shadow boundaries
27
+ ensureFont();
197
28
  const host = hostRef.current;
198
29
  if (!host || host.shadowRoot)
199
30
  return;
200
31
  const shadow = host.attachShadow({ mode: 'open' });
201
- // Inject all widget CSS into the shadow root
202
32
  const style = document.createElement('style');
203
33
  style.textContent = WIDGET_CSS;
204
34
  shadow.appendChild(style);
205
- // The container where React will portal the widget tree
206
35
  const container = document.createElement('div');
207
36
  shadow.appendChild(container);
208
37
  setPortalTarget(container);
@@ -4,6 +4,7 @@ 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';
7
8
  import { useAiWidget } from './hooks';
8
9
  import { ShadowWrapper } from './ShadowWrapper';
9
10
  // Chat sound — Web Audio API (no external files)
@@ -217,10 +218,10 @@ export const AiChatWidget = ({ config, apiKey, jwtToken, title = 'AI Assistant',
217
218
  const themeClass = mergedConfig.theme === 'dark' ? 'aiwg-dark' : '';
218
219
  const minimizedClass = isMinimized ? 'aiwg-minimized' : '';
219
220
  const poweredByLabel = poweredBy || 'CourseEcho.com';
220
- 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" }), _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" }), _jsx("h4", { children: "Hi there! I'm your AI assistant." }), _jsx("p", { children: "Ask me anything or pick a suggestion below." })] })), messages.map((msg) => {
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) => {
221
222
  const isUser = msg.role === 'user';
222
223
  const time = new Date(msg.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
223
- 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" }), _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 })] })] }) }));
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 })] })] }) }));
225
226
  };
226
227
  export default AiChatWidget;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@courseecho/ai-widget-react",
3
- "version": "1.0.24",
3
+ "version": "1.0.26",
4
4
  "description": "React AI chat widget component for CourseEcho.",
5
5
  "license": "MIT",
6
6
  "author": "CourseEcho",
@@ -1,118 +0,0 @@
1
- /**
2
- * Page Content Scanner and AI Suggestion Generator
3
- * Automatically extracts page content and generates AI-powered suggestions
4
- */
5
- import { AiSuggestion, PageScanConfig, AiSuggestionGeneratorConfig } from './models';
6
- export interface PageContent {
7
- url: string;
8
- title: string;
9
- description: string;
10
- headings: string[];
11
- keywords: string[];
12
- pageType: string;
13
- rawText: string;
14
- }
15
- /**
16
- * Scans page content and extracts relevant information
17
- */
18
- export declare class PageContentScanner {
19
- private config;
20
- private cachedContent;
21
- constructor(config?: PageScanConfig);
22
- /**
23
- * Scan page content
24
- */
25
- scanPage(): PageContent;
26
- /**
27
- * Get cached content if still valid
28
- */
29
- private getCachedContent;
30
- /**
31
- * Cache content
32
- */
33
- private cacheContent;
34
- /**
35
- * Extract page title
36
- */
37
- private extractTitle;
38
- /**
39
- * Extract meta description
40
- */
41
- private extractDescription;
42
- /**
43
- * Extract all headings
44
- */
45
- private extractHeadings;
46
- /**
47
- * Extract keywords from meta tags
48
- */
49
- private extractKeywords;
50
- /**
51
- * Detect page type (course, product, blog, etc.)
52
- */
53
- private detectPageType;
54
- /**
55
- * Extract main text content
56
- */
57
- private extractMainContent;
58
- /**
59
- * Clear cache
60
- */
61
- clearCache(): void;
62
- /**
63
- * Update config
64
- */
65
- updateConfig(config: Partial<PageScanConfig>): void;
66
- }
67
- /**
68
- * Generates AI-powered suggestions based on page content
69
- */
70
- export declare class AiSuggestionGenerator {
71
- private config;
72
- private pageScanner;
73
- constructor(config?: AiSuggestionGeneratorConfig, pageScanner?: PageContentScanner);
74
- /**
75
- * Generate suggestions based on page content
76
- */
77
- generateSuggestions(): Promise<AiSuggestion[]>;
78
- /**
79
- * Free tier: Generate suggestions locally based on heuristics
80
- */
81
- private generateFreeSuggestions;
82
- /**
83
- * Premium tier: Use backend AI for smart suggestions
84
- */
85
- private generatePremiumSuggestions;
86
- /**
87
- * Course page suggestions
88
- */
89
- private generateCourseSuggestions;
90
- /**
91
- * Product page suggestions
92
- */
93
- private generateProductSuggestions;
94
- /**
95
- * Blog page suggestions
96
- */
97
- private generateBlogSuggestions;
98
- /**
99
- * Support page suggestions
100
- */
101
- private generateSupportSuggestions;
102
- /**
103
- * Documentation page suggestions
104
- */
105
- private generateDocsSuggestions;
106
- /**
107
- * Generic page suggestions
108
- */
109
- private generateGenericSuggestions;
110
- /**
111
- * Update config
112
- */
113
- updateConfig(config: Partial<AiSuggestionGeneratorConfig>): void;
114
- /**
115
- * Get current tier
116
- */
117
- getTier(): string;
118
- }