open-ask-ai 0.1.0

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.
@@ -0,0 +1,609 @@
1
+ 'use strict';
2
+
3
+ var jsxRuntime = require('react/jsx-runtime');
4
+ var React = require('react');
5
+ var DialogPrimitive = require('@radix-ui/react-dialog');
6
+ var lucideReact = require('lucide-react');
7
+ var reactSlot = require('@radix-ui/react-slot');
8
+ var ReactMarkdown = require('react-markdown');
9
+ var remarkGfm = require('remark-gfm');
10
+
11
+ function _interopNamespaceDefault(e) {
12
+ var n = Object.create(null);
13
+ if (e) {
14
+ Object.keys(e).forEach(function (k) {
15
+ if (k !== 'default') {
16
+ var d = Object.getOwnPropertyDescriptor(e, k);
17
+ Object.defineProperty(n, k, d.get ? d : {
18
+ enumerable: true,
19
+ get: function () { return e[k]; }
20
+ });
21
+ }
22
+ });
23
+ }
24
+ n.default = e;
25
+ return Object.freeze(n);
26
+ }
27
+
28
+ var React__namespace = /*#__PURE__*/_interopNamespaceDefault(React);
29
+ var DialogPrimitive__namespace = /*#__PURE__*/_interopNamespaceDefault(DialogPrimitive);
30
+
31
+ /**
32
+ * API client for Ask AI widget
33
+ * Supports dynamic API URL configuration
34
+ */
35
+ class APIClient {
36
+ constructor(baseUrl) {
37
+ Object.defineProperty(this, "baseUrl", {
38
+ enumerable: true,
39
+ configurable: true,
40
+ writable: true,
41
+ value: void 0
42
+ });
43
+ this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
44
+ }
45
+ /**
46
+ * Create a new session
47
+ */
48
+ async createSession() {
49
+ const response = await fetch(`${this.baseUrl}/api/session`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Content-Type': 'application/json',
53
+ },
54
+ });
55
+ if (!response.ok) {
56
+ throw new Error(`Failed to create session: ${response.statusText}`);
57
+ }
58
+ return response.json();
59
+ }
60
+ /**
61
+ * Delete a session
62
+ */
63
+ async deleteSession(sessionId) {
64
+ const response = await fetch(`${this.baseUrl}/api/session/${sessionId}`, {
65
+ method: 'DELETE',
66
+ });
67
+ if (!response.ok) {
68
+ throw new Error(`Failed to delete session: ${response.statusText}`);
69
+ }
70
+ }
71
+ /**
72
+ * Ask a question and return the SSE stream response
73
+ */
74
+ async askQuestion(sessionId, question, system) {
75
+ const response = await fetch(`${this.baseUrl}/api/ask`, {
76
+ method: 'POST',
77
+ headers: {
78
+ 'Content-Type': 'application/json',
79
+ },
80
+ body: JSON.stringify({
81
+ sessionId,
82
+ question,
83
+ system,
84
+ }),
85
+ });
86
+ if (!response.ok) {
87
+ throw new Error(`Failed to ask question: ${response.statusText}`);
88
+ }
89
+ return response;
90
+ }
91
+ }
92
+
93
+ function useSession({ apiClient }) {
94
+ const [sessionId, setSessionId] = React.useState(null);
95
+ const [isCreating, setIsCreating] = React.useState(false);
96
+ const [error, setError] = React.useState(null);
97
+ const initializeSession = React.useCallback(async () => {
98
+ setIsCreating(true);
99
+ setError(null);
100
+ try {
101
+ const { sessionId: newSessionId } = await apiClient.createSession();
102
+ setSessionId(newSessionId);
103
+ return newSessionId;
104
+ }
105
+ catch (err) {
106
+ const error = err instanceof Error ? err : new Error('Failed to create session');
107
+ setError(error);
108
+ console.error('Failed to create session:', error);
109
+ throw error;
110
+ }
111
+ finally {
112
+ setIsCreating(false);
113
+ }
114
+ }, [apiClient]);
115
+ const recreateSession = React.useCallback(async () => {
116
+ // Clean up old session if it exists
117
+ if (sessionId) {
118
+ try {
119
+ await apiClient.deleteSession(sessionId);
120
+ }
121
+ catch (err) {
122
+ console.error('Failed to delete old session:', err);
123
+ }
124
+ }
125
+ await initializeSession();
126
+ }, [sessionId, apiClient, initializeSession]);
127
+ // Cleanup on unmount
128
+ React.useEffect(() => {
129
+ return () => {
130
+ if (sessionId) {
131
+ apiClient.deleteSession(sessionId).catch((err) => {
132
+ console.error('Failed to cleanup session on unmount:', err);
133
+ });
134
+ }
135
+ };
136
+ }, [sessionId, apiClient]);
137
+ return {
138
+ sessionId,
139
+ isCreating,
140
+ error,
141
+ initializeSession,
142
+ recreateSession,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Parse SSE chunk format: "event: type\ndata: json\n\n"
148
+ */
149
+ function parseSSEChunk(chunk) {
150
+ const events = [];
151
+ const lines = chunk.split('\n');
152
+ let currentEvent = '';
153
+ let currentData = '';
154
+ for (const line of lines) {
155
+ if (line.startsWith('event: ')) {
156
+ currentEvent = line.slice(7).trim();
157
+ }
158
+ else if (line.startsWith('data: ')) {
159
+ currentData = line.slice(6).trim();
160
+ }
161
+ else if (line === '') {
162
+ // Empty line marks end of event
163
+ if (currentEvent && currentData) {
164
+ try {
165
+ const parsedData = JSON.parse(currentData);
166
+ events.push({
167
+ event: currentEvent,
168
+ data: parsedData,
169
+ });
170
+ }
171
+ catch (err) {
172
+ console.error('Failed to parse SSE data:', currentData, err);
173
+ }
174
+ currentEvent = '';
175
+ currentData = '';
176
+ }
177
+ }
178
+ }
179
+ return events;
180
+ }
181
+ function useSSE(options = {}) {
182
+ const { onConnected, onMessage, onDone, onError } = options;
183
+ const handleSSEStream = React.useCallback(async (response) => {
184
+ const reader = response.body?.getReader();
185
+ const decoder = new TextDecoder();
186
+ if (!reader) {
187
+ onError?.(new Error('No response body'));
188
+ return;
189
+ }
190
+ let buffer = '';
191
+ try {
192
+ while (true) {
193
+ const { done, value } = await reader.read();
194
+ if (done) {
195
+ break;
196
+ }
197
+ buffer += decoder.decode(value, { stream: true });
198
+ // Process complete events
199
+ const events = parseSSEChunk(buffer);
200
+ for (const { event, data } of events) {
201
+ if (event === 'connected') {
202
+ onConnected?.();
203
+ onMessage?.(data);
204
+ }
205
+ else if (event === 'answer') {
206
+ onMessage?.(data);
207
+ }
208
+ else if (event === 'done') {
209
+ onDone?.();
210
+ onMessage?.(data);
211
+ }
212
+ else if (event === 'error') {
213
+ onError?.(new Error(data.error || 'Unknown error'));
214
+ onMessage?.(data);
215
+ }
216
+ }
217
+ // Clear processed events from buffer (keep incomplete data)
218
+ const lastEventEnd = buffer.lastIndexOf('\n\n');
219
+ if (lastEventEnd !== -1) {
220
+ buffer = buffer.slice(lastEventEnd + 2);
221
+ }
222
+ }
223
+ }
224
+ catch (err) {
225
+ const error = err instanceof Error ? err : new Error('SSE stream error');
226
+ onError?.(error);
227
+ }
228
+ finally {
229
+ reader.releaseLock();
230
+ }
231
+ }, [onConnected, onMessage, onDone, onError]);
232
+ return {
233
+ handleSSEStream,
234
+ };
235
+ }
236
+
237
+ function useChat({ apiClient, systemPrompt }) {
238
+ const [messages, setMessages] = React.useState([]);
239
+ const [isStreaming, setIsStreaming] = React.useState(false);
240
+ const [error, setError] = React.useState(null);
241
+ const { sessionId, isCreating: isCreatingSession, error: sessionError, initializeSession } = useSession({ apiClient });
242
+ const { handleSSEStream } = useSSE({
243
+ onConnected: () => {
244
+ console.log('SSE connected');
245
+ },
246
+ onMessage: (data) => {
247
+ if (data.type === 'answer' && data.text) {
248
+ // Accumulate the answer text
249
+ setMessages((prev) => {
250
+ const lastMessage = prev[prev.length - 1];
251
+ if (lastMessage && lastMessage.role === 'assistant' && lastMessage.isStreaming) {
252
+ // Update the last assistant message
253
+ return prev.slice(0, -1).concat({
254
+ ...lastMessage,
255
+ content: lastMessage.content + data.text,
256
+ });
257
+ }
258
+ return prev;
259
+ });
260
+ }
261
+ },
262
+ onDone: () => {
263
+ // Mark streaming complete
264
+ setMessages((prev) => {
265
+ const lastMessage = prev[prev.length - 1];
266
+ if (lastMessage && lastMessage.role === 'assistant') {
267
+ return prev.slice(0, -1).concat({
268
+ ...lastMessage,
269
+ isStreaming: false,
270
+ });
271
+ }
272
+ return prev;
273
+ });
274
+ setIsStreaming(false);
275
+ },
276
+ onError: (err) => {
277
+ setError(err);
278
+ setIsStreaming(false);
279
+ // Remove the failed assistant message
280
+ setMessages((prev) => {
281
+ const lastMessage = prev[prev.length - 1];
282
+ if (lastMessage && lastMessage.role === 'assistant' && lastMessage.isStreaming) {
283
+ return prev.slice(0, -1);
284
+ }
285
+ return prev;
286
+ });
287
+ },
288
+ });
289
+ const sendMessage = React.useCallback(async (text) => {
290
+ if (!text.trim()) {
291
+ return;
292
+ }
293
+ setError(null);
294
+ // Create session if it doesn't exist yet
295
+ let currentSessionId = sessionId;
296
+ if (!currentSessionId && !isCreatingSession) {
297
+ try {
298
+ setIsStreaming(true);
299
+ currentSessionId = await initializeSession();
300
+ }
301
+ catch (err) {
302
+ setError(new Error('Failed to create session'));
303
+ setIsStreaming(false);
304
+ return;
305
+ }
306
+ }
307
+ // Wait for session to be created if it's currently being created
308
+ if (isCreatingSession) {
309
+ setError(new Error('Session is being created, please try again'));
310
+ return;
311
+ }
312
+ if (!currentSessionId) {
313
+ setError(new Error('No active session'));
314
+ return;
315
+ }
316
+ setIsStreaming(true);
317
+ // Add user message immediately (optimistic UI)
318
+ const userMessage = {
319
+ id: crypto.randomUUID(),
320
+ role: 'user',
321
+ content: text,
322
+ timestamp: Date.now(),
323
+ };
324
+ setMessages((prev) => [...prev, userMessage]);
325
+ // Add placeholder for assistant response
326
+ const assistantMessage = {
327
+ id: crypto.randomUUID(),
328
+ role: 'assistant',
329
+ content: '',
330
+ timestamp: Date.now(),
331
+ isStreaming: true,
332
+ };
333
+ setMessages((prev) => [...prev, assistantMessage]);
334
+ try {
335
+ const response = await apiClient.askQuestion(currentSessionId, text, systemPrompt);
336
+ await handleSSEStream(response);
337
+ }
338
+ catch (err) {
339
+ const error = err instanceof Error ? err : new Error('Failed to send message');
340
+ setError(error);
341
+ setIsStreaming(false);
342
+ // Remove the failed messages
343
+ setMessages((prev) => prev.slice(0, -2));
344
+ }
345
+ }, [sessionId, isCreatingSession, initializeSession, apiClient, handleSSEStream, systemPrompt]);
346
+ const clearMessages = React.useCallback(() => {
347
+ setMessages([]);
348
+ setError(null);
349
+ }, []);
350
+ return {
351
+ messages,
352
+ isStreaming,
353
+ error,
354
+ sessionError,
355
+ isCreatingSession,
356
+ sendMessage,
357
+ clearMessages,
358
+ };
359
+ }
360
+
361
+ function styleInject(css, ref) {
362
+ if ( ref === void 0 ) ref = {};
363
+ var insertAt = ref.insertAt;
364
+
365
+ if (!css || typeof document === 'undefined') { return; }
366
+
367
+ var head = document.head || document.getElementsByTagName('head')[0];
368
+ var style = document.createElement('style');
369
+ style.type = 'text/css';
370
+
371
+ if (insertAt === 'top') {
372
+ if (head.firstChild) {
373
+ head.insertBefore(style, head.firstChild);
374
+ } else {
375
+ head.appendChild(style);
376
+ }
377
+ } else {
378
+ head.appendChild(style);
379
+ }
380
+
381
+ if (style.styleSheet) {
382
+ style.styleSheet.cssText = css;
383
+ } else {
384
+ style.appendChild(document.createTextNode(css));
385
+ }
386
+ }
387
+
388
+ var css_248z$5 = "/* Button 组件样式 */\n\n.button_hpHiE {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--ask-ai-radius-md);\n font-size: 14px;\n font-weight: 500;\n transition: all var(--ask-ai-transition);\n border: none;\n cursor: pointer;\n}\n\n.button_hpHiE:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--ask-ai-background), 0 0 0 4px var(--ask-ai-primary);\n}\n\n.button_hpHiE:disabled {\n pointer-events: none;\n opacity: 0.5;\n}\n\n/* Variants */\n.variant-default_kUb7W {\n background-color: var(--ask-ai-primary);\n color: #ffffff;\n}\n\n.variant-default_kUb7W:hover:not(:disabled) {\n background-color: var(--ask-ai-primary-hover);\n}\n\n.variant-outline_B-jpO {\n border: 1px solid var(--ask-ai-border);\n background-color: var(--ask-ai-background);\n color: var(--ask-ai-foreground);\n}\n\n.variant-outline_B-jpO:hover:not(:disabled) {\n background-color: var(--ask-ai-ai-message-bg);\n}\n\n.variant-ghost_Rfses {\n background-color: transparent;\n color: var(--ask-ai-foreground);\n}\n\n.variant-ghost_Rfses:hover:not(:disabled) {\n background-color: var(--ask-ai-ai-message-bg);\n}\n\n.variant-icon_eyYEu {\n background-color: transparent;\n color: var(--ask-ai-foreground);\n}\n\n.variant-icon_eyYEu:hover:not(:disabled) {\n background-color: var(--ask-ai-ai-message-bg);\n}\n\n/* Sizes */\n.size-default_-O-7z {\n height: 40px;\n padding: 8px 16px;\n}\n\n.size-sm_3B2YW {\n height: 36px;\n padding: 6px 12px;\n}\n\n.size-lg_pg-gK {\n height: 44px;\n padding: 10px 32px;\n}\n\n.size-icon_JKSSE {\n height: 40px;\n width: 40px;\n padding: 0;\n}\n";
389
+ var styles$3 = {"button":"button_hpHiE","variant-default":"variant-default_kUb7W","variant-outline":"variant-outline_B-jpO","variant-ghost":"variant-ghost_Rfses","variant-icon":"variant-icon_eyYEu","size-default":"size-default_-O-7z","size-sm":"size-sm_3B2YW","size-lg":"size-lg_pg-gK","size-icon":"size-icon_JKSSE"};
390
+ styleInject(css_248z$5);
391
+
392
+ const Button = React__namespace.forwardRef(({ className, variant = 'default', size = 'default', asChild = false, ...props }, ref) => {
393
+ const Comp = asChild ? reactSlot.Slot : 'button';
394
+ const classNames = [
395
+ styles$3.button,
396
+ styles$3[`variant-${variant}`],
397
+ styles$3[`size-${size}`],
398
+ className,
399
+ ]
400
+ .filter(Boolean)
401
+ .join(' ');
402
+ return (jsxRuntime.jsx(Comp, { className: classNames, ref: ref, ...props }));
403
+ });
404
+ Button.displayName = 'Button';
405
+
406
+ var css_248z$4 = "/* Drawer 组件样式 */\n\n.overlay_M4Ctc {\n position: fixed;\n inset: 0;\n z-index: 50;\n background-color: rgba(0, 0, 0, 0.5);\n}\n\n.overlay_M4Ctc[data-state=\"open\"] {\n animation: fadeIn_ofwCi 300ms ease-in-out;\n}\n\n.overlay_M4Ctc[data-state=\"closed\"] {\n animation: fadeOut_gGY8D 300ms ease-in-out;\n}\n\n.content_CFbqn {\n position: fixed;\n z-index: 50;\n display: flex;\n flex-direction: column;\n background-color: var(--ask-ai-background);\n box-shadow: var(--ask-ai-shadow);\n top: 16px;\n bottom: 16px;\n height: calc(100% - 32px);\n border-radius: var(--ask-ai-radius-lg);\n transition: transform 300ms cubic-bezier(0.4, 0, 0.2, 1);\n width: 100%;\n max-width: calc(100% - 32px);\n overflow: hidden;\n}\n\n@media (min-width: 632px) {\n .content_CFbqn {\n max-width: 600px;\n }\n}\n\n.content_CFbqn[data-state=\"open\"].position-right_jfDyQ {\n animation: slideInFromRight_1XcEG 300ms ease-in-out;\n}\n\n.content_CFbqn[data-state=\"closed\"].position-right_jfDyQ {\n animation: slideOutToRight_R3Q0I 300ms ease-in-out;\n}\n\n.content_CFbqn[data-state=\"open\"].position-left_GhNmP {\n animation: slideInFromLeft_g-jJq 300ms ease-in-out;\n}\n\n.content_CFbqn[data-state=\"closed\"].position-left_GhNmP {\n animation: slideOutToLeft_K488H 300ms ease-in-out;\n}\n\n.position-right_jfDyQ {\n right: 16px;\n}\n\n.position-left_GhNmP {\n left: 16px;\n}\n\n.header_m6xZ6 {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: 1px solid var(--ask-ai-border);\n padding: 16px;\n}\n\n.title_jiPst {\n font-size: 18px;\n font-weight: 600;\n color: var(--ask-ai-foreground);\n}\n\n.closeButton_HTzsf {\n border-radius: 4px;\n opacity: 0.7;\n}\n\n.closeButton_HTzsf:hover {\n opacity: 1;\n}\n\n.closeIcon_QvQtm {\n height: 16px;\n width: 16px;\n}\n\n.body_EPX-M {\n flex: 1;\n overflow: hidden;\n}\n\n/* Animations */\n@keyframes fadeIn_ofwCi {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n@keyframes fadeOut_gGY8D {\n from {\n opacity: 1;\n }\n to {\n opacity: 0;\n }\n}\n\n@keyframes slideInFromRight_1XcEG {\n from {\n transform: translateX(100%);\n }\n to {\n transform: translateX(0);\n }\n}\n\n@keyframes slideOutToRight_R3Q0I {\n from {\n transform: translateX(0);\n }\n to {\n transform: translateX(100%);\n }\n}\n\n@keyframes slideInFromLeft_g-jJq {\n from {\n transform: translateX(-100%);\n }\n to {\n transform: translateX(0);\n }\n}\n\n@keyframes slideOutToLeft_K488H {\n from {\n transform: translateX(0);\n }\n to {\n transform: translateX(-100%);\n }\n}\n";
407
+ var styles$2 = {"overlay":"overlay_M4Ctc","fadeIn":"fadeIn_ofwCi","fadeOut":"fadeOut_gGY8D","content":"content_CFbqn","position-right":"position-right_jfDyQ","slideInFromRight":"slideInFromRight_1XcEG","slideOutToRight":"slideOutToRight_R3Q0I","position-left":"position-left_GhNmP","slideInFromLeft":"slideInFromLeft_g-jJq","slideOutToLeft":"slideOutToLeft_K488H","header":"header_m6xZ6","title":"title_jiPst","closeButton":"closeButton_HTzsf","closeIcon":"closeIcon_QvQtm","body":"body_EPX-M"};
408
+ styleInject(css_248z$4);
409
+
410
+ function Drawer({ isOpen, onClose, position = 'right', width = 600, title = 'Ask AI', closeAriaLabel = 'Close', children, theme = 'light', }) {
411
+ const widthStyle = typeof width === 'number' ? `${width}px` : width;
412
+ const contentClasses = [
413
+ styles$2.content,
414
+ styles$2[`position-${position}`],
415
+ ].join(' ');
416
+ // Create a portal container that inherits theme from the nearest .ask-ai ancestor
417
+ const [portalContainer, setPortalContainer] = React__namespace.useState(null);
418
+ React__namespace.useEffect(() => {
419
+ if (typeof document === 'undefined')
420
+ return;
421
+ // Create a portal container with ask-ai class
422
+ const container = document.createElement('div');
423
+ container.className = `ask-ai${theme === 'dark' ? ' dark' : ''}`;
424
+ document.body.appendChild(container);
425
+ setPortalContainer(container);
426
+ return () => {
427
+ document.body.removeChild(container);
428
+ };
429
+ }, [theme]);
430
+ return (jsxRuntime.jsx(DialogPrimitive__namespace.Root, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: jsxRuntime.jsxs(DialogPrimitive__namespace.Portal, { container: portalContainer, children: [jsxRuntime.jsx(DialogPrimitive__namespace.Overlay, { className: styles$2.overlay }), jsxRuntime.jsxs(DialogPrimitive__namespace.Content, { className: contentClasses, style: { maxWidth: widthStyle }, children: [jsxRuntime.jsxs("div", { className: styles$2.header, children: [jsxRuntime.jsx(DialogPrimitive__namespace.Title, { className: styles$2.title, children: title }), jsxRuntime.jsx(DialogPrimitive__namespace.Close, { asChild: true, children: jsxRuntime.jsx(Button, { variant: "ghost", size: "icon", "aria-label": closeAriaLabel, className: styles$2.closeButton, children: jsxRuntime.jsx(lucideReact.X, { className: styles$2.closeIcon }) }) })] }), jsxRuntime.jsx("div", { className: styles$2.body, children: children })] })] }) }));
431
+ }
432
+
433
+ var css_248z$3 = "/* Trigger 组件样式 */\n\n.trigger_whnWp {\n position: relative;\n z-index: 51;\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 8px;\n box-shadow: var(--ask-ai-shadow);\n}\n\n.icon_KBJUj {\n height: 20px;\n width: 20px;\n}\n";
434
+ var styles$1 = {"trigger":"trigger_whnWp","icon":"icon_KBJUj"};
435
+ styleInject(css_248z$3);
436
+
437
+ function Trigger({ onClick, text = 'Ask AI', ariaLabel = 'Open AI assistant', className, }) {
438
+ const classNames = [styles$1.trigger, className].filter(Boolean).join(' ');
439
+ return (jsxRuntime.jsxs(Button, { onClick: onClick, variant: "outline", "aria-label": ariaLabel, className: classNames, children: [jsxRuntime.jsx(lucideReact.Sparkles, { className: styles$1.icon }), text && jsxRuntime.jsx("span", { children: text })] }));
440
+ }
441
+
442
+ var css_248z$2 = "/* ChatContainer 组件样式 */\n\n.container_FRFCj {\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n\n.messagesArea_Hi-49 {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n}\n\n/* Welcome Screen */\n.welcomeScreen_fJ8-E {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n text-align: center;\n}\n\n.welcomeMessage_unMIK {\n font-size: 18px;\n color: var(--ask-ai-foreground);\n margin-bottom: 16px;\n}\n\n.exampleQuestionsContainer_RBWq6 {\n margin-top: 24px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.exampleQuestionsTitle_Vurj8 {\n font-size: 14px;\n color: var(--ask-ai-muted);\n margin-bottom: 8px;\n}\n\n.exampleButton_6sOzR {\n display: block;\n width: 100%;\n text-align: left;\n padding: 8px 16px;\n font-size: 14px;\n color: var(--ask-ai-foreground);\n background-color: var(--ask-ai-ai-message-bg);\n border: none;\n border-radius: var(--ask-ai-radius-md);\n cursor: pointer;\n transition: background-color var(--ask-ai-transition);\n}\n\n.exampleButton_6sOzR:hover {\n background-color: var(--ask-ai-border);\n}\n\n/* Messages */\n.messageWrapper_nsQr8 {\n display: flex;\n}\n\n.messageWrapper_nsQr8.user_s1Gzu {\n justify-content: flex-end;\n}\n\n.messageWrapper_nsQr8.assistant_xU0-H {\n justify-content: flex-start;\n}\n\n.message_jxpo4 {\n max-width: 85%;\n border-radius: var(--ask-ai-radius-lg);\n padding: 8px 16px;\n}\n\n.message_jxpo4.user_s1Gzu {\n background-color: var(--ask-ai-user-message-bg);\n color: var(--ask-ai-user-message-text);\n}\n\n.message_jxpo4.assistant_xU0-H {\n background-color: var(--ask-ai-ai-message-bg);\n color: var(--ask-ai-ai-message-text);\n}\n\n.messageText_neW3m {\n font-size: 14px;\n white-space: pre-wrap;\n}\n\n/* Markdown Styles */\n.markdown_mNC3q {\n font-size: 14px;\n line-height: 1.6;\n}\n\n.markdown_mNC3q > *:first-child {\n margin-top: 0;\n}\n\n.markdown_mNC3q > *:last-child {\n margin-bottom: 0;\n}\n\n/* 段落 */\n.markdown_mNC3q p {\n display: block;\n margin: 0.5em 0;\n}\n\n/* 标题 */\n.markdown_mNC3q h1,\n.markdown_mNC3q h2,\n.markdown_mNC3q h3,\n.markdown_mNC3q h4,\n.markdown_mNC3q h5,\n.markdown_mNC3q h6 {\n display: block;\n margin: 1em 0 0.5em;\n font-weight: 600;\n line-height: 1.3;\n}\n\n.markdown_mNC3q h1 {\n font-size: 1.5em;\n}\n\n.markdown_mNC3q h2 {\n font-size: 1.3em;\n}\n\n.markdown_mNC3q h3 {\n font-size: 1.1em;\n}\n\n/* 无序列表 */\n.markdown_mNC3q ul {\n display: block;\n margin: 0.5em 0;\n padding-left: 1.5em;\n list-style-type: disc;\n list-style-position: outside;\n}\n\n/* 有序列表 */\n.markdown_mNC3q ol {\n display: block;\n margin: 0.5em 0;\n padding-left: 1.5em;\n list-style-type: decimal;\n list-style-position: outside;\n}\n\n/* 嵌套列表 */\n.markdown_mNC3q ul ul {\n list-style-type: circle;\n}\n\n.markdown_mNC3q ul ul ul {\n list-style-type: square;\n}\n\n.markdown_mNC3q ol ol {\n list-style-type: lower-alpha;\n}\n\n.markdown_mNC3q ol ol ol {\n list-style-type: lower-roman;\n}\n\n/* 列表项 */\n.markdown_mNC3q li {\n display: list-item;\n margin: 0.25em 0;\n}\n\n/* 文本样式 */\n.markdown_mNC3q strong,\n.markdown_mNC3q b {\n font-weight: bold;\n}\n\n.markdown_mNC3q em,\n.markdown_mNC3q i {\n font-style: italic;\n}\n\n.markdown_mNC3q u {\n text-decoration: underline;\n}\n\n.markdown_mNC3q s,\n.markdown_mNC3q del {\n text-decoration: line-through;\n}\n\n.markdown_mNC3q code {\n font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;\n font-size: 0.9em;\n padding: 0.15em 0.4em;\n background-color: var(--ask-ai-border);\n border-radius: 4px;\n}\n\n.markdown_mNC3q pre {\n margin: 0.75em 0;\n padding: 0.75em 1em;\n background-color: var(--ask-ai-border);\n border-radius: var(--ask-ai-radius-md);\n overflow-x: auto;\n}\n\n.markdown_mNC3q pre code {\n padding: 0;\n background-color: transparent;\n border-radius: 0;\n font-size: 0.85em;\n}\n\n.markdown_mNC3q blockquote {\n margin: 0.5em 0;\n padding: 0.5em 1em;\n border-left: 3px solid var(--ask-ai-primary);\n background-color: var(--ask-ai-border);\n border-radius: 0 var(--ask-ai-radius-sm) var(--ask-ai-radius-sm) 0;\n}\n\n.markdown_mNC3q blockquote p {\n margin: 0;\n}\n\n.markdown_mNC3q a {\n color: var(--ask-ai-primary);\n text-decoration: none;\n}\n\n.markdown_mNC3q a:hover {\n text-decoration: underline;\n}\n\n.markdown_mNC3q table {\n width: 100%;\n margin: 0.75em 0;\n border-collapse: collapse;\n font-size: 0.9em;\n}\n\n.markdown_mNC3q th,\n.markdown_mNC3q td {\n padding: 0.5em 0.75em;\n border: 1px solid var(--ask-ai-border);\n text-align: left;\n}\n\n.markdown_mNC3q th {\n background-color: var(--ask-ai-border);\n font-weight: 600;\n}\n\n.markdown_mNC3q hr {\n margin: 1em 0;\n border: none;\n border-top: 1px solid var(--ask-ai-border);\n}\n\n.markdown_mNC3q img {\n max-width: 100%;\n height: auto;\n border-radius: var(--ask-ai-radius-md);\n}\n\n.cursor_9Dhwg {\n display: inline-block;\n width: 2px;\n height: 16px;\n margin-left: 4px;\n background-color: currentColor;\n animation: pulse_STM-e 1s cubic-bezier(0.4, 0, 0.6, 1) infinite;\n}\n\n@keyframes pulse_STM-e {\n 0%, 100% {\n opacity: 1;\n }\n 50% {\n opacity: 0.5;\n }\n}\n\n/* Error */\n.error_uFX9a {\n padding: 16px;\n background-color: rgba(220, 38, 38, 0.1);\n color: var(--ask-ai-error);\n border-radius: var(--ask-ai-radius-md);\n font-size: 14px;\n}\n\n/* Input Area */\n.inputForm_nC0l- {\n border-top: 1px solid var(--ask-ai-border);\n padding: 16px;\n}\n\n.inputWrapper_zGeKy {\n display: flex;\n gap: 8px;\n align-items: flex-end;\n}\n\n.input_7lMOc {\n flex: 1;\n padding: 8px 16px;\n font-size: 14px;\n background-color: var(--ask-ai-background);\n border: 1px solid var(--ask-ai-border);\n border-radius: var(--ask-ai-radius-md);\n color: var(--ask-ai-foreground);\n font-family: inherit;\n resize: none;\n min-height: 40px;\n max-height: 200px;\n line-height: 1.5;\n overflow-y: auto;\n}\n\n.input_7lMOc:focus {\n outline: none;\n box-shadow: 0 0 0 2px var(--ask-ai-primary);\n}\n\n.input_7lMOc:disabled {\n opacity: 0.5;\n}\n\n.submitButton_2XrPY {\n padding: 8px 16px;\n font-size: 14px;\n background-color: var(--ask-ai-primary);\n color: #ffffff;\n border: none;\n border-radius: var(--ask-ai-radius-md);\n cursor: pointer;\n transition: all var(--ask-ai-transition);\n}\n\n.submitButton_2XrPY:hover:not(:disabled) {\n background-color: var(--ask-ai-primary-hover);\n}\n\n.submitButton_2XrPY:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n";
443
+ var styles = {"container":"container_FRFCj","messagesArea":"messagesArea_Hi-49","welcomeScreen":"welcomeScreen_fJ8-E","welcomeMessage":"welcomeMessage_unMIK","exampleQuestionsContainer":"exampleQuestionsContainer_RBWq6","exampleQuestionsTitle":"exampleQuestionsTitle_Vurj8","exampleButton":"exampleButton_6sOzR","messageWrapper":"messageWrapper_nsQr8","user":"user_s1Gzu","assistant":"assistant_xU0-H","message":"message_jxpo4","messageText":"messageText_neW3m","markdown":"markdown_mNC3q","cursor":"cursor_9Dhwg","pulse":"pulse_STM-e","error":"error_uFX9a","inputForm":"inputForm_nC0l-","inputWrapper":"inputWrapper_zGeKy","input":"input_7lMOc","submitButton":"submitButton_2XrPY"};
444
+ styleInject(css_248z$2);
445
+
446
+ function ChatContainer({ texts, exampleQuestions, onMessage, onError, messages, isStreaming, error, sendMessage, input, setInput, }) {
447
+ const textareaRef = React__namespace.useRef(null);
448
+ const messagesAreaRef = React__namespace.useRef(null);
449
+ const shouldAutoScrollRef = React__namespace.useRef(true); // Track if auto-scroll is enabled
450
+ // Check if user is at the bottom of the messages area
451
+ const isAtBottom = React__namespace.useCallback(() => {
452
+ const messagesArea = messagesAreaRef.current;
453
+ if (!messagesArea)
454
+ return true;
455
+ const threshold = 50; // pixels from bottom to consider "at bottom"
456
+ const scrollBottom = messagesArea.scrollHeight - messagesArea.scrollTop - messagesArea.clientHeight;
457
+ return scrollBottom < threshold;
458
+ }, []);
459
+ // Scroll to bottom of messages area
460
+ const scrollToBottom = React__namespace.useCallback(() => {
461
+ const messagesArea = messagesAreaRef.current;
462
+ if (messagesArea) {
463
+ messagesArea.scrollTop = messagesArea.scrollHeight;
464
+ }
465
+ }, []);
466
+ // Handle user scroll events
467
+ const handleScroll = React__namespace.useCallback(() => {
468
+ const atBottom = isAtBottom();
469
+ shouldAutoScrollRef.current = atBottom;
470
+ }, [isAtBottom]);
471
+ // Auto-scroll when messages change (if enabled)
472
+ React__namespace.useEffect(() => {
473
+ if (shouldAutoScrollRef.current) {
474
+ scrollToBottom();
475
+ }
476
+ }, [messages, scrollToBottom]);
477
+ // Auto-resize textarea based on content
478
+ const adjustTextareaHeight = React__namespace.useCallback(() => {
479
+ const textarea = textareaRef.current;
480
+ if (textarea) {
481
+ textarea.style.height = 'auto';
482
+ textarea.style.height = `${textarea.scrollHeight}px`;
483
+ }
484
+ }, []);
485
+ // Call onError callback when error occurs
486
+ React__namespace.useEffect(() => {
487
+ if (error && onError) {
488
+ onError(error);
489
+ }
490
+ }, [error, onError]);
491
+ // Call onMessage callback when new message arrives
492
+ React__namespace.useEffect(() => {
493
+ if (messages.length > 0 && onMessage) {
494
+ onMessage(messages[messages.length - 1]);
495
+ }
496
+ }, [messages, onMessage]);
497
+ const handleSubmit = async (e) => {
498
+ e.preventDefault();
499
+ if (!input.trim() || isStreaming)
500
+ return;
501
+ // Enable auto-scroll for new user message
502
+ shouldAutoScrollRef.current = true;
503
+ await sendMessage(input);
504
+ setInput('');
505
+ // Reset textarea height after submission
506
+ setTimeout(() => {
507
+ if (textareaRef.current) {
508
+ textareaRef.current.style.height = 'auto';
509
+ }
510
+ }, 0);
511
+ };
512
+ const handleInputChange = (e) => {
513
+ setInput(e.target.value);
514
+ adjustTextareaHeight();
515
+ };
516
+ const handleKeyDown = (e) => {
517
+ // Submit on Enter (without Shift)
518
+ if (e.key === 'Enter' && !e.shiftKey) {
519
+ e.preventDefault();
520
+ if (input.trim() && !isStreaming) {
521
+ handleSubmit(e);
522
+ }
523
+ }
524
+ // Allow Shift+Enter for new line (default textarea behavior)
525
+ };
526
+ const handleExampleClick = async (question) => {
527
+ if (isStreaming)
528
+ return;
529
+ // Enable auto-scroll for new user message
530
+ shouldAutoScrollRef.current = true;
531
+ await sendMessage(question);
532
+ };
533
+ const inputPlaceholder = texts?.inputPlaceholder || 'Type your question...';
534
+ const welcomeMessage = texts?.welcomeMessage || 'Hi! How can I help you today?';
535
+ const exampleQuestionsTitle = texts?.exampleQuestionsTitle || 'Example questions:';
536
+ return (jsxRuntime.jsxs("div", { className: styles.container, children: [jsxRuntime.jsxs("div", { ref: messagesAreaRef, onScroll: handleScroll, className: styles.messagesArea, children: [messages.length === 0 ? (
537
+ // Welcome Screen
538
+ jsxRuntime.jsxs("div", { className: styles.welcomeScreen, children: [jsxRuntime.jsx("p", { className: styles.welcomeMessage, children: welcomeMessage }), exampleQuestions && exampleQuestions.length > 0 && (jsxRuntime.jsxs("div", { className: styles.exampleQuestionsContainer, children: [jsxRuntime.jsx("p", { className: styles.exampleQuestionsTitle, children: exampleQuestionsTitle }), exampleQuestions.map((question, index) => (jsxRuntime.jsx("button", { onClick: () => handleExampleClick(question), className: styles.exampleButton, children: question }, index)))] }))] })) : (
539
+ // Messages
540
+ jsxRuntime.jsx(jsxRuntime.Fragment, { children: messages.map((message) => (jsxRuntime.jsx("div", { className: `${styles.messageWrapper} ${styles[message.role]}`, children: jsxRuntime.jsx("div", { className: `${styles.message} ${styles[message.role]}`, children: message.role === 'assistant' ? (jsxRuntime.jsxs("div", { className: styles.markdown, children: [jsxRuntime.jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], children: message.content }), message.isStreaming && (jsxRuntime.jsx("span", { className: styles.cursor }))] })) : (jsxRuntime.jsx("p", { className: styles.messageText, children: message.content })) }) }, message.id))) })), error && (jsxRuntime.jsx("div", { className: styles.error, children: error.message }))] }), jsxRuntime.jsx("form", { onSubmit: handleSubmit, className: styles.inputForm, children: jsxRuntime.jsxs("div", { className: styles.inputWrapper, children: [jsxRuntime.jsx("textarea", { ref: textareaRef, value: input, onChange: handleInputChange, onKeyDown: handleKeyDown, placeholder: inputPlaceholder, disabled: isStreaming, className: styles.input, rows: 1 }), jsxRuntime.jsx("button", { type: "submit", disabled: !input.trim() || isStreaming, className: styles.submitButton, children: isStreaming ? 'Sending...' : 'Send' })] }) })] }));
541
+ }
542
+
543
+ function Widget(props) {
544
+ const { apiUrl, drawerPosition = 'right', drawerWidth = 600, theme = 'light', texts = {}, exampleQuestions = [], systemPrompt, hotkey, enableHotkey = true, onOpen, onClose, onMessage, onError, className, style, children, } = props;
545
+ const [isOpen, setIsOpen] = React__namespace.useState(false);
546
+ const [apiClient] = React__namespace.useState(() => new APIClient(apiUrl));
547
+ // Lift chat state to Widget level to persist across drawer open/close
548
+ const { messages, isStreaming, error, sendMessage } = useChat({ apiClient, systemPrompt });
549
+ // Input state also needs to persist
550
+ const [input, setInput] = React__namespace.useState('');
551
+ // Handle drawer open
552
+ const handleOpen = React__namespace.useCallback(() => {
553
+ setIsOpen(true);
554
+ onOpen?.();
555
+ }, [onOpen]);
556
+ // Handle drawer close
557
+ const handleClose = React__namespace.useCallback(() => {
558
+ setIsOpen(false);
559
+ onClose?.();
560
+ }, [onClose]);
561
+ // Handle keyboard shortcut
562
+ React__namespace.useEffect(() => {
563
+ if (!enableHotkey || !hotkey)
564
+ return;
565
+ const handleKeyDown = (e) => {
566
+ const keys = hotkey.toLowerCase().split('+');
567
+ const ctrl = keys.includes('ctrl') || keys.includes('control');
568
+ const cmd = keys.includes('cmd') || keys.includes('command') || keys.includes('meta');
569
+ const shift = keys.includes('shift');
570
+ const alt = keys.includes('alt');
571
+ const key = keys[keys.length - 1];
572
+ const ctrlPressed = ctrl && (e.ctrlKey || e.metaKey);
573
+ const cmdPressed = cmd && (e.metaKey || e.ctrlKey);
574
+ const shiftPressed = !shift || e.shiftKey;
575
+ const altPressed = !alt || e.altKey;
576
+ if ((ctrlPressed || cmdPressed) && shiftPressed && altPressed && e.key.toLowerCase() === key) {
577
+ e.preventDefault();
578
+ setIsOpen((prev) => !prev);
579
+ }
580
+ };
581
+ document.addEventListener('keydown', handleKeyDown);
582
+ return () => document.removeEventListener('keydown', handleKeyDown);
583
+ }, [enableHotkey, hotkey]);
584
+ return (jsxRuntime.jsxs("div", { className: `ask-ai ${className || ''} ${theme === 'dark' ? 'dark' : ''}`, style: style, children: [children && React__namespace.isValidElement(children) ? (React__namespace.cloneElement(children, {
585
+ ...children.props,
586
+ onClick: (e) => {
587
+ // Call existing onClick if present
588
+ const existingOnClick = children.props?.onClick;
589
+ if (existingOnClick) {
590
+ existingOnClick(e);
591
+ }
592
+ // Then call our handleOpen
593
+ handleOpen();
594
+ },
595
+ })) : (jsxRuntime.jsx(Trigger, { onClick: handleOpen, text: texts.triggerButtonText, ariaLabel: texts.triggerButtonAriaLabel })), jsxRuntime.jsx(Drawer, { isOpen: isOpen, onClose: handleClose, position: drawerPosition, width: drawerWidth, title: texts.drawerTitle, closeAriaLabel: texts.drawerCloseAriaLabel, theme: theme, children: jsxRuntime.jsx(ChatContainer, { texts: texts, exampleQuestions: exampleQuestions, onMessage: onMessage, onError: onError, messages: messages, isStreaming: isStreaming, error: error, sendMessage: sendMessage, input: input, setInput: setInput }) })] }));
596
+ }
597
+
598
+ var css_248z$1 = "/* CSS 变量定义 - 亮色主题(默认) */\n.ask-ai {\n /* 颜色 */\n --ask-ai-primary: #2563eb;\n --ask-ai-primary-hover: #1d4ed8;\n --ask-ai-background: #ffffff;\n --ask-ai-foreground: #0f172a;\n --ask-ai-muted: #64748b;\n --ask-ai-border: #e2e8f0;\n --ask-ai-error: #dc2626;\n\n /* 用户消息 */\n --ask-ai-user-message-bg: #2563eb;\n --ask-ai-user-message-text: #ffffff;\n\n /* AI 消息 */\n --ask-ai-ai-message-bg: #f1f5f9;\n --ask-ai-ai-message-text: #0f172a;\n\n /* 字体 */\n --ask-ai-font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n --ask-ai-font-size-base: 14px;\n --ask-ai-line-height: 1.5;\n\n /* 间距 */\n --ask-ai-spacing-xs: 4px;\n --ask-ai-spacing-sm: 8px;\n --ask-ai-spacing-md: 16px;\n --ask-ai-spacing-lg: 24px;\n\n /* 圆角 */\n --ask-ai-radius-sm: 4px;\n --ask-ai-radius-md: 8px;\n --ask-ai-radius-lg: 12px;\n\n /* 阴影 */\n --ask-ai-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);\n\n /* 动画 */\n --ask-ai-transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n/* 明确指定暗色主题 */\n.ask-ai.dark {\n --ask-ai-primary: #3b82f6;\n --ask-ai-primary-hover: #2563eb;\n --ask-ai-background: #0f172a;\n --ask-ai-foreground: #f1f5f9;\n --ask-ai-muted: #94a3b8;\n --ask-ai-border: #334155;\n --ask-ai-ai-message-bg: #1e293b;\n --ask-ai-ai-message-text: #f1f5f9;\n --ask-ai-error: #ef4444;\n}\n";
599
+ styleInject(css_248z$1);
600
+
601
+ var css_248z = "/* 全局样式 - 所有样式都在 .ask-ai 命名空间下 */\n\n/* 基础样式重置 */\n.ask-ai *,\n.ask-ai *::before,\n.ask-ai *::after {\n box-sizing: border-box;\n}\n\n.ask-ai {\n font-family: var(--ask-ai-font-family);\n font-size: var(--ask-ai-font-size-base);\n line-height: var(--ask-ai-line-height);\n color: var(--ask-ai-foreground);\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n/* 确保组件不受外部样式影响 */\n.ask-ai button {\n font-family: inherit;\n}\n\n.ask-ai input,\n.ask-ai textarea {\n font-family: inherit;\n font-size: inherit;\n}\n\n/* 滚动条样式 */\n.ask-ai ::-webkit-scrollbar {\n width: 8px;\n height: 8px;\n}\n\n.ask-ai ::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.ask-ai ::-webkit-scrollbar-thumb {\n background: var(--ask-ai-border);\n border-radius: var(--ask-ai-radius-sm);\n}\n\n.ask-ai ::-webkit-scrollbar-thumb:hover {\n background: var(--ask-ai-muted);\n}\n";
602
+ styleInject(css_248z);
603
+
604
+ exports.APIClient = APIClient;
605
+ exports.AskAIWidget = Widget;
606
+ exports.useChat = useChat;
607
+ exports.useSSE = useSSE;
608
+ exports.useSession = useSession;
609
+ //# sourceMappingURL=index.cjs.js.map