@djangocfg/layouts 2.1.103 → 2.1.105

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.
Files changed (94) hide show
  1. package/package.json +33 -37
  2. package/src/components/RedirectPage/RedirectPage.tsx +2 -2
  3. package/src/components/core/ClientOnly.tsx +1 -1
  4. package/src/components/errors/ErrorLayout.tsx +1 -1
  5. package/src/components/errors/ErrorsTracker/components/ErrorButtons.tsx +1 -1
  6. package/src/components/index.ts +2 -0
  7. package/src/index.ts +2 -0
  8. package/src/layouts/AuthLayout/components/AuthHelp.tsx +1 -1
  9. package/src/layouts/AuthLayout/components/AuthSuccess.tsx +1 -1
  10. package/src/layouts/AuthLayout/components/IdentifierForm.tsx +1 -1
  11. package/src/layouts/AuthLayout/components/OTPForm.tsx +1 -1
  12. package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +1 -1
  13. package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +1 -1
  14. package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +1 -1
  15. package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +1 -1
  16. package/src/layouts/PrivateLayout/PrivateLayout.tsx +3 -2
  17. package/src/layouts/PrivateLayout/components/PrivateHeader.tsx +2 -2
  18. package/src/layouts/ProfileLayout/ProfileLayout.tsx +1 -1
  19. package/src/layouts/ProfileLayout/__tests__/TwoFactorSection.test.tsx +1 -1
  20. package/src/layouts/ProfileLayout/components/AvatarSection.tsx +1 -1
  21. package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +1 -1
  22. package/src/layouts/ProfileLayout/components/ProfileForm.tsx +1 -1
  23. package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +1 -1
  24. package/src/layouts/PublicLayout/components/PublicFooter/PublicFooter.tsx +1 -1
  25. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +1 -1
  26. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +2 -2
  27. package/src/layouts/_components/UserMenu.tsx +1 -1
  28. package/src/layouts/index.ts +2 -0
  29. package/src/pages/index.ts +2 -0
  30. package/src/pages/legal/LegalPage.tsx +1 -1
  31. package/src/snippets/AuthDialog/AuthDialog.tsx +3 -2
  32. package/src/snippets/McpChat/components/AIChatWidget.tsx +1 -1
  33. package/src/snippets/McpChat/components/AskAIButton.tsx +1 -1
  34. package/src/snippets/McpChat/components/ChatMessages.tsx +1 -1
  35. package/src/snippets/McpChat/components/ChatPanel.tsx +1 -1
  36. package/src/snippets/McpChat/components/ChatSidebar.tsx +1 -1
  37. package/src/snippets/McpChat/components/ChatWidget.tsx +1 -1
  38. package/src/snippets/McpChat/components/MessageBubble.tsx +1 -1
  39. package/src/snippets/McpChat/components/MessageInput.tsx +1 -1
  40. package/src/snippets/McpChat/context/AIChatContext.tsx +1 -1
  41. package/src/snippets/McpChat/context/ChatContext.tsx +1 -1
  42. package/src/snippets/McpChat/hooks/useChatLayout.ts +1 -1
  43. package/src/snippets/PWAInstall/components/A2HSHint.tsx +0 -1
  44. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +1 -1
  45. package/src/snippets/PWAInstall/components/IOSGuide.tsx +1 -1
  46. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +1 -1
  47. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +1 -1
  48. package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +2 -2
  49. package/src/snippets/PushNotifications/components/PushPrompt.tsx +1 -1
  50. package/src/snippets/index.ts +1 -0
  51. package/dist/AIChatWidget-LUPM7S2O.mjs +0 -1644
  52. package/dist/AIChatWidget-LUPM7S2O.mjs.map +0 -1
  53. package/dist/AIChatWidget-O23TJJ7C.mjs +0 -3
  54. package/dist/AIChatWidget-O23TJJ7C.mjs.map +0 -1
  55. package/dist/chunk-53YKWR6F.mjs +0 -6
  56. package/dist/chunk-53YKWR6F.mjs.map +0 -1
  57. package/dist/chunk-EI7TDN2G.mjs +0 -1652
  58. package/dist/chunk-EI7TDN2G.mjs.map +0 -1
  59. package/dist/components.cjs +0 -925
  60. package/dist/components.cjs.map +0 -1
  61. package/dist/components.d.mts +0 -583
  62. package/dist/components.d.ts +0 -583
  63. package/dist/components.mjs +0 -879
  64. package/dist/components.mjs.map +0 -1
  65. package/dist/index.cjs +0 -7573
  66. package/dist/index.cjs.map +0 -1
  67. package/dist/index.d.mts +0 -2376
  68. package/dist/index.d.ts +0 -2376
  69. package/dist/index.mjs +0 -5673
  70. package/dist/index.mjs.map +0 -1
  71. package/dist/layouts.cjs +0 -6530
  72. package/dist/layouts.cjs.map +0 -1
  73. package/dist/layouts.d.mts +0 -748
  74. package/dist/layouts.d.ts +0 -748
  75. package/dist/layouts.mjs +0 -4741
  76. package/dist/layouts.mjs.map +0 -1
  77. package/dist/pages.cjs +0 -178
  78. package/dist/pages.cjs.map +0 -1
  79. package/dist/pages.d.mts +0 -57
  80. package/dist/pages.d.ts +0 -57
  81. package/dist/pages.mjs +0 -168
  82. package/dist/pages.mjs.map +0 -1
  83. package/dist/snippets.cjs +0 -3793
  84. package/dist/snippets.cjs.map +0 -1
  85. package/dist/snippets.d.mts +0 -1192
  86. package/dist/snippets.d.ts +0 -1192
  87. package/dist/snippets.mjs +0 -3738
  88. package/dist/snippets.mjs.map +0 -1
  89. package/dist/utils.cjs +0 -34
  90. package/dist/utils.cjs.map +0 -1
  91. package/dist/utils.d.mts +0 -40
  92. package/dist/utils.d.ts +0 -40
  93. package/dist/utils.mjs +0 -25
  94. package/dist/utils.mjs.map +0 -1
package/dist/snippets.mjs DELETED
@@ -1,3738 +0,0 @@
1
- import { User, Bot, Loader2, ExternalLink, MessageSquare, StopCircle, Send, RotateCcw, PanelRight, X, GripVertical, PanelRightClose, Zap, MessageCircle, LogIn, Monitor, Check, Share, Bell, ChevronRight, Download, ArrowDownToLine, Menu, Search, Plus, ArrowUpRight, ArrowDown, CheckCircle } from 'lucide-react';
2
- import 'next/link';
3
- import { usePathname } from 'next/navigation';
4
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
- import React3, { createContext, forwardRef, useRef, useImperativeHandle, useState, useCallback, useEffect, useContext, useMemo } from 'react';
6
- import { Dialog, DialogContent, DialogHeader, DialogTitle, Button as Button$1 } from '@djangocfg/ui-nextjs/components';
7
- import { useLocalStorage, useIsMobile, useCfgRouter, useEventListener } from '@djangocfg/ui-nextjs/hooks';
8
- import { events, toast } from '@djangocfg/ui-core/hooks';
9
- import ReactGA from 'react-ga4';
10
- import { useAuth } from '@djangocfg/api/auth';
11
- import { Avatar, AvatarImage, AvatarFallback, Card, CardContent, Badge, Button, CardHeader, CardFooter, Portal, Dialog as Dialog$1, DialogContent as DialogContent$1, DialogHeader as DialogHeader$1, DialogTitle as DialogTitle$1, DialogDescription, DialogFooter, useBrowserDetect, useDeviceDetect, Drawer, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription } from '@djangocfg/ui-nextjs';
12
- import { v4 } from 'uuid';
13
- import { MarkdownMessage } from '@djangocfg/ui-tools';
14
- import { consola } from 'consola';
15
- import { cn } from '@djangocfg/ui-core/lib';
16
- import { apiWebPush } from '@djangocfg/api/clients';
17
-
18
- var __defProp = Object.defineProperty;
19
- var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
20
- function generateBreadcrumbsFromPath(pathname) {
21
- const segments = pathname.split("/").filter(Boolean);
22
- const breadcrumbs = [
23
- { path: "/", label: "Home", isActive: pathname === "/" }
24
- ];
25
- let currentPath = "";
26
- segments.forEach((segment, index) => {
27
- currentPath += `/${segment}`;
28
- breadcrumbs.push({
29
- path: currentPath,
30
- label: segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, " "),
31
- isActive: index === segments.length - 1
32
- });
33
- });
34
- return breadcrumbs;
35
- }
36
- __name(generateBreadcrumbsFromPath, "generateBreadcrumbsFromPath");
37
- var DIALOG_EVENTS = {
38
- OPEN_AUTH_DIALOG: "OPEN_AUTH_DIALOG",
39
- CLOSE_AUTH_DIALOG: "CLOSE_AUTH_DIALOG",
40
- AUTH_SUCCESS: "AUTH_SUCCESS",
41
- AUTH_FAILURE: "AUTH_FAILURE"
42
- };
43
- var AuthDialog = /* @__PURE__ */ __name(({
44
- onAuthRequired,
45
- authPath = "/auth"
46
- }) => {
47
- const [open, setOpen] = useState(false);
48
- const [message, setMessage] = useState("Please sign in to continue");
49
- const router = useCfgRouter();
50
- useEventListener(DIALOG_EVENTS.OPEN_AUTH_DIALOG, (payload) => {
51
- if (payload?.message) {
52
- setMessage(payload.message);
53
- }
54
- setOpen(true);
55
- });
56
- useEventListener(DIALOG_EVENTS.CLOSE_AUTH_DIALOG, () => {
57
- setOpen(false);
58
- });
59
- const handleClose = /* @__PURE__ */ __name(() => {
60
- setMessage("Please sign in to continue");
61
- setOpen(false);
62
- }, "handleClose");
63
- const handleGoToAuth = /* @__PURE__ */ __name(() => {
64
- if (typeof window !== "undefined") {
65
- sessionStorage.setItem("redirectAfterAuth", window.location.pathname);
66
- }
67
- if (onAuthRequired) {
68
- onAuthRequired();
69
- } else {
70
- router.push(authPath);
71
- }
72
- handleClose();
73
- }, "handleGoToAuth");
74
- return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: handleClose, children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-w-sm", children: [
75
- /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: "Authentication Required" }) }),
76
- /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
77
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: message }),
78
- /* @__PURE__ */ jsxs(Button$1, { onClick: handleGoToAuth, className: "w-full", children: [
79
- /* @__PURE__ */ jsx(LogIn, { className: "h-4 w-4 mr-2" }),
80
- "Go to Sign In"
81
- ] })
82
- ] })
83
- ] }) });
84
- }, "AuthDialog");
85
-
86
- // src/snippets/AuthDialog/events.ts
87
- var AUTH_EVENTS = {
88
- OPEN_AUTH_DIALOG: "OPEN_AUTH_DIALOG",
89
- CLOSE_AUTH_DIALOG: "CLOSE_AUTH_DIALOG",
90
- AUTH_SUCCESS: "AUTH_SUCCESS",
91
- AUTH_FAILURE: "AUTH_FAILURE"
92
- };
93
- function useAuthDialog() {
94
- const openAuthDialog = useCallback((options) => {
95
- events.publish({
96
- type: AUTH_EVENTS.OPEN_AUTH_DIALOG,
97
- payload: options
98
- });
99
- }, []);
100
- const closeAuthDialog = useCallback(() => {
101
- events.publish({
102
- type: AUTH_EVENTS.CLOSE_AUTH_DIALOG
103
- });
104
- }, []);
105
- return {
106
- openAuthDialog,
107
- closeAuthDialog
108
- };
109
- }
110
- __name(useAuthDialog, "useAuthDialog");
111
- var isProduction = false;
112
- var Analytics = {
113
- /**
114
- * Initialize Google Analytics (called automatically by useAnalytics hook)
115
- */
116
- init: /* @__PURE__ */ __name((trackingId) => {
117
- return;
118
- }, "init"),
119
- /**
120
- * Check if Analytics is enabled and initialized
121
- */
122
- isEnabled: /* @__PURE__ */ __name(() => isProduction, "isEnabled"),
123
- /**
124
- * Track a page view
125
- */
126
- pageview: /* @__PURE__ */ __name((path) => {
127
- if (!Analytics.isEnabled()) return;
128
- ReactGA.send({ hitType: "pageview", page: path });
129
- }, "pageview"),
130
- /**
131
- * Track a custom event
132
- * @param name - Event name (action)
133
- * @param params - Optional event parameters
134
- */
135
- event: /* @__PURE__ */ __name((name, params = {}) => {
136
- if (!Analytics.isEnabled()) return;
137
- ReactGA.event(name, params);
138
- }, "event"),
139
- /**
140
- * Set user ID for tracking
141
- */
142
- setUser: /* @__PURE__ */ __name((userId) => {
143
- if (!Analytics.isEnabled()) return;
144
- ReactGA.set({ user_id: userId });
145
- }, "setUser"),
146
- /**
147
- * Set custom dimensions/metrics
148
- */
149
- set: /* @__PURE__ */ __name((fieldsObject) => {
150
- if (!Analytics.isEnabled()) return;
151
- ReactGA.set(fieldsObject);
152
- }, "set")
153
- };
154
- function useAnalytics(trackingIdProp) {
155
- const pathname = usePathname();
156
- const { user, isAuthenticated } = useAuth();
157
- const trackingId = trackingIdProp;
158
- const isEnabled = isProduction;
159
- useEffect(() => {
160
- return;
161
- }, [isEnabled, trackingId]);
162
- useEffect(() => {
163
- return;
164
- }, [isEnabled, isAuthenticated, user?.id]);
165
- useEffect(() => {
166
- return;
167
- }, [pathname, isEnabled]);
168
- return {
169
- isEnabled,
170
- trackingId,
171
- pageview: Analytics.pageview,
172
- event: Analytics.event,
173
- setUser: Analytics.setUser,
174
- set: Analytics.set
175
- };
176
- }
177
- __name(useAnalytics, "useAnalytics");
178
- function AnalyticsProvider({ children, trackingId }) {
179
- useAnalytics(trackingId);
180
- return /* @__PURE__ */ jsx(Fragment, { children });
181
- }
182
- __name(AnalyticsProvider, "AnalyticsProvider");
183
-
184
- // src/snippets/Analytics/events.ts
185
- var AnalyticsCategory = {
186
- AUTH: "auth",
187
- ERROR: "error",
188
- NAVIGATION: "navigation",
189
- ENGAGEMENT: "engagement",
190
- USER: "user"
191
- };
192
- var AnalyticsEvent = {
193
- // Auth Events
194
- AUTH_OTP_REQUEST: "auth_otp_request",
195
- AUTH_OTP_VERIFY_SUCCESS: "auth_otp_verify_success",
196
- AUTH_OTP_VERIFY_FAIL: "auth_otp_verify_fail",
197
- AUTH_LOGIN_SUCCESS: "auth_login_success",
198
- AUTH_LOGOUT: "auth_logout",
199
- AUTH_SESSION_EXPIRED: "auth_session_expired",
200
- AUTH_TOKEN_REFRESH: "auth_token_refresh",
201
- AUTH_TOKEN_REFRESH_FAIL: "auth_token_refresh_fail",
202
- // OAuth Events
203
- AUTH_OAUTH_START: "auth_oauth_start",
204
- AUTH_OAUTH_SUCCESS: "auth_oauth_success",
205
- AUTH_OAUTH_FAIL: "auth_oauth_fail",
206
- // Error Events
207
- ERROR_BOUNDARY: "error_boundary",
208
- ERROR_API: "error_api",
209
- ERROR_VALIDATION: "error_validation",
210
- ERROR_NETWORK: "error_network",
211
- // Navigation Events
212
- NAV_ADMIN_ENTER: "nav_admin_enter",
213
- NAV_DASHBOARD_ENTER: "nav_dashboard_enter",
214
- NAV_PAGE_VIEW: "nav_page_view",
215
- // Engagement Events
216
- THEME_CHANGE: "theme_change",
217
- SIDEBAR_TOGGLE: "sidebar_toggle",
218
- MOBILE_MENU_OPEN: "mobile_menu_open",
219
- // User Events
220
- USER_PROFILE_VIEW: "user_profile_view",
221
- USER_PROFILE_UPDATE: "user_profile_update"
222
- };
223
-
224
- // src/snippets/McpChat/config.ts
225
- var PROD_HOST = "https://mcp.djangocfg.com";
226
- var DEV_HOST = "http://localhost:3002";
227
- function getHost(autoDetect = false) {
228
- if (autoDetect && true) {
229
- return DEV_HOST;
230
- }
231
- return PROD_HOST;
232
- }
233
- __name(getHost, "getHost");
234
- function getMcpEndpoints(autoDetect = false) {
235
- const HOST = getHost(autoDetect);
236
- return {
237
- /** Base URL */
238
- baseUrl: HOST,
239
- /** Chat API endpoint */
240
- chat: `${HOST}/api/chat`,
241
- /** Search API endpoint */
242
- search: `${HOST}/api/search`,
243
- /** Conversations API endpoint */
244
- conversations: `${HOST}/api/conversations`,
245
- /** Health check endpoint */
246
- health: `${HOST}/health`,
247
- /** MCP protocol endpoint (Streamable HTTP) */
248
- mcp: `${HOST}/mcp`,
249
- /** SSE endpoint for legacy clients */
250
- sse: `${HOST}/mcp/sse`
251
- };
252
- }
253
- __name(getMcpEndpoints, "getMcpEndpoints");
254
- var mcpEndpoints = getMcpEndpoints(false);
255
- var sidebarConfig = {
256
- /** Minimum sidebar width in pixels */
257
- minWidth: 320,
258
- /** Maximum sidebar width in pixels */
259
- maxWidth: 600,
260
- /** Default sidebar width in pixels */
261
- defaultWidth: 400,
262
- /** Z-index for chat elements */
263
- zIndex: 300,
264
- /** Animation duration in milliseconds */
265
- animationDuration: 200
266
- };
267
- var fabConfig = {
268
- /** Bottom offset in pixels */
269
- bottom: 24,
270
- /** Right offset in pixels */
271
- right: 24};
272
- var storageKeys = {
273
- /** Display mode (closed, floating, sidebar) */
274
- mode: "djangocfg-chat-mode",
275
- /** User ID for conversation tracking */
276
- userId: "djangocfg-chat-user-id",
277
- /** Chat messages history */
278
- messages: "djangocfg-chat-messages",
279
- /** Sidebar width */
280
- sidebarWidth: "djangocfg-chat-sidebar-width"
281
- };
282
- function generateMessageId() {
283
- return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
284
- }
285
- __name(generateMessageId, "generateMessageId");
286
- var ChatContext = createContext(null);
287
- function ChatProvider({
288
- children,
289
- apiEndpoint = "/api/chat",
290
- config: userConfig = {},
291
- onError
292
- }) {
293
- const [messages, setMessages] = useState([]);
294
- const [isLoading, setIsLoading] = useState(false);
295
- const [error, setError] = useState(null);
296
- const [isMinimized, setIsMinimized] = useState(false);
297
- const isHydratedRef = useRef(false);
298
- const mobileResetRef = useRef(false);
299
- const [storedMode, setStoredMode] = useLocalStorage(storageKeys.mode, "closed");
300
- const [userId, setUserId] = useLocalStorage(storageKeys.userId, "");
301
- const [storedMessages, setStoredMessages] = useLocalStorage(storageKeys.messages, []);
302
- const isMobile = useIsMobile();
303
- useEffect(() => {
304
- if (isMobile && !mobileResetRef.current && storedMode !== "closed") {
305
- mobileResetRef.current = true;
306
- setStoredMode("closed");
307
- }
308
- }, [isMobile, storedMode, setStoredMode]);
309
- useEffect(() => {
310
- if (!userId) {
311
- setUserId(v4());
312
- }
313
- }, [userId, setUserId]);
314
- useEffect(() => {
315
- if (isHydratedRef.current) return;
316
- isHydratedRef.current = true;
317
- if (storedMessages.length > 0) {
318
- const hydratedMessages = storedMessages.map((msg) => ({
319
- ...msg,
320
- timestamp: new Date(msg.timestamp)
321
- }));
322
- setMessages(hydratedMessages);
323
- }
324
- }, [storedMessages]);
325
- useEffect(() => {
326
- if (!isHydratedRef.current) return;
327
- const toStore = messages.slice(-50).map((msg) => ({
328
- id: msg.id,
329
- role: msg.role,
330
- content: msg.content,
331
- timestamp: msg.timestamp.toISOString(),
332
- sources: msg.sources
333
- }));
334
- setStoredMessages(toStore);
335
- }, [messages]);
336
- const displayMode = useMemo(() => {
337
- if (isMobile && storedMode === "sidebar") {
338
- return "floating";
339
- }
340
- return storedMode;
341
- }, [isMobile, storedMode]);
342
- const isOpen = displayMode !== "closed";
343
- const config = useMemo(
344
- () => ({
345
- apiEndpoint,
346
- title: "DjangoCFG AI",
347
- placeholder: "Ask about DjangoCFG...",
348
- greeting: "Hi! I'm your DjangoCFG documentation assistant. Ask me anything about configuration, features, or how to use the library.",
349
- position: "bottom-right",
350
- variant: "default",
351
- ...userConfig
352
- }),
353
- [apiEndpoint, userConfig]
354
- );
355
- const sendMessage = useCallback(
356
- async (content) => {
357
- if (!content.trim() || isLoading) return;
358
- const userMessage = {
359
- id: generateMessageId(),
360
- role: "user",
361
- content: content.trim(),
362
- timestamp: /* @__PURE__ */ new Date()
363
- };
364
- setMessages((prev) => [...prev, userMessage]);
365
- setIsLoading(true);
366
- setError(null);
367
- try {
368
- const response = await fetch(config.apiEndpoint || apiEndpoint, {
369
- method: "POST",
370
- headers: { "Content-Type": "application/json" },
371
- body: JSON.stringify({
372
- query: content,
373
- userId: userId || void 0
374
- })
375
- });
376
- if (!response.ok) {
377
- throw new Error(`HTTP error: ${response.status}`);
378
- }
379
- const data = await response.json();
380
- if (!data.success) {
381
- throw new Error(data.error || "Failed to get response");
382
- }
383
- const sources = data.results?.map((r) => ({
384
- title: r.chunk.title,
385
- path: r.chunk.path,
386
- url: r.chunk.url,
387
- section: r.chunk.section,
388
- score: r.score
389
- })) || [];
390
- const assistantMessage = {
391
- id: generateMessageId(),
392
- role: "assistant",
393
- content: data.answer || "I found some relevant documentation.",
394
- timestamp: /* @__PURE__ */ new Date(),
395
- sources
396
- };
397
- setMessages((prev) => [...prev, assistantMessage]);
398
- } catch (err) {
399
- const error2 = err instanceof Error ? err : new Error("Unknown error");
400
- setError(error2);
401
- onError?.(error2);
402
- const errorMessage = {
403
- id: generateMessageId(),
404
- role: "assistant",
405
- content: `Sorry, I encountered an error: ${error2.message}. Please try again.`,
406
- timestamp: /* @__PURE__ */ new Date()
407
- };
408
- setMessages((prev) => [...prev, errorMessage]);
409
- } finally {
410
- setIsLoading(false);
411
- }
412
- },
413
- [apiEndpoint, config.apiEndpoint, isLoading, onError, userId]
414
- );
415
- const clearMessages = useCallback(() => {
416
- setMessages([]);
417
- setStoredMessages([]);
418
- setError(null);
419
- }, [setStoredMessages]);
420
- const openChat = useCallback(() => {
421
- setStoredMode("floating");
422
- setIsMinimized(false);
423
- }, [setStoredMode]);
424
- const closeChat = useCallback(() => {
425
- setStoredMode("closed");
426
- setIsMinimized(false);
427
- }, [setStoredMode]);
428
- const toggleChat = useCallback(() => {
429
- if (displayMode === "closed") {
430
- setStoredMode("floating");
431
- setIsMinimized(false);
432
- } else {
433
- setStoredMode("closed");
434
- }
435
- }, [displayMode, setStoredMode]);
436
- const toggleMinimize = useCallback(() => {
437
- setIsMinimized((prev) => !prev);
438
- }, []);
439
- const setDisplayMode = useCallback(
440
- (mode) => {
441
- if (isMobile && mode === "sidebar") {
442
- setStoredMode("floating");
443
- } else {
444
- setStoredMode(mode);
445
- }
446
- setIsMinimized(false);
447
- },
448
- [isMobile, setStoredMode]
449
- );
450
- const value = useMemo(
451
- () => ({
452
- messages,
453
- isLoading,
454
- error,
455
- isOpen,
456
- isMinimized,
457
- config,
458
- displayMode,
459
- isMobile,
460
- userId: userId || "",
461
- sendMessage,
462
- clearMessages,
463
- openChat,
464
- closeChat,
465
- toggleChat,
466
- toggleMinimize,
467
- setDisplayMode
468
- }),
469
- [
470
- messages,
471
- isLoading,
472
- error,
473
- isOpen,
474
- isMinimized,
475
- config,
476
- displayMode,
477
- isMobile,
478
- userId,
479
- sendMessage,
480
- clearMessages,
481
- openChat,
482
- closeChat,
483
- toggleChat,
484
- toggleMinimize,
485
- setDisplayMode
486
- ]
487
- );
488
- return /* @__PURE__ */ jsx(ChatContext.Provider, { value, children });
489
- }
490
- __name(ChatProvider, "ChatProvider");
491
- function useChatContext() {
492
- const context = useContext(ChatContext);
493
- if (!context) {
494
- throw new Error("useChatContext must be used within a ChatProvider");
495
- }
496
- return context;
497
- }
498
- __name(useChatContext, "useChatContext");
499
- function useChatContextOptional() {
500
- return useContext(ChatContext);
501
- }
502
- __name(useChatContextOptional, "useChatContextOptional");
503
- function generateId() {
504
- return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
505
- }
506
- __name(generateId, "generateId");
507
- function generateThreadId() {
508
- return `thread_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
509
- }
510
- __name(generateThreadId, "generateThreadId");
511
- function generateUserId() {
512
- return `user_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
513
- }
514
- __name(generateUserId, "generateUserId");
515
- var STORAGE_KEY = "djangocfg_chat";
516
- function getPersistedIds() {
517
- if (typeof window === "undefined") {
518
- return { threadId: generateThreadId(), userId: generateUserId() };
519
- }
520
- try {
521
- const stored = localStorage.getItem(STORAGE_KEY);
522
- if (stored) {
523
- const data = JSON.parse(stored);
524
- if (data.threadId && data.userId) {
525
- return data;
526
- }
527
- }
528
- } catch {
529
- }
530
- const ids = { threadId: generateThreadId(), userId: generateUserId() };
531
- try {
532
- localStorage.setItem(STORAGE_KEY, JSON.stringify(ids));
533
- } catch {
534
- }
535
- return ids;
536
- }
537
- __name(getPersistedIds, "getPersistedIds");
538
- function persistThreadId(threadId, userId) {
539
- if (typeof window === "undefined") return;
540
- try {
541
- localStorage.setItem(STORAGE_KEY, JSON.stringify({ threadId, userId }));
542
- } catch {
543
- }
544
- }
545
- __name(persistThreadId, "persistThreadId");
546
- async function saveMessageToServer(threadId, userId, message) {
547
- try {
548
- await fetch(`${mcpEndpoints.conversations}/${threadId}/messages`, {
549
- method: "POST",
550
- headers: { "Content-Type": "application/json" },
551
- body: JSON.stringify({
552
- userId,
553
- message: {
554
- id: message.id,
555
- role: message.role,
556
- content: message.content,
557
- timestamp: message.timestamp.getTime(),
558
- sources: message.sources
559
- }
560
- })
561
- });
562
- } catch (error) {
563
- console.warn("[Chat] Failed to save message to server:", error);
564
- }
565
- }
566
- __name(saveMessageToServer, "saveMessageToServer");
567
- async function loadConversationFromServer(threadId) {
568
- try {
569
- const response = await fetch(`${mcpEndpoints.conversations}/${threadId}`);
570
- if (response.status === 404) return null;
571
- if (!response.ok) return [];
572
- const data = await response.json();
573
- if (!data.messages || !Array.isArray(data.messages)) return [];
574
- return data.messages.map((m) => ({
575
- id: m.id,
576
- role: m.role,
577
- content: m.content,
578
- timestamp: new Date(m.timestamp),
579
- sources: m.sources
580
- }));
581
- } catch (error) {
582
- console.warn("[Chat] Failed to load conversation from server:", error);
583
- return [];
584
- }
585
- }
586
- __name(loadConversationFromServer, "loadConversationFromServer");
587
- async function deleteConversationFromServer(threadId) {
588
- try {
589
- await fetch(`${mcpEndpoints.conversations}/${threadId}`, {
590
- method: "DELETE"
591
- });
592
- } catch (error) {
593
- console.warn("[Chat] Failed to delete conversation from server:", error);
594
- }
595
- }
596
- __name(deleteConversationFromServer, "deleteConversationFromServer");
597
- function useAIChat(options) {
598
- const {
599
- apiEndpoint = mcpEndpoints.chat,
600
- initialMessages = [],
601
- onError,
602
- enableStreaming = true,
603
- threadId: initialThreadId,
604
- userId: initialUserId
605
- } = options;
606
- const [messages, setMessages] = useState(initialMessages);
607
- const [isLoadingHistory, setIsLoadingHistory] = useState(true);
608
- const persistedIds = useRef(null);
609
- if (persistedIds.current === null && typeof window !== "undefined") {
610
- persistedIds.current = getPersistedIds();
611
- }
612
- const [threadId, setThreadId] = useState(
613
- () => initialThreadId || persistedIds.current?.threadId || generateThreadId()
614
- );
615
- const [userId] = useState(
616
- () => initialUserId || persistedIds.current?.userId || generateUserId()
617
- );
618
- const [isLoading, setIsLoading] = useState(false);
619
- const [error, setError] = useState(null);
620
- const abortControllerRef = useRef(null);
621
- useEffect(() => {
622
- if (typeof window === "undefined") {
623
- setIsLoadingHistory(false);
624
- return;
625
- }
626
- const loadHistory = /* @__PURE__ */ __name(async () => {
627
- const serverMessages = await loadConversationFromServer(threadId);
628
- if (serverMessages === null) {
629
- console.log("[Chat] Session expired or invalid, starting new session");
630
- const newThreadId = generateThreadId();
631
- setThreadId(newThreadId);
632
- persistThreadId(newThreadId, userId);
633
- setMessages([]);
634
- } else if (serverMessages.length > 0) {
635
- setMessages(serverMessages);
636
- }
637
- setIsLoadingHistory(false);
638
- }, "loadHistory");
639
- loadHistory();
640
- }, [threadId, userId]);
641
- const sendMessage = useCallback(
642
- async (content) => {
643
- if (!content.trim() || isLoading) return;
644
- if (abortControllerRef.current) {
645
- abortControllerRef.current.abort();
646
- }
647
- abortControllerRef.current = new AbortController();
648
- const userMessage = {
649
- id: generateId(),
650
- role: "user",
651
- content: content.trim(),
652
- timestamp: /* @__PURE__ */ new Date()
653
- };
654
- const assistantMessageId = generateId();
655
- const assistantMessage = {
656
- id: assistantMessageId,
657
- role: "assistant",
658
- content: "",
659
- timestamp: /* @__PURE__ */ new Date(),
660
- isStreaming: true
661
- };
662
- setMessages((prev) => [...prev, userMessage, assistantMessage]);
663
- setIsLoading(true);
664
- setError(null);
665
- saveMessageToServer(threadId, userId, userMessage);
666
- try {
667
- const chatMessages = [
668
- ...messages.filter((m) => m.role !== "system").slice(-10).map((m) => ({
669
- role: m.role,
670
- content: m.content
671
- })),
672
- { role: "user", content }
673
- ];
674
- const response = await fetch(apiEndpoint, {
675
- method: "POST",
676
- headers: {
677
- "Content-Type": "application/json"
678
- },
679
- body: JSON.stringify({
680
- messages: chatMessages,
681
- stream: enableStreaming
682
- }),
683
- signal: abortControllerRef.current.signal
684
- });
685
- if (!response.ok) {
686
- throw new Error(`HTTP error: ${response.status}`);
687
- }
688
- if (enableStreaming && response.headers.get("content-type")?.includes("text/event-stream")) {
689
- await handleStreamingResponse(response, assistantMessageId);
690
- } else {
691
- const data = await response.json();
692
- if (!data.success) {
693
- throw new Error(data.error || "Failed to get response");
694
- }
695
- if (data.threadId && data.threadId !== threadId) {
696
- setThreadId(data.threadId);
697
- }
698
- const sources = data.sources?.map((s) => ({
699
- title: s.title,
700
- path: s.path,
701
- url: s.url,
702
- section: s.section,
703
- score: s.score
704
- })) || [];
705
- const finalContent = data.content || "I found some relevant documentation.";
706
- setMessages(
707
- (prev) => prev.map(
708
- (m) => m.id === assistantMessageId ? {
709
- ...m,
710
- content: finalContent,
711
- sources,
712
- isStreaming: false
713
- } : m
714
- )
715
- );
716
- saveMessageToServer(threadId, userId, {
717
- id: assistantMessageId,
718
- role: "assistant",
719
- content: finalContent,
720
- timestamp: /* @__PURE__ */ new Date(),
721
- sources
722
- });
723
- }
724
- } catch (err) {
725
- if (err instanceof Error && err.name === "AbortError") {
726
- setMessages((prev) => prev.filter((m) => m.id !== assistantMessageId));
727
- return;
728
- }
729
- const error2 = err instanceof Error ? err : new Error("Unknown error");
730
- setError(error2);
731
- onError?.(error2);
732
- setMessages(
733
- (prev) => prev.map(
734
- (m) => m.id === assistantMessageId ? {
735
- ...m,
736
- content: `Sorry, I encountered an error: ${error2.message}. Please try again.`,
737
- isStreaming: false
738
- } : m
739
- )
740
- );
741
- } finally {
742
- setIsLoading(false);
743
- abortControllerRef.current = null;
744
- }
745
- },
746
- [apiEndpoint, isLoading, messages, threadId, userId, enableStreaming, onError]
747
- );
748
- const handleStreamingResponse = /* @__PURE__ */ __name(async (response, messageId) => {
749
- const reader = response.body?.getReader();
750
- if (!reader) {
751
- throw new Error("No response body");
752
- }
753
- const decoder = new TextDecoder();
754
- let buffer = "";
755
- let fullContent = "";
756
- const sources = [];
757
- try {
758
- while (true) {
759
- const { done, value } = await reader.read();
760
- if (done) break;
761
- buffer += decoder.decode(value, { stream: true });
762
- const lines = buffer.split("\n");
763
- buffer = lines.pop() || "";
764
- for (const line of lines) {
765
- if (line.startsWith("data: ")) {
766
- const data = line.slice(6);
767
- if (data === "[DONE]") {
768
- setMessages(
769
- (prev) => prev.map(
770
- (m) => m.id === messageId ? {
771
- ...m,
772
- content: fullContent,
773
- sources,
774
- isStreaming: false
775
- } : m
776
- )
777
- );
778
- saveMessageToServer(threadId, userId, {
779
- id: messageId,
780
- role: "assistant",
781
- content: fullContent,
782
- timestamp: /* @__PURE__ */ new Date(),
783
- sources
784
- });
785
- return;
786
- }
787
- try {
788
- const parsed = JSON.parse(data);
789
- if (parsed.type === "text" && parsed.content) {
790
- fullContent += parsed.content;
791
- setMessages(
792
- (prev) => prev.map(
793
- (m) => m.id === messageId ? {
794
- ...m,
795
- content: fullContent,
796
- isStreaming: true
797
- } : m
798
- )
799
- );
800
- } else if (parsed.type === "source" && parsed.source) {
801
- sources.push({
802
- title: parsed.source.title,
803
- path: parsed.source.path,
804
- url: parsed.source.url,
805
- section: parsed.source.section,
806
- score: parsed.source.score
807
- });
808
- } else if (parsed.type === "done") {
809
- setMessages(
810
- (prev) => prev.map(
811
- (m) => m.id === messageId ? {
812
- ...m,
813
- content: fullContent,
814
- sources,
815
- isStreaming: false
816
- } : m
817
- )
818
- );
819
- saveMessageToServer(threadId, userId, {
820
- id: messageId,
821
- role: "assistant",
822
- content: fullContent,
823
- timestamp: /* @__PURE__ */ new Date(),
824
- sources
825
- });
826
- } else if (parsed.type === "error") {
827
- throw new Error(parsed.error || "Stream error");
828
- }
829
- } catch {
830
- }
831
- }
832
- }
833
- }
834
- } finally {
835
- reader.releaseLock();
836
- }
837
- }, "handleStreamingResponse");
838
- const clearMessages = useCallback(async () => {
839
- if (abortControllerRef.current) {
840
- abortControllerRef.current.abort();
841
- }
842
- await deleteConversationFromServer(threadId);
843
- setMessages([]);
844
- setError(null);
845
- const newThreadId = generateThreadId();
846
- setThreadId(newThreadId);
847
- persistThreadId(newThreadId, userId);
848
- }, [threadId, userId]);
849
- const stopStreaming = useCallback(() => {
850
- if (abortControllerRef.current) {
851
- abortControllerRef.current.abort();
852
- }
853
- }, []);
854
- return {
855
- messages,
856
- isLoading: isLoading || isLoadingHistory,
857
- error,
858
- threadId,
859
- userId,
860
- sendMessage,
861
- clearMessages,
862
- stopStreaming
863
- };
864
- }
865
- __name(useAIChat, "useAIChat");
866
- var STORAGE_KEY_MODE = "djangocfg-ai-chat-mode";
867
- var AIChatContext = createContext(null);
868
- function AIChatProvider({
869
- children,
870
- apiEndpoint = mcpEndpoints.chat,
871
- config: userConfig = {},
872
- onError,
873
- enableStreaming = true
874
- }) {
875
- const {
876
- messages,
877
- isLoading,
878
- error,
879
- threadId,
880
- userId,
881
- sendMessage: sendAIMessage,
882
- clearMessages: clearAIMessages,
883
- stopStreaming
884
- } = useAIChat({
885
- apiEndpoint,
886
- onError,
887
- enableStreaming
888
- });
889
- const [isMinimized, setIsMinimized] = useState(false);
890
- const [storedMode, setStoredMode] = useLocalStorage(STORAGE_KEY_MODE, "closed");
891
- const isMobile = useIsMobile();
892
- const displayMode = useMemo(() => {
893
- if (isMobile && storedMode === "sidebar") {
894
- return "floating";
895
- }
896
- return storedMode;
897
- }, [isMobile, storedMode]);
898
- const isOpen = displayMode !== "closed";
899
- const isOpenRef = useRef(isOpen);
900
- useEffect(() => {
901
- isOpenRef.current = isOpen;
902
- }, [isOpen]);
903
- const lastActiveModeRef = useRef("floating");
904
- useEffect(() => {
905
- if (displayMode !== "closed") {
906
- lastActiveModeRef.current = displayMode;
907
- }
908
- }, [displayMode]);
909
- const config = useMemo(
910
- () => ({
911
- apiEndpoint,
912
- title: "DjangoCFG AI",
913
- placeholder: "Ask about DjangoCFG...",
914
- greeting: "Hi! I'm your DjangoCFG AI assistant powered by GPT. Ask me anything about configuration, features, or how to use the library.",
915
- position: "bottom-right",
916
- variant: "default",
917
- ...userConfig
918
- }),
919
- [apiEndpoint, userConfig]
920
- );
921
- const sendMessage = useCallback(
922
- async (content) => {
923
- await sendAIMessage(content);
924
- },
925
- [sendAIMessage]
926
- );
927
- const clearMessages = useCallback(() => {
928
- clearAIMessages();
929
- }, [clearAIMessages]);
930
- const openChat = useCallback(() => {
931
- setStoredMode(lastActiveModeRef.current);
932
- setIsMinimized(false);
933
- }, [setStoredMode]);
934
- const closeChat = useCallback(() => {
935
- setStoredMode("closed");
936
- setIsMinimized(false);
937
- }, [setStoredMode]);
938
- const toggleChat = useCallback(() => {
939
- if (displayMode === "closed") {
940
- setStoredMode("floating");
941
- setIsMinimized(false);
942
- } else {
943
- setStoredMode("closed");
944
- }
945
- }, [displayMode, setStoredMode]);
946
- const toggleMinimize = useCallback(() => {
947
- setIsMinimized((prev) => !prev);
948
- }, []);
949
- const setDisplayMode = useCallback(
950
- (mode) => {
951
- if (isMobile && mode === "sidebar") {
952
- setStoredMode("floating");
953
- } else {
954
- setStoredMode(mode);
955
- }
956
- setIsMinimized(false);
957
- },
958
- [isMobile, setStoredMode]
959
- );
960
- const value = useMemo(
961
- () => ({
962
- messages,
963
- isLoading,
964
- error,
965
- isOpen,
966
- isMinimized,
967
- config,
968
- displayMode,
969
- isMobile,
970
- threadId,
971
- userId,
972
- sendMessage,
973
- clearMessages,
974
- openChat,
975
- closeChat,
976
- toggleChat,
977
- toggleMinimize,
978
- setDisplayMode,
979
- stopStreaming
980
- }),
981
- [
982
- messages,
983
- isLoading,
984
- error,
985
- isOpen,
986
- isMinimized,
987
- config,
988
- displayMode,
989
- isMobile,
990
- threadId,
991
- userId,
992
- sendMessage,
993
- clearMessages,
994
- openChat,
995
- closeChat,
996
- toggleChat,
997
- toggleMinimize,
998
- setDisplayMode,
999
- stopStreaming
1000
- ]
1001
- );
1002
- useEffect(() => {
1003
- if (typeof window !== "undefined") {
1004
- window.__MCP_CHAT_AVAILABLE__ = true;
1005
- }
1006
- const handleChatEvent = /* @__PURE__ */ __name((event) => {
1007
- const customEvent = event;
1008
- const { message, context, autoSend = true, displayMode: requestedMode } = customEvent.detail;
1009
- window.dispatchEvent(new CustomEvent("mcp:chat:handled"));
1010
- let fullMessage = message;
1011
- if (context) {
1012
- if (context.type) {
1013
- fullMessage = `[${context.type.toUpperCase()}] ${message}`;
1014
- }
1015
- if (context.data) {
1016
- fullMessage += `
1017
-
1018
- **Context:**
1019
- \`\`\`json
1020
- ${JSON.stringify(context.data, null, 2)}
1021
- \`\`\``;
1022
- }
1023
- if (context.source) {
1024
- fullMessage += `
1025
-
1026
- _Source: ${context.source}_`;
1027
- }
1028
- }
1029
- if (requestedMode) {
1030
- setDisplayMode(requestedMode);
1031
- } else if (!isOpenRef.current) {
1032
- openChat();
1033
- }
1034
- if (autoSend) {
1035
- setTimeout(() => {
1036
- sendMessage(fullMessage);
1037
- }, 100);
1038
- }
1039
- }, "handleChatEvent");
1040
- window.addEventListener("mcp:chat:send", handleChatEvent);
1041
- return () => {
1042
- window.removeEventListener("mcp:chat:send", handleChatEvent);
1043
- if (typeof window !== "undefined") {
1044
- window.__MCP_CHAT_AVAILABLE__ = false;
1045
- }
1046
- };
1047
- }, [sendMessage, setDisplayMode, openChat]);
1048
- return /* @__PURE__ */ jsx(AIChatContext.Provider, { value, children });
1049
- }
1050
- __name(AIChatProvider, "AIChatProvider");
1051
- function useAIChatContext() {
1052
- const context = useContext(AIChatContext);
1053
- if (!context) {
1054
- throw new Error("useAIChatContext must be used within an AIChatProvider");
1055
- }
1056
- return context;
1057
- }
1058
- __name(useAIChatContext, "useAIChatContext");
1059
- function useAIChatContextOptional() {
1060
- return useContext(AIChatContext);
1061
- }
1062
- __name(useAIChatContextOptional, "useAIChatContextOptional");
1063
- var MIN_SIDEBAR_WIDTH = sidebarConfig.minWidth;
1064
- var MAX_SIDEBAR_WIDTH = sidebarConfig.maxWidth;
1065
- var DEFAULT_CONFIG = {
1066
- initialWidth: sidebarConfig.defaultWidth,
1067
- animationDuration: sidebarConfig.animationDuration,
1068
- pushTarget: "body"
1069
- };
1070
- function useChatLayout(config) {
1071
- const mergedConfig = { ...DEFAULT_CONFIG, ...config };
1072
- const { initialWidth, animationDuration, pushTarget } = mergedConfig;
1073
- const [storedWidth, setStoredWidth] = useLocalStorage(storageKeys.sidebarWidth, initialWidth);
1074
- const sidebarWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, storedWidth));
1075
- const sidebarWidthRef = useRef(sidebarWidth);
1076
- const [isResizing, setIsResizing] = useState(false);
1077
- useEffect(() => {
1078
- sidebarWidthRef.current = sidebarWidth;
1079
- }, [sidebarWidth]);
1080
- const originalStylesRef = useRef(null);
1081
- const fixedElementsRef = useRef([]);
1082
- const currentModeRef = useRef("closed");
1083
- const getTargetElement = useCallback(() => {
1084
- if (typeof window === "undefined") return null;
1085
- if (pushTarget === "body") {
1086
- return document.body;
1087
- } else if (pushTarget === "main") {
1088
- return document.querySelector("main");
1089
- } else {
1090
- return document.querySelector(pushTarget);
1091
- }
1092
- }, [pushTarget]);
1093
- const getFixedElements = useCallback(() => {
1094
- if (typeof window === "undefined") return [];
1095
- const elements = [];
1096
- const allElements = document.querySelectorAll("*");
1097
- allElements.forEach((el) => {
1098
- if (!(el instanceof HTMLElement)) return;
1099
- if (el.closest("[data-chat-sidebar-panel]")) return;
1100
- const style = window.getComputedStyle(el);
1101
- const position = style.position;
1102
- const right = style.right;
1103
- if ((position === "fixed" || position === "sticky") && right === "0px") {
1104
- elements.push(el);
1105
- }
1106
- });
1107
- return elements;
1108
- }, []);
1109
- const saveOriginalStyles = useCallback((element) => {
1110
- if (!originalStylesRef.current) {
1111
- originalStylesRef.current = {
1112
- marginRight: element.style.marginRight,
1113
- overflowX: element.style.overflowX,
1114
- transition: element.style.transition
1115
- };
1116
- }
1117
- }, []);
1118
- const restoreOriginalStyles = useCallback((element) => {
1119
- if (originalStylesRef.current) {
1120
- element.style.marginRight = originalStylesRef.current.marginRight || "";
1121
- element.style.overflowX = originalStylesRef.current.overflowX || "";
1122
- element.style.transition = originalStylesRef.current.transition || "";
1123
- element.removeAttribute("data-chat-sidebar");
1124
- originalStylesRef.current = null;
1125
- }
1126
- }, []);
1127
- const adjustFixedElements = useCallback(
1128
- (open) => {
1129
- const currentWidth = sidebarWidthRef.current;
1130
- if (open) {
1131
- const fixedElements = getFixedElements();
1132
- fixedElementsRef.current = fixedElements.map((el) => ({
1133
- element: el,
1134
- right: el.style.right,
1135
- transition: el.style.transition
1136
- }));
1137
- fixedElements.forEach((el) => {
1138
- el.style.transition = `right ${animationDuration}ms ease`;
1139
- el.style.right = `${currentWidth}px`;
1140
- });
1141
- } else {
1142
- fixedElementsRef.current.forEach(({ element, right, transition }) => {
1143
- element.style.transition = `right ${animationDuration}ms ease`;
1144
- element.style.right = "0px";
1145
- setTimeout(() => {
1146
- element.style.right = right;
1147
- element.style.transition = transition;
1148
- }, animationDuration);
1149
- });
1150
- fixedElementsRef.current = [];
1151
- }
1152
- },
1153
- [getFixedElements, animationDuration]
1154
- );
1155
- const applySidebarLayout = useCallback(() => {
1156
- const target = getTargetElement();
1157
- if (!target) return;
1158
- const currentWidth = sidebarWidthRef.current;
1159
- saveOriginalStyles(target);
1160
- target.style.transition = `margin-right ${animationDuration}ms ease`;
1161
- target.style.marginRight = `${currentWidth}px`;
1162
- target.style.overflowX = "hidden";
1163
- target.setAttribute("data-chat-sidebar", "open");
1164
- adjustFixedElements(true);
1165
- currentModeRef.current = "sidebar";
1166
- }, [getTargetElement, saveOriginalStyles, animationDuration, adjustFixedElements]);
1167
- const applyDefaultLayout = useCallback(
1168
- (mode) => {
1169
- const target = getTargetElement();
1170
- if (!target) return;
1171
- if (currentModeRef.current === "sidebar") {
1172
- target.style.transition = `margin-right ${animationDuration}ms ease`;
1173
- target.style.marginRight = "0px";
1174
- adjustFixedElements(false);
1175
- setTimeout(() => {
1176
- restoreOriginalStyles(target);
1177
- }, animationDuration);
1178
- }
1179
- currentModeRef.current = mode;
1180
- },
1181
- [getTargetElement, restoreOriginalStyles, animationDuration, adjustFixedElements]
1182
- );
1183
- const applyLayout = useCallback(
1184
- (mode) => {
1185
- if (mode === "sidebar") {
1186
- applySidebarLayout();
1187
- } else {
1188
- applyDefaultLayout(mode);
1189
- }
1190
- },
1191
- [applySidebarLayout, applyDefaultLayout]
1192
- );
1193
- const resetLayout = useCallback(() => {
1194
- const target = getTargetElement();
1195
- if (target && originalStylesRef.current) {
1196
- restoreOriginalStyles(target);
1197
- }
1198
- fixedElementsRef.current.forEach(({ element, right, transition }) => {
1199
- element.style.right = right;
1200
- element.style.transition = transition;
1201
- });
1202
- fixedElementsRef.current = [];
1203
- currentModeRef.current = "closed";
1204
- }, [getTargetElement, restoreOriginalStyles]);
1205
- const updateWidthImmediate = useCallback(
1206
- (newWidth) => {
1207
- const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, newWidth));
1208
- const target = getTargetElement();
1209
- if (target && currentModeRef.current === "sidebar") {
1210
- target.style.transition = "none";
1211
- target.style.marginRight = `${clampedWidth}px`;
1212
- }
1213
- fixedElementsRef.current.forEach(({ element }) => {
1214
- element.style.transition = "none";
1215
- element.style.right = `${clampedWidth}px`;
1216
- });
1217
- return clampedWidth;
1218
- },
1219
- [getTargetElement]
1220
- );
1221
- const updateWidth = useCallback(
1222
- (newWidth) => {
1223
- const clampedWidth = updateWidthImmediate(newWidth);
1224
- setStoredWidth(clampedWidth);
1225
- },
1226
- [updateWidthImmediate, setStoredWidth]
1227
- );
1228
- const startResize = useCallback(
1229
- (e) => {
1230
- e.preventDefault();
1231
- setIsResizing(true);
1232
- const startX = e.clientX;
1233
- const startWidth = sidebarWidthRef.current;
1234
- const handleMouseMove = /* @__PURE__ */ __name((moveEvent) => {
1235
- const deltaX = startX - moveEvent.clientX;
1236
- const newWidth = startWidth + deltaX;
1237
- const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, newWidth));
1238
- updateWidthImmediate(clampedWidth);
1239
- sidebarWidthRef.current = clampedWidth;
1240
- setStoredWidth(clampedWidth);
1241
- }, "handleMouseMove");
1242
- const handleMouseUp = /* @__PURE__ */ __name(() => {
1243
- setIsResizing(false);
1244
- document.removeEventListener("mousemove", handleMouseMove);
1245
- document.removeEventListener("mouseup", handleMouseUp);
1246
- document.body.style.cursor = "";
1247
- document.body.style.userSelect = "";
1248
- }, "handleMouseUp");
1249
- document.addEventListener("mousemove", handleMouseMove);
1250
- document.addEventListener("mouseup", handleMouseUp);
1251
- document.body.style.cursor = "ew-resize";
1252
- document.body.style.userSelect = "none";
1253
- },
1254
- [updateWidthImmediate, setStoredWidth]
1255
- );
1256
- const getSidebarStyles = useCallback(() => {
1257
- return {
1258
- position: "fixed",
1259
- top: 0,
1260
- right: 0,
1261
- bottom: 0,
1262
- width: `${sidebarWidth}px`,
1263
- zIndex: sidebarConfig.zIndex
1264
- };
1265
- }, [sidebarWidth]);
1266
- const getFloatingStyles = useCallback(
1267
- (position) => {
1268
- return {
1269
- position: "fixed",
1270
- zIndex: sidebarConfig.zIndex - 50,
1271
- bottom: fabConfig.bottom,
1272
- ...position === "bottom-right" ? { right: fabConfig.right } : { left: fabConfig.right }
1273
- };
1274
- },
1275
- []
1276
- );
1277
- const getFabStyles = useCallback(
1278
- (position) => {
1279
- return {
1280
- position: "fixed",
1281
- zIndex: sidebarConfig.zIndex - 50,
1282
- bottom: fabConfig.bottom,
1283
- ...position === "bottom-right" ? { right: fabConfig.right } : { left: fabConfig.right }
1284
- };
1285
- },
1286
- []
1287
- );
1288
- useEffect(() => {
1289
- return () => {
1290
- resetLayout();
1291
- };
1292
- }, [resetLayout]);
1293
- return {
1294
- sidebarWidth,
1295
- applyLayout,
1296
- resetLayout,
1297
- updateWidth,
1298
- startResize,
1299
- isResizing,
1300
- getSidebarStyles,
1301
- getFloatingStyles,
1302
- getFabStyles
1303
- };
1304
- }
1305
- __name(useChatLayout, "useChatLayout");
1306
- function formatTime(date) {
1307
- return date.toLocaleTimeString("en-US", {
1308
- hour: "2-digit",
1309
- minute: "2-digit"
1310
- });
1311
- }
1312
- __name(formatTime, "formatTime");
1313
- var MessageBubble = React3.memo(
1314
- ({ message, isCompact = false }) => {
1315
- const isUser = message.role === "user";
1316
- const isAssistant = message.role === "assistant";
1317
- const { user, isAuthenticated } = useAuth();
1318
- const showUserAvatar = isUser && isAuthenticated && user;
1319
- const userAvatar = user?.avatar || "";
1320
- const userDisplayName = user?.display_username || user?.email || "User";
1321
- const userInitial = userDisplayName.charAt(0).toUpperCase();
1322
- const avatarSize = isCompact ? "28px" : "36px";
1323
- const iconSize = isCompact ? "h-3.5 w-3.5" : "h-4 w-4";
1324
- return /* @__PURE__ */ jsxs(
1325
- "div",
1326
- {
1327
- className: `flex gap-3 animate-in fade-in slide-in-from-bottom-2 duration-300 max-w-full overflow-hidden ${isUser ? "flex-row-reverse" : ""}`,
1328
- children: [
1329
- showUserAvatar ? (
1330
- // Authenticated user avatar
1331
- /* @__PURE__ */ jsxs(Avatar, { className: "flex-shrink-0", style: { width: avatarSize, height: avatarSize }, children: [
1332
- /* @__PURE__ */ jsx(AvatarImage, { src: userAvatar, alt: userDisplayName }),
1333
- /* @__PURE__ */ jsx(AvatarFallback, { className: "bg-primary text-primary-foreground text-xs", children: userInitial })
1334
- ] })
1335
- ) : isUser ? (
1336
- // Guest user icon
1337
- /* @__PURE__ */ jsx(
1338
- "div",
1339
- {
1340
- className: "flex-shrink-0 rounded-full flex items-center justify-center bg-primary text-primary-foreground",
1341
- style: { width: avatarSize, height: avatarSize },
1342
- children: /* @__PURE__ */ jsx(User, { className: iconSize })
1343
- }
1344
- )
1345
- ) : (
1346
- // Bot icon
1347
- /* @__PURE__ */ jsx(
1348
- "div",
1349
- {
1350
- className: "flex-shrink-0 rounded-full flex items-center justify-center bg-muted text-muted-foreground",
1351
- style: { width: avatarSize, height: avatarSize },
1352
- children: /* @__PURE__ */ jsx(Bot, { className: iconSize })
1353
- }
1354
- )
1355
- ),
1356
- /* @__PURE__ */ jsxs("div", { className: `flex-1 min-w-0 ${isUser ? "max-w-[80%] ml-auto" : "max-w-[85%]"}`, children: [
1357
- /* @__PURE__ */ jsxs("div", { className: `flex items-baseline gap-2 mb-1 ${isUser ? "justify-end" : ""}`, children: [
1358
- /* @__PURE__ */ jsx("span", { className: `font-medium ${isCompact ? "text-xs" : "text-sm"}`, children: isUser ? userDisplayName : "DjangoCFG AI" }),
1359
- /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: formatTime(message.timestamp) })
1360
- ] }),
1361
- /* @__PURE__ */ jsx(
1362
- Card,
1363
- {
1364
- className: `transition-all duration-200 ${isUser ? "bg-primary text-primary-foreground ml-auto" : "bg-muted"}`,
1365
- children: /* @__PURE__ */ jsxs(CardContent, { className: isCompact ? "p-2" : "p-3", children: [
1366
- /* @__PURE__ */ jsxs("div", { className: `${isCompact ? "text-xs" : "text-sm"} overflow-hidden`, style: { overflowWrap: "anywhere", wordBreak: "break-word" }, children: [
1367
- /* @__PURE__ */ jsx(
1368
- MarkdownMessage,
1369
- {
1370
- content: message.content,
1371
- isUser,
1372
- isCompact
1373
- }
1374
- ),
1375
- message.isStreaming && /* @__PURE__ */ jsx(Loader2, { className: "inline-block ml-1 h-3 w-3 animate-spin" })
1376
- ] }),
1377
- isAssistant && message.sources && message.sources.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-3 pt-2 border-t border-border/50", children: [
1378
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mb-1.5", children: "Related docs:" }),
1379
- /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: message.sources.slice(0, 3).map((source, idx) => /* @__PURE__ */ jsx(
1380
- "a",
1381
- {
1382
- href: source.url || source.path,
1383
- target: "_blank",
1384
- rel: "noopener noreferrer",
1385
- className: "inline-block",
1386
- children: /* @__PURE__ */ jsxs(
1387
- Badge,
1388
- {
1389
- variant: "outline",
1390
- className: "text-xs gap-1 hover:bg-primary/10 transition-colors cursor-pointer",
1391
- children: [
1392
- source.title,
1393
- source.section && ` - ${source.section}`,
1394
- /* @__PURE__ */ jsx(ExternalLink, { className: "h-2.5 w-2.5" })
1395
- ]
1396
- }
1397
- )
1398
- },
1399
- idx
1400
- )) })
1401
- ] })
1402
- ] })
1403
- }
1404
- )
1405
- ] })
1406
- ]
1407
- }
1408
- );
1409
- }
1410
- );
1411
- MessageBubble.displayName = "MessageBubble";
1412
- var ChatMessages = forwardRef(
1413
- ({
1414
- messages,
1415
- isLoading,
1416
- greeting,
1417
- onStopStreaming,
1418
- isCompact = false,
1419
- largeGreetingIcon = false,
1420
- greetingIcon = "bot",
1421
- greetingTitle
1422
- }, ref) => {
1423
- const scrollContainerRef = useRef(null);
1424
- useImperativeHandle(ref, () => ({
1425
- scrollToBottom: /* @__PURE__ */ __name(() => {
1426
- scrollContainerRef.current?.scrollTo({ top: 0, behavior: "smooth" });
1427
- }, "scrollToBottom"),
1428
- scrollToLastMessage: /* @__PURE__ */ __name(() => {
1429
- scrollContainerRef.current?.scrollTo({ top: 0, behavior: "smooth" });
1430
- }, "scrollToLastMessage")
1431
- }), []);
1432
- const GreetingIcon = greetingIcon === "message" ? MessageSquare : Bot;
1433
- const iconSize = largeGreetingIcon ? { container: "64px", icon: "h-8 w-8" } : { container: "48px", icon: "h-6 w-6" };
1434
- const padding = largeGreetingIcon ? "py-12" : "py-8";
1435
- return /* @__PURE__ */ jsx("div", { ref: scrollContainerRef, className: "h-full w-full overflow-y-auto flex flex-col-reverse", children: /* @__PURE__ */ jsxs("div", { className: `${isCompact ? "p-3" : "p-4"} space-y-4 max-w-full overflow-x-hidden`, children: [
1436
- messages.length === 0 && greeting && /* @__PURE__ */ jsxs("div", { className: `text-center ${padding}`, children: [
1437
- /* @__PURE__ */ jsx(
1438
- "div",
1439
- {
1440
- className: "mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center",
1441
- style: { width: iconSize.container, height: iconSize.container },
1442
- children: /* @__PURE__ */ jsx(GreetingIcon, { className: `${iconSize.icon} text-primary` })
1443
- }
1444
- ),
1445
- greetingTitle && /* @__PURE__ */ jsx("h4", { className: "font-medium mb-2", children: greetingTitle }),
1446
- /* @__PURE__ */ jsx("p", { className: `text-sm text-muted-foreground ${largeGreetingIcon ? "max-w-[300px]" : "max-w-[280px]"} mx-auto`, children: greeting })
1447
- ] }),
1448
- messages.map((message) => /* @__PURE__ */ jsx("div", { "data-message-bubble": true, children: /* @__PURE__ */ jsx(MessageBubble, { message, isCompact }) }, message.id)),
1449
- isLoading && messages.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-muted-foreground text-sm", children: [
1450
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1451
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
1452
- /* @__PURE__ */ jsx("span", { className: "animate-bounce", style: { animationDelay: "0ms" }, children: "." }),
1453
- /* @__PURE__ */ jsx("span", { className: "animate-bounce", style: { animationDelay: "150ms" }, children: "." }),
1454
- /* @__PURE__ */ jsx("span", { className: "animate-bounce", style: { animationDelay: "300ms" }, children: "." })
1455
- ] }),
1456
- /* @__PURE__ */ jsx("span", { children: "Generating response..." })
1457
- ] }),
1458
- onStopStreaming && /* @__PURE__ */ jsxs(
1459
- Button,
1460
- {
1461
- variant: "ghost",
1462
- size: "sm",
1463
- onClick: onStopStreaming,
1464
- className: "h-6 px-2 text-xs",
1465
- children: [
1466
- /* @__PURE__ */ jsx(StopCircle, { className: "h-3 w-3 mr-1" }),
1467
- "Stop"
1468
- ]
1469
- }
1470
- )
1471
- ] })
1472
- ] }) });
1473
- }
1474
- );
1475
- ChatMessages.displayName = "ChatMessages";
1476
- var AIMessageInput = React3.memo(
1477
- ({
1478
- onSend,
1479
- disabled = false,
1480
- isLoading = false,
1481
- placeholder = "Ask about DjangoCFG...",
1482
- maxRows = 5
1483
- }) => {
1484
- const [value, setValue] = useState("");
1485
- const textareaRef = useRef(null);
1486
- const adjustHeight = useCallback(() => {
1487
- const textarea = textareaRef.current;
1488
- if (!textarea) return;
1489
- textarea.style.height = "auto";
1490
- const lineHeight = 24;
1491
- const minHeight = 44;
1492
- const maxHeight = lineHeight * maxRows + 20;
1493
- const newHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
1494
- textarea.style.height = `${newHeight}px`;
1495
- }, [maxRows]);
1496
- useEffect(() => {
1497
- adjustHeight();
1498
- }, [value, adjustHeight]);
1499
- const handleSubmit = useCallback(
1500
- (e) => {
1501
- e?.preventDefault();
1502
- const trimmed = value.trim();
1503
- if (!trimmed || disabled || isLoading) return;
1504
- onSend(trimmed);
1505
- setValue("");
1506
- if (textareaRef.current) {
1507
- textareaRef.current.style.height = "auto";
1508
- }
1509
- textareaRef.current?.focus();
1510
- },
1511
- [value, disabled, isLoading, onSend]
1512
- );
1513
- const handleKeyDown = useCallback(
1514
- (e) => {
1515
- if (e.key === "Enter" && !e.shiftKey) {
1516
- e.preventDefault();
1517
- handleSubmit();
1518
- }
1519
- },
1520
- [handleSubmit]
1521
- );
1522
- const canSend = value.trim().length > 0 && !disabled && !isLoading;
1523
- return /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "w-full", children: [
1524
- /* @__PURE__ */ jsxs(
1525
- "div",
1526
- {
1527
- className: "relative flex items-end rounded-2xl border border-input bg-background transition-colors focus-within:ring-1 focus-within:ring-ring focus-within:border-ring",
1528
- style: { minHeight: "44px" },
1529
- children: [
1530
- /* @__PURE__ */ jsx(
1531
- "textarea",
1532
- {
1533
- ref: textareaRef,
1534
- value,
1535
- onChange: (e) => setValue(e.target.value),
1536
- onKeyDown: handleKeyDown,
1537
- placeholder,
1538
- disabled: disabled || isLoading,
1539
- rows: 1,
1540
- className: "flex-1 resize-none bg-transparent px-4 py-3 text-sm placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 pr-12",
1541
- style: {
1542
- minHeight: "44px",
1543
- maxHeight: `${24 * maxRows + 20}px`,
1544
- lineHeight: "1.5rem"
1545
- },
1546
- autoComplete: "off"
1547
- }
1548
- ),
1549
- /* @__PURE__ */ jsx(
1550
- "div",
1551
- {
1552
- className: "absolute flex items-center justify-center",
1553
- style: {
1554
- right: "6px",
1555
- bottom: "6px"
1556
- },
1557
- children: /* @__PURE__ */ jsx(
1558
- Button,
1559
- {
1560
- type: "submit",
1561
- size: "icon",
1562
- disabled: !canSend,
1563
- className: "h-8 w-8 rounded-full transition-all",
1564
- style: {
1565
- opacity: canSend ? 1 : 0.5
1566
- },
1567
- children: isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Send, { className: "h-4 w-4" })
1568
- }
1569
- )
1570
- }
1571
- )
1572
- ]
1573
- }
1574
- ),
1575
- /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-xs text-muted-foreground text-center", children: "Press Enter to send, Shift+Enter for new line" })
1576
- ] });
1577
- }
1578
- );
1579
- AIMessageInput.displayName = "AIMessageInput";
1580
- var ChatPanel = React3.memo(() => {
1581
- const {
1582
- messages,
1583
- isLoading,
1584
- config,
1585
- isMobile,
1586
- sendMessage,
1587
- closeChat,
1588
- setDisplayMode,
1589
- stopStreaming,
1590
- clearMessages
1591
- } = useAIChatContext();
1592
- const panelStyles = isMobile ? {
1593
- position: "absolute",
1594
- top: 0,
1595
- left: 0,
1596
- right: 0,
1597
- bottom: 0,
1598
- width: "100%",
1599
- height: "100%",
1600
- maxHeight: "100dvh",
1601
- borderRadius: 0,
1602
- display: "flex",
1603
- flexDirection: "column",
1604
- margin: 0,
1605
- border: "none"
1606
- } : {
1607
- width: "380px",
1608
- height: "520px",
1609
- maxHeight: "calc(100vh - 100px)"
1610
- };
1611
- return /* @__PURE__ */ jsxs(
1612
- Card,
1613
- {
1614
- className: `flex flex-col ${isMobile ? "rounded-none border-0 shadow-none" : "shadow-2xl border-border/50"}`,
1615
- style: panelStyles,
1616
- children: [
1617
- /* @__PURE__ */ jsxs(CardHeader, { className: "flex flex-row items-center justify-between p-3 border-b", children: [
1618
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1619
- /* @__PURE__ */ jsx(
1620
- "div",
1621
- {
1622
- className: "rounded-full bg-primary/10 flex items-center justify-center",
1623
- style: { width: "32px", height: "32px" },
1624
- children: /* @__PURE__ */ jsx(Bot, { className: "h-4 w-4 text-primary" })
1625
- }
1626
- ),
1627
- /* @__PURE__ */ jsxs("div", { children: [
1628
- /* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm", children: config.title || "DjangoCFG AI" }),
1629
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "AI Assistant" })
1630
- ] })
1631
- ] }),
1632
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
1633
- messages.length > 0 && /* @__PURE__ */ jsx(
1634
- Button,
1635
- {
1636
- variant: "ghost",
1637
- size: "icon",
1638
- className: "h-8 w-8",
1639
- onClick: clearMessages,
1640
- title: "New chat",
1641
- children: /* @__PURE__ */ jsx(RotateCcw, { className: "h-4 w-4" })
1642
- }
1643
- ),
1644
- !isMobile && /* @__PURE__ */ jsx(
1645
- Button,
1646
- {
1647
- variant: "ghost",
1648
- size: "icon",
1649
- className: "h-8 w-8",
1650
- onClick: () => setDisplayMode("sidebar"),
1651
- title: "Switch to sidebar mode",
1652
- children: /* @__PURE__ */ jsx(PanelRight, { className: "h-4 w-4" })
1653
- }
1654
- ),
1655
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8", onClick: closeChat, title: "Close", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
1656
- ] })
1657
- ] }),
1658
- /* @__PURE__ */ jsx(CardContent, { className: "flex-1 p-0 overflow-hidden", children: /* @__PURE__ */ jsx(
1659
- ChatMessages,
1660
- {
1661
- messages,
1662
- isLoading,
1663
- greeting: config.greeting,
1664
- onStopStreaming: stopStreaming,
1665
- isCompact: true,
1666
- greetingIcon: "bot"
1667
- }
1668
- ) }),
1669
- /* @__PURE__ */ jsx(CardFooter, { className: "p-3 border-t", children: /* @__PURE__ */ jsx(
1670
- AIMessageInput,
1671
- {
1672
- onSend: sendMessage,
1673
- isLoading,
1674
- placeholder: config.placeholder
1675
- }
1676
- ) })
1677
- ]
1678
- }
1679
- );
1680
- });
1681
- ChatPanel.displayName = "ChatPanel";
1682
- var ChatSidebar = React3.memo(({
1683
- resizeHandleWidth = 12,
1684
- showResizeIcon = true,
1685
- resizeHandleClassName
1686
- }) => {
1687
- const {
1688
- messages,
1689
- isLoading,
1690
- config,
1691
- sendMessage,
1692
- closeChat,
1693
- setDisplayMode,
1694
- stopStreaming,
1695
- clearMessages
1696
- } = useAIChatContext();
1697
- const { applyLayout, getSidebarStyles, startResize, isResizing } = useChatLayout();
1698
- useEffect(() => {
1699
- applyLayout("sidebar");
1700
- return () => {
1701
- applyLayout("closed");
1702
- };
1703
- }, []);
1704
- const sidebarStyles = getSidebarStyles();
1705
- return /* @__PURE__ */ jsxs(
1706
- "div",
1707
- {
1708
- className: "flex bg-background",
1709
- style: sidebarStyles,
1710
- "data-chat-sidebar-panel": true,
1711
- children: [
1712
- /* @__PURE__ */ jsx(
1713
- "div",
1714
- {
1715
- className: `
1716
- flex items-center justify-center cursor-ew-resize
1717
- border-l border-border transition-colors select-none flex-shrink-0
1718
- ${isResizing ? "bg-primary/20" : "bg-muted/30 hover:bg-muted/50"}
1719
- ${resizeHandleClassName || ""}
1720
- `,
1721
- style: { width: resizeHandleWidth },
1722
- onMouseDown: startResize,
1723
- title: "Drag to resize",
1724
- children: showResizeIcon && /* @__PURE__ */ jsx(GripVertical, { className: `h-4 w-4 ${isResizing ? "text-primary" : "text-muted-foreground/50"}` })
1725
- }
1726
- ),
1727
- /* @__PURE__ */ jsxs("div", { className: "flex flex-col flex-1 min-w-0", children: [
1728
- /* @__PURE__ */ jsxs(
1729
- "div",
1730
- {
1731
- className: "flex items-center justify-between px-4 border-b border-border bg-muted/30",
1732
- style: { height: "var(--nextra-navbar-height, 64px)", minHeight: "var(--nextra-navbar-height, 64px)" },
1733
- children: [
1734
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1735
- /* @__PURE__ */ jsx(
1736
- "div",
1737
- {
1738
- className: "rounded-full bg-primary/10 flex items-center justify-center",
1739
- style: { width: "32px", height: "32px" },
1740
- children: /* @__PURE__ */ jsx(Bot, { className: "h-4 w-4 text-primary" })
1741
- }
1742
- ),
1743
- /* @__PURE__ */ jsxs("div", { children: [
1744
- /* @__PURE__ */ jsx("h3", { className: "font-semibold text-sm", children: config.title || "DjangoCFG AI" }),
1745
- /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: "AI Assistant" })
1746
- ] })
1747
- ] }),
1748
- /* @__PURE__ */ jsxs("div", { className: "flex gap-1", children: [
1749
- messages.length > 0 && /* @__PURE__ */ jsx(
1750
- Button,
1751
- {
1752
- variant: "ghost",
1753
- size: "icon",
1754
- className: "h-8 w-8",
1755
- onClick: clearMessages,
1756
- title: "New chat",
1757
- children: /* @__PURE__ */ jsx(RotateCcw, { className: "h-4 w-4" })
1758
- }
1759
- ),
1760
- /* @__PURE__ */ jsx(
1761
- Button,
1762
- {
1763
- variant: "ghost",
1764
- size: "icon",
1765
- className: "h-8 w-8",
1766
- onClick: () => setDisplayMode("floating"),
1767
- title: "Switch to floating mode",
1768
- children: /* @__PURE__ */ jsx(PanelRightClose, { className: "h-4 w-4" })
1769
- }
1770
- ),
1771
- /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8", onClick: closeChat, title: "Close chat", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
1772
- ] })
1773
- ]
1774
- }
1775
- ),
1776
- /* @__PURE__ */ jsx("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ jsx(
1777
- ChatMessages,
1778
- {
1779
- messages,
1780
- isLoading,
1781
- greeting: config.greeting,
1782
- onStopStreaming: stopStreaming,
1783
- isCompact: false,
1784
- largeGreetingIcon: true,
1785
- greetingIcon: "message",
1786
- greetingTitle: "How can I help?"
1787
- }
1788
- ) }),
1789
- /* @__PURE__ */ jsx("div", { className: "p-4 border-t border-border bg-muted/30", children: /* @__PURE__ */ jsx(AIMessageInput, { onSend: sendMessage, isLoading, placeholder: config.placeholder }) })
1790
- ] })
1791
- ]
1792
- }
1793
- );
1794
- });
1795
- ChatSidebar.displayName = "ChatSidebar";
1796
- var ChatWidgetInternal = /* @__PURE__ */ __name(({ className }) => {
1797
- const { config, displayMode, openChat, isMobile } = useChatContext();
1798
- const { getFabStyles, getFloatingStyles } = useChatLayout();
1799
- const position = config.position || "bottom-right";
1800
- const fabStyles = getFabStyles(position);
1801
- const floatingStyles = getFloatingStyles(position);
1802
- const mobileFullscreenStyles = {
1803
- position: "fixed",
1804
- inset: 0,
1805
- height: "100dvh",
1806
- width: "100vw",
1807
- zIndex: 400,
1808
- // Higher z-index for mobile overlay
1809
- overflow: "hidden"
1810
- };
1811
- if (displayMode === "closed") {
1812
- return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx("div", { style: fabStyles, className: className || "", children: /* @__PURE__ */ jsx(
1813
- Button,
1814
- {
1815
- onClick: openChat,
1816
- className: "rounded-full shadow-lg hover:shadow-xl transition-shadow",
1817
- style: { width: "56px", height: "56px" },
1818
- children: /* @__PURE__ */ jsx(MessageCircle, { className: "h-6 w-6" })
1819
- }
1820
- ) }) });
1821
- }
1822
- if (displayMode === "sidebar") {
1823
- return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(ChatSidebar, {}) });
1824
- }
1825
- return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx("div", { style: isMobile ? mobileFullscreenStyles : floatingStyles, className: className || "", children: /* @__PURE__ */ jsx(ChatPanel, {}) }) });
1826
- }, "ChatWidgetInternal");
1827
- var ChatWidget = /* @__PURE__ */ __name(({
1828
- apiEndpoint = "/api/chat",
1829
- title = "DjangoCFG AI",
1830
- placeholder = "Ask about DjangoCFG...",
1831
- greeting = "Hi! I'm your DjangoCFG documentation assistant. Ask me anything about configuration, features, or how to use the library.",
1832
- position = "bottom-right",
1833
- variant = "default",
1834
- className
1835
- }) => {
1836
- const existingContext = useChatContextOptional();
1837
- if (existingContext) {
1838
- return /* @__PURE__ */ jsx(ChatWidgetInternal, { className });
1839
- }
1840
- return /* @__PURE__ */ jsx(
1841
- ChatProvider,
1842
- {
1843
- apiEndpoint,
1844
- config: { title, placeholder, greeting, position, variant },
1845
- children: /* @__PURE__ */ jsx(ChatWidgetInternal, { className })
1846
- }
1847
- );
1848
- }, "ChatWidget");
1849
- ChatWidget.displayName = "ChatWidget";
1850
- var fabAnimationStyles = `
1851
- @keyframes rotate-gradient {
1852
- 0% { transform: rotate(0deg); }
1853
- 100% { transform: rotate(360deg); }
1854
- }
1855
-
1856
- @keyframes rotate-gradient-reverse {
1857
- 0% { transform: rotate(360deg); }
1858
- 100% { transform: rotate(0deg); }
1859
- }
1860
-
1861
- @keyframes color-shift-glow {
1862
- 0%, 100% {
1863
- box-shadow:
1864
- 0 0 20px rgba(251, 191, 36, 0.5),
1865
- 0 0 40px rgba(168, 85, 247, 0.3),
1866
- 0 0 60px rgba(20, 184, 166, 0.2);
1867
- }
1868
- 33% {
1869
- box-shadow:
1870
- 0 0 20px rgba(168, 85, 247, 0.5),
1871
- 0 0 40px rgba(20, 184, 166, 0.3),
1872
- 0 0 60px rgba(251, 191, 36, 0.2);
1873
- }
1874
- 66% {
1875
- box-shadow:
1876
- 0 0 20px rgba(20, 184, 166, 0.5),
1877
- 0 0 40px rgba(236, 72, 153, 0.3),
1878
- 0 0 60px rgba(168, 85, 247, 0.2);
1879
- }
1880
- }
1881
-
1882
- @keyframes icon-pulse {
1883
- 0%, 100% {
1884
- opacity: 1;
1885
- transform: scale(1);
1886
- filter: drop-shadow(0 0 4px rgba(251, 191, 36, 0.7));
1887
- }
1888
- 50% {
1889
- opacity: 0.85;
1890
- transform: scale(1.15);
1891
- filter: drop-shadow(0 0 12px rgba(251, 191, 36, 1));
1892
- }
1893
- }
1894
-
1895
- @keyframes border-pulse {
1896
- 0%, 100% {
1897
- opacity: 1;
1898
- filter: blur(0px);
1899
- }
1900
- 50% {
1901
- opacity: 0.85;
1902
- filter: blur(0.5px);
1903
- }
1904
- }
1905
-
1906
- @keyframes inner-glow-pulse {
1907
- 0%, 100% {
1908
- box-shadow:
1909
- inset 0 0 15px rgba(251, 191, 36, 0.3),
1910
- inset 0 0 25px rgba(168, 85, 247, 0.2);
1911
- }
1912
- 50% {
1913
- box-shadow:
1914
- inset 0 0 20px rgba(168, 85, 247, 0.35),
1915
- inset 0 0 30px rgba(20, 184, 166, 0.25);
1916
- }
1917
- }
1918
-
1919
- @keyframes fab-entrance {
1920
- 0% {
1921
- transform: scale(0);
1922
- }
1923
- 50% {
1924
- transform: scale(1.08);
1925
- }
1926
- 70% {
1927
- transform: scale(0.98);
1928
- }
1929
- 85% {
1930
- transform: scale(1.02);
1931
- }
1932
- 100% {
1933
- transform: scale(1);
1934
- }
1935
- }
1936
-
1937
- @keyframes fab-glow-entrance {
1938
- 0% {
1939
- box-shadow: 0 0 0 rgba(251, 191, 36, 0);
1940
- }
1941
- 40% {
1942
- box-shadow:
1943
- 0 0 25px rgba(251, 191, 36, 0.6),
1944
- 0 0 50px rgba(168, 85, 247, 0.4),
1945
- 0 0 75px rgba(20, 184, 166, 0.25);
1946
- }
1947
- 100% {
1948
- box-shadow:
1949
- 0 0 20px rgba(251, 191, 36, 0.5),
1950
- 0 0 40px rgba(168, 85, 247, 0.3),
1951
- 0 0 60px rgba(20, 184, 166, 0.2);
1952
- }
1953
- }
1954
- `;
1955
- var AIChatWidgetInternal = React3.memo(({ className }) => {
1956
- const { config, displayMode, openChat, isMobile } = useAIChatContext();
1957
- const { getFabStyles, getFloatingStyles } = useChatLayout();
1958
- const position = config.position || "bottom-right";
1959
- const fabStyles = getFabStyles(position);
1960
- const floatingStyles = getFloatingStyles(position);
1961
- if (displayMode === "closed") {
1962
- return /* @__PURE__ */ jsxs(Portal, { children: [
1963
- /* @__PURE__ */ jsx("style", { children: fabAnimationStyles }),
1964
- /* @__PURE__ */ jsx("div", { style: fabStyles, className: className || "", children: /* @__PURE__ */ jsx(
1965
- "div",
1966
- {
1967
- className: "relative rounded-full",
1968
- style: {
1969
- width: "68px",
1970
- height: "68px",
1971
- overflow: "hidden",
1972
- animation: "fab-entrance 0.6s cubic-bezier(0.34, 1.45, 0.64, 1) 0s 1 normal forwards, fab-glow-entrance 0.8s ease-out 0s 1 normal forwards, color-shift-glow 8s ease-in-out 0.6s infinite"
1973
- },
1974
- children: /* @__PURE__ */ jsxs(
1975
- "div",
1976
- {
1977
- className: "absolute rounded-full",
1978
- style: {
1979
- inset: "0",
1980
- overflow: "hidden"
1981
- },
1982
- children: [
1983
- /* @__PURE__ */ jsx(
1984
- "div",
1985
- {
1986
- className: "absolute rounded-full",
1987
- style: {
1988
- inset: "0",
1989
- background: `conic-gradient(
1990
- from 0deg,
1991
- rgba(251, 191, 36, 1) 0%,
1992
- rgba(251, 191, 36, 0.7) 8%,
1993
- rgba(251, 191, 36, 0) 15%,
1994
- rgba(168, 85, 247, 0) 20%,
1995
- rgba(168, 85, 247, 0.7) 28%,
1996
- rgba(168, 85, 247, 1) 35%,
1997
- rgba(168, 85, 247, 0.7) 42%,
1998
- rgba(168, 85, 247, 0) 50%,
1999
- rgba(20, 184, 166, 0) 55%,
2000
- rgba(20, 184, 166, 0.7) 63%,
2001
- rgba(20, 184, 166, 1) 70%,
2002
- rgba(20, 184, 166, 0.7) 77%,
2003
- rgba(20, 184, 166, 0) 85%,
2004
- rgba(236, 72, 153, 0) 88%,
2005
- rgba(236, 72, 153, 0.7) 93%,
2006
- rgba(236, 72, 153, 1) 97%,
2007
- rgba(251, 191, 36, 1) 100%
2008
- )`,
2009
- animation: "rotate-gradient 7s linear infinite, border-pulse 4s ease-in-out infinite",
2010
- filter: "blur(1px)",
2011
- opacity: 0.95
2012
- }
2013
- }
2014
- ),
2015
- /* @__PURE__ */ jsx(
2016
- "div",
2017
- {
2018
- className: "absolute rounded-full",
2019
- style: {
2020
- inset: "1px",
2021
- background: `conic-gradient(
2022
- from 180deg,
2023
- rgba(168, 85, 247, 0.85) 0%,
2024
- rgba(168, 85, 247, 0.5) 10%,
2025
- rgba(168, 85, 247, 0) 20%,
2026
- rgba(20, 184, 166, 0) 30%,
2027
- rgba(20, 184, 166, 0.5) 40%,
2028
- rgba(20, 184, 166, 0.85) 50%,
2029
- rgba(20, 184, 166, 0.5) 60%,
2030
- rgba(20, 184, 166, 0) 70%,
2031
- rgba(251, 191, 36, 0) 75%,
2032
- rgba(251, 191, 36, 0.5) 85%,
2033
- rgba(251, 191, 36, 0.85) 95%,
2034
- rgba(168, 85, 247, 0.85) 100%
2035
- )`,
2036
- animation: "rotate-gradient-reverse 9s linear infinite",
2037
- filter: "blur(0.75px)",
2038
- opacity: 0.75
2039
- }
2040
- }
2041
- ),
2042
- /* @__PURE__ */ jsx(
2043
- "div",
2044
- {
2045
- className: "absolute rounded-full bg-background",
2046
- style: {
2047
- inset: "4px",
2048
- animation: "inner-glow-pulse 5s ease-in-out infinite"
2049
- }
2050
- }
2051
- ),
2052
- /* @__PURE__ */ jsx(
2053
- Button,
2054
- {
2055
- onClick: openChat,
2056
- variant: "ghost",
2057
- className: "absolute rounded-full hover:scale-105 transition-all duration-300 bg-background/80 hover:bg-background/95 border-0 backdrop-blur-sm",
2058
- style: {
2059
- inset: "2.5px",
2060
- width: "auto",
2061
- height: "auto"
2062
- },
2063
- children: /* @__PURE__ */ jsx(
2064
- Zap,
2065
- {
2066
- className: "h-6 w-6",
2067
- style: {
2068
- animation: "icon-pulse 2.5s ease-in-out infinite",
2069
- color: "#fbbf24",
2070
- fill: "#fbbf24"
2071
- }
2072
- }
2073
- )
2074
- }
2075
- )
2076
- ]
2077
- }
2078
- )
2079
- }
2080
- ) })
2081
- ] });
2082
- }
2083
- if (displayMode === "sidebar") {
2084
- return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(ChatSidebar, {}) });
2085
- }
2086
- if (isMobile) {
2087
- return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx(
2088
- "div",
2089
- {
2090
- className: "z-[400] overflow-hidden",
2091
- style: {
2092
- position: "fixed",
2093
- top: 0,
2094
- left: 0,
2095
- right: 0,
2096
- bottom: 0,
2097
- width: "100vw",
2098
- height: "100dvh"
2099
- },
2100
- children: /* @__PURE__ */ jsx(ChatPanel, {})
2101
- }
2102
- ) });
2103
- }
2104
- return /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx("div", { style: floatingStyles, className: className || "", children: /* @__PURE__ */ jsx(ChatPanel, {}) }) });
2105
- });
2106
- AIChatWidgetInternal.displayName = "AIChatWidgetInternal";
2107
- var AIChatWidget = /* @__PURE__ */ __name(({
2108
- apiEndpoint,
2109
- title = "DjangoCFG AI",
2110
- placeholder = "Ask about DjangoCFG...",
2111
- greeting = "Hi! I'm your DjangoCFG AI assistant powered by GPT. Ask me anything about configuration, features, or how to use the library.",
2112
- position = "bottom-right",
2113
- variant = "default",
2114
- className,
2115
- enableStreaming = true,
2116
- autoDetectEnvironment = false
2117
- }) => {
2118
- const existingContext = useAIChatContextOptional();
2119
- if (existingContext) {
2120
- return /* @__PURE__ */ jsx(AIChatWidgetInternal, { className });
2121
- }
2122
- const finalApiEndpoint = apiEndpoint || getMcpEndpoints(autoDetectEnvironment).chat;
2123
- return /* @__PURE__ */ jsx(
2124
- AIChatProvider,
2125
- {
2126
- apiEndpoint: finalApiEndpoint,
2127
- config: { title, placeholder, greeting, position, variant, autoDetectEnvironment },
2128
- enableStreaming,
2129
- children: /* @__PURE__ */ jsx(AIChatWidgetInternal, { className })
2130
- }
2131
- );
2132
- }, "AIChatWidget");
2133
- AIChatWidget.displayName = "AIChatWidget";
2134
- function useMcpChat() {
2135
- const sendToChat = useCallback((detail) => {
2136
- if (typeof window === "undefined") {
2137
- console.error("[useMcpChat] Cannot send message: window is not available");
2138
- return;
2139
- }
2140
- const event = new CustomEvent("mcp:chat:send", {
2141
- detail,
2142
- bubbles: true
2143
- });
2144
- let handled = false;
2145
- const handleConfirmation = /* @__PURE__ */ __name(() => {
2146
- handled = true;
2147
- }, "handleConfirmation");
2148
- window.addEventListener("mcp:chat:handled", handleConfirmation, { once: true });
2149
- window.dispatchEvent(event);
2150
- setTimeout(() => {
2151
- window.removeEventListener("mcp:chat:handled", handleConfirmation);
2152
- if (!handled) {
2153
- const errorMessage = "AI Chat is not available. Please make sure the chat component is loaded.";
2154
- console.error("[useMcpChat]", errorMessage);
2155
- if (typeof window !== "undefined" && window.consola) {
2156
- window.consola.error("[useMcpChat] Chat not available");
2157
- }
2158
- alert(errorMessage);
2159
- }
2160
- }, 100);
2161
- }, []);
2162
- const isChatAvailable = useCallback(() => {
2163
- if (typeof window === "undefined") return false;
2164
- return window.__MCP_CHAT_AVAILABLE__ === true;
2165
- }, []);
2166
- return {
2167
- sendToChat,
2168
- isChatAvailable
2169
- };
2170
- }
2171
- __name(useMcpChat, "useMcpChat");
2172
- function AskAIButton({
2173
- message,
2174
- contextData,
2175
- source,
2176
- autoSend = true,
2177
- showIcon = true,
2178
- onSent,
2179
- children = "Ask AI",
2180
- variant = "outline",
2181
- size = "default",
2182
- className,
2183
- ...buttonProps
2184
- }) {
2185
- const { sendToChat } = useMcpChat();
2186
- const handleClick = /* @__PURE__ */ __name(() => {
2187
- const detail = {
2188
- message,
2189
- autoSend
2190
- // No displayMode - chat will use remembered mode automatically
2191
- };
2192
- if (contextData || source) {
2193
- detail.context = {
2194
- data: contextData,
2195
- source
2196
- };
2197
- }
2198
- sendToChat(detail);
2199
- onSent?.();
2200
- }, "handleClick");
2201
- return /* @__PURE__ */ jsxs(
2202
- Button,
2203
- {
2204
- onClick: handleClick,
2205
- variant,
2206
- size,
2207
- className,
2208
- ...buttonProps,
2209
- children: [
2210
- showIcon && /* @__PURE__ */ jsx(Bot, { className: "h-4 w-4 mr-2" }),
2211
- children
2212
- ]
2213
- }
2214
- );
2215
- }
2216
- __name(AskAIButton, "AskAIButton");
2217
-
2218
- // src/snippets/PWAInstall/utils/localStorage.ts
2219
- var STORAGE_KEYS = {
2220
- IOS_GUIDE_DISMISSED: "pwa_ios_guide_dismissed_at",
2221
- APP_INSTALLED: "pwa_app_installed",
2222
- A2HS_DISMISSED: "pwa_a2hs_dismissed_at"
2223
- };
2224
- function clearIOSGuideDismissal() {
2225
- if (typeof window === "undefined") return;
2226
- try {
2227
- localStorage.removeItem(STORAGE_KEYS.IOS_GUIDE_DISMISSED);
2228
- } catch {
2229
- }
2230
- }
2231
- __name(clearIOSGuideDismissal, "clearIOSGuideDismissal");
2232
- function markAppInstalled() {
2233
- if (typeof window === "undefined") return;
2234
- try {
2235
- localStorage.setItem(STORAGE_KEYS.APP_INSTALLED, "true");
2236
- clearIOSGuideDismissal();
2237
- } catch {
2238
- }
2239
- }
2240
- __name(markAppInstalled, "markAppInstalled");
2241
- function isA2HSDismissedRecently(resetDays = 3) {
2242
- return isDismissedRecentlyHelper(resetDays, STORAGE_KEYS.A2HS_DISMISSED);
2243
- }
2244
- __name(isA2HSDismissedRecently, "isA2HSDismissedRecently");
2245
- function markA2HSDismissed() {
2246
- if (typeof window === "undefined") return;
2247
- try {
2248
- localStorage.setItem(STORAGE_KEYS.A2HS_DISMISSED, Date.now().toString());
2249
- } catch {
2250
- }
2251
- }
2252
- __name(markA2HSDismissed, "markA2HSDismissed");
2253
- function isDismissedRecentlyHelper(resetDays, key) {
2254
- if (typeof window === "undefined") return false;
2255
- try {
2256
- const dismissed = localStorage.getItem(key);
2257
- if (!dismissed) return false;
2258
- const dismissedAt = parseInt(dismissed, 10);
2259
- const daysSince = (Date.now() - dismissedAt) / (1e3 * 60 * 60 * 24);
2260
- return daysSince < resetDays;
2261
- } catch {
2262
- return false;
2263
- }
2264
- }
2265
- __name(isDismissedRecentlyHelper, "isDismissedRecentlyHelper");
2266
- function clearAllPWAInstallData() {
2267
- if (typeof window === "undefined") return;
2268
- try {
2269
- localStorage.removeItem(STORAGE_KEYS.IOS_GUIDE_DISMISSED);
2270
- localStorage.removeItem(STORAGE_KEYS.APP_INSTALLED);
2271
- localStorage.removeItem(STORAGE_KEYS.A2HS_DISMISSED);
2272
- } catch {
2273
- }
2274
- }
2275
- __name(clearAllPWAInstallData, "clearAllPWAInstallData");
2276
- function isDebugEnabled() {
2277
- if (typeof window === "undefined") return false;
2278
- try {
2279
- return localStorage.getItem("pwa_debug") === "true";
2280
- } catch {
2281
- return false;
2282
- }
2283
- }
2284
- __name(isDebugEnabled, "isDebugEnabled");
2285
- var pwaLogger = {
2286
- /**
2287
- * Info level logging
2288
- * Only logs in development or when debug is enabled
2289
- */
2290
- info: /* @__PURE__ */ __name((...args) => {
2291
- {
2292
- consola.info(...args);
2293
- }
2294
- }, "info"),
2295
- /**
2296
- * Warning level logging
2297
- * Only logs in development or when debug is enabled
2298
- */
2299
- warn: /* @__PURE__ */ __name((...args) => {
2300
- {
2301
- consola.warn(...args);
2302
- }
2303
- }, "warn"),
2304
- /**
2305
- * Error level logging
2306
- * Always logs (production + development)
2307
- */
2308
- error: /* @__PURE__ */ __name((...args) => {
2309
- consola.error(...args);
2310
- }, "error"),
2311
- /**
2312
- * Debug level logging
2313
- * Only logs when debug is explicitly enabled
2314
- */
2315
- debug: /* @__PURE__ */ __name((...args) => {
2316
- if (isDebugEnabled()) {
2317
- consola.debug(...args);
2318
- }
2319
- }, "debug"),
2320
- /**
2321
- * Success level logging
2322
- * Only logs in development or when debug is enabled
2323
- */
2324
- success: /* @__PURE__ */ __name((...args) => {
2325
- {
2326
- consola.success(...args);
2327
- }
2328
- }, "success")
2329
- };
2330
-
2331
- // src/snippets/PWAInstall/utils/platform.ts
2332
- function isStandalone() {
2333
- if (typeof window === "undefined") return false;
2334
- if (!window.matchMedia) {
2335
- const nav2 = navigator;
2336
- return nav2.standalone === true;
2337
- }
2338
- const isStandaloneDisplay = window.matchMedia("(display-mode: standalone)").matches;
2339
- const nav = navigator;
2340
- const isStandaloneNavigator = nav.standalone === true;
2341
- return isStandaloneDisplay || isStandaloneNavigator;
2342
- }
2343
- __name(isStandalone, "isStandalone");
2344
- function isMobileDevice() {
2345
- if (typeof window === "undefined") return false;
2346
- return /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
2347
- }
2348
- __name(isMobileDevice, "isMobileDevice");
2349
- function hasValidManifest() {
2350
- if (typeof document === "undefined") return false;
2351
- const manifestLink = document.querySelector('link[rel="manifest"]');
2352
- return !!manifestLink;
2353
- }
2354
- __name(hasValidManifest, "hasValidManifest");
2355
- function isStandaloneReliable() {
2356
- const standalone = isStandalone();
2357
- if (!standalone) return false;
2358
- if (isMobileDevice()) return true;
2359
- return hasValidManifest();
2360
- }
2361
- __name(isStandaloneReliable, "isStandaloneReliable");
2362
- function getDisplayMode() {
2363
- if (typeof window === "undefined") return "browser";
2364
- if (!window.matchMedia) return "browser";
2365
- const modes = [
2366
- "fullscreen",
2367
- "standalone",
2368
- "minimal-ui"
2369
- ];
2370
- for (const mode of modes) {
2371
- if (window.matchMedia(`(display-mode: ${mode})`).matches) {
2372
- return mode;
2373
- }
2374
- }
2375
- return "browser";
2376
- }
2377
- __name(getDisplayMode, "getDisplayMode");
2378
- function onDisplayModeChange(callback) {
2379
- if (typeof window === "undefined" || !window.matchMedia) {
2380
- return () => {
2381
- };
2382
- }
2383
- const mediaQuery = window.matchMedia("(display-mode: standalone)");
2384
- const handleChange = /* @__PURE__ */ __name((e) => {
2385
- callback(e.matches);
2386
- }, "handleChange");
2387
- mediaQuery.addEventListener("change", handleChange);
2388
- return () => {
2389
- mediaQuery.removeEventListener("change", handleChange);
2390
- };
2391
- }
2392
- __name(onDisplayModeChange, "onDisplayModeChange");
2393
-
2394
- // src/snippets/PWAInstall/hooks/useInstallPrompt.ts
2395
- function useInstallPrompt() {
2396
- const browser = useBrowserDetect();
2397
- const device = useDeviceDetect();
2398
- const [state, setState] = useState(() => {
2399
- if (typeof window === "undefined") {
2400
- return {
2401
- isIOS: false,
2402
- isAndroid: false,
2403
- isSafari: false,
2404
- isChrome: false,
2405
- isInstalled: false,
2406
- canPrompt: false,
2407
- deferredPrompt: null
2408
- };
2409
- }
2410
- const isSafari = browser.isSafari && !browser.isChromium;
2411
- return {
2412
- isIOS: device.isIOS,
2413
- isAndroid: device.isAndroid,
2414
- isSafari,
2415
- isChrome: browser.isChrome || browser.isChromium,
2416
- isInstalled: isStandalone(),
2417
- canPrompt: false,
2418
- deferredPrompt: null
2419
- };
2420
- });
2421
- useEffect(() => {
2422
- const isSafari = browser.isSafari && !browser.isChromium;
2423
- setState((prev) => ({
2424
- ...prev,
2425
- isIOS: device.isIOS,
2426
- isAndroid: device.isAndroid,
2427
- isSafari,
2428
- isChrome: browser.isChrome || browser.isChromium,
2429
- isInstalled: isStandalone()
2430
- }));
2431
- }, [browser, device]);
2432
- useEffect(() => {
2433
- if (typeof window === "undefined") return;
2434
- const handleBeforeInstallPrompt = /* @__PURE__ */ __name((e) => {
2435
- e.preventDefault();
2436
- const event = e;
2437
- setState((prev) => ({
2438
- ...prev,
2439
- canPrompt: true,
2440
- deferredPrompt: event
2441
- }));
2442
- }, "handleBeforeInstallPrompt");
2443
- window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
2444
- return () => {
2445
- window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt);
2446
- };
2447
- }, []);
2448
- useEffect(() => {
2449
- if (typeof window === "undefined") return;
2450
- const handleAppInstalled = /* @__PURE__ */ __name(() => {
2451
- setState((prev) => ({
2452
- ...prev,
2453
- canPrompt: false,
2454
- deferredPrompt: null,
2455
- isInstalled: true
2456
- }));
2457
- markAppInstalled();
2458
- }, "handleAppInstalled");
2459
- window.addEventListener("appinstalled", handleAppInstalled);
2460
- return () => {
2461
- window.removeEventListener("appinstalled", handleAppInstalled);
2462
- };
2463
- }, []);
2464
- useEffect(() => {
2465
- const cleanup = onDisplayModeChange((isStandaloneMode) => {
2466
- if (isStandaloneMode) {
2467
- setState((prev) => ({
2468
- ...prev,
2469
- isInstalled: true,
2470
- canPrompt: false,
2471
- deferredPrompt: null
2472
- }));
2473
- markAppInstalled();
2474
- }
2475
- });
2476
- return cleanup;
2477
- }, []);
2478
- const promptInstall = /* @__PURE__ */ __name(async () => {
2479
- if (!state.deferredPrompt) {
2480
- pwaLogger.warn("[PWA Install] No deferred prompt available");
2481
- return null;
2482
- }
2483
- try {
2484
- await state.deferredPrompt.prompt();
2485
- const { outcome } = await state.deferredPrompt.userChoice;
2486
- pwaLogger.info("[PWA Install] User choice:", outcome);
2487
- setState((prev) => ({
2488
- ...prev,
2489
- deferredPrompt: null,
2490
- canPrompt: false
2491
- }));
2492
- return outcome;
2493
- } catch (error) {
2494
- pwaLogger.error("[PWA Install] Error showing install prompt:", error);
2495
- return null;
2496
- }
2497
- }, "promptInstall");
2498
- return {
2499
- ...state,
2500
- promptInstall,
2501
- // Expose full browser info
2502
- browser,
2503
- device
2504
- };
2505
- }
2506
- __name(useInstallPrompt, "useInstallPrompt");
2507
- var PwaContext = createContext(void 0);
2508
- function PwaProvider({ children, ...config }) {
2509
- if (config.enabled === false) {
2510
- return /* @__PURE__ */ jsx(Fragment, { children });
2511
- }
2512
- const prompt = useInstallPrompt();
2513
- const value = {
2514
- // Platform
2515
- isIOS: prompt.isIOS,
2516
- isAndroid: prompt.isAndroid,
2517
- isDesktop: !prompt.isIOS && !prompt.isAndroid,
2518
- // Browsers (from useBrowserDetect)
2519
- isSafari: prompt.browser.isSafari && !prompt.browser.isChromium,
2520
- // Real Safari only
2521
- isChrome: prompt.browser.isChrome,
2522
- isFirefox: prompt.browser.isFirefox,
2523
- isEdge: prompt.browser.isEdge,
2524
- isOpera: prompt.browser.isOpera,
2525
- isBrave: prompt.browser.isBrave,
2526
- isArc: prompt.browser.isArc,
2527
- isVivaldi: prompt.browser.isVivaldi,
2528
- isYandex: prompt.browser.isYandex,
2529
- isSamsungBrowser: prompt.browser.isSamsungBrowser,
2530
- isUCBrowser: prompt.browser.isUCBrowser,
2531
- isChromium: prompt.browser.isChromium,
2532
- browserName: prompt.browser.browserName,
2533
- // State
2534
- isInstalled: prompt.isInstalled,
2535
- canPrompt: prompt.canPrompt,
2536
- // Actions
2537
- install: prompt.promptInstall
2538
- };
2539
- return /* @__PURE__ */ jsx(PwaContext.Provider, { value, children });
2540
- }
2541
- __name(PwaProvider, "PwaProvider");
2542
- function useInstall() {
2543
- const context = useContext(PwaContext);
2544
- if (context === void 0) {
2545
- throw new Error("useInstall must be used within <PwaProvider>");
2546
- }
2547
- return context;
2548
- }
2549
- __name(useInstall, "useInstall");
2550
- function getBrowserCategory(browser) {
2551
- if (browser.isChromium) return "chromium";
2552
- if (browser.isFirefox) return "firefox";
2553
- if (browser.isSafari) return "safari";
2554
- return "unknown";
2555
- }
2556
- __name(getBrowserCategory, "getBrowserCategory");
2557
- function getBrowserSteps(category, browserName) {
2558
- switch (category) {
2559
- case "chromium":
2560
- return [
2561
- {
2562
- number: 1,
2563
- title: "Find Install Icon",
2564
- icon: ArrowDownToLine,
2565
- description: "Look for install icon in address bar (right side)"
2566
- },
2567
- {
2568
- number: 2,
2569
- title: "Click Install",
2570
- icon: Plus,
2571
- description: 'Click the icon and select "Install"'
2572
- },
2573
- {
2574
- number: 3,
2575
- title: "Confirm",
2576
- icon: Check,
2577
- description: 'Click "Install" in the popup dialog'
2578
- }
2579
- ];
2580
- case "firefox":
2581
- return [
2582
- {
2583
- number: 1,
2584
- title: "Open Menu",
2585
- icon: Menu,
2586
- description: "Click the menu button (three lines)"
2587
- },
2588
- {
2589
- number: 2,
2590
- title: "Find Install Option",
2591
- icon: Search,
2592
- description: 'Look for "Install" or "Add to Home Screen"'
2593
- },
2594
- {
2595
- number: 3,
2596
- title: "Confirm",
2597
- icon: Check,
2598
- description: "Follow the installation prompts"
2599
- }
2600
- ];
2601
- case "safari":
2602
- return [
2603
- {
2604
- number: 1,
2605
- title: "Limited Support",
2606
- icon: Monitor,
2607
- description: "Safari on macOS has limited PWA support"
2608
- },
2609
- {
2610
- number: 2,
2611
- title: "Use Chromium Browser",
2612
- icon: ArrowDownToLine,
2613
- description: "Consider using Chrome, Edge, or Brave for full PWA experience"
2614
- }
2615
- ];
2616
- default:
2617
- return [
2618
- {
2619
- number: 1,
2620
- title: "Check Address Bar",
2621
- icon: ArrowDownToLine,
2622
- description: "Look for an install or download icon"
2623
- },
2624
- {
2625
- number: 2,
2626
- title: "Or Use Menu",
2627
- icon: Menu,
2628
- description: "Check browser menu for install option"
2629
- },
2630
- {
2631
- number: 3,
2632
- title: "Confirm",
2633
- icon: Check,
2634
- description: "Follow the installation prompts"
2635
- }
2636
- ];
2637
- }
2638
- }
2639
- __name(getBrowserSteps, "getBrowserSteps");
2640
- function StepCard({ step }) {
2641
- return /* @__PURE__ */ jsx(Card, { className: "border border-border", children: /* @__PURE__ */ jsx(CardContent, { className: "p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
2642
- /* @__PURE__ */ jsx(
2643
- "div",
2644
- {
2645
- className: "flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0",
2646
- style: { width: "32px", height: "32px" },
2647
- children: /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: step.number })
2648
- }
2649
- ),
2650
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2651
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-1", children: [
2652
- /* @__PURE__ */ jsx(step.icon, { className: "w-5 h-5 text-primary" }),
2653
- /* @__PURE__ */ jsx("h3", { className: "font-semibold text-foreground", children: step.title })
2654
- ] }),
2655
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: step.description })
2656
- ] })
2657
- ] }) }) });
2658
- }
2659
- __name(StepCard, "StepCard");
2660
- function DesktopGuide({ onDismiss, open = true }) {
2661
- const {
2662
- browserName,
2663
- isChromium,
2664
- isFirefox,
2665
- isSafari,
2666
- isEdge,
2667
- isBrave,
2668
- isArc,
2669
- isVivaldi,
2670
- isOpera,
2671
- isYandex
2672
- } = useInstall();
2673
- const category = useMemo(
2674
- () => getBrowserCategory({ isChromium, isFirefox, isSafari }),
2675
- [isChromium, isFirefox, isSafari]
2676
- );
2677
- const steps3 = useMemo(() => getBrowserSteps(category), [category, browserName]);
2678
- const displayName = useMemo(() => {
2679
- if (isEdge) return "Edge";
2680
- if (isBrave) return "Brave";
2681
- if (isArc) return "Arc";
2682
- if (isVivaldi) return "Vivaldi";
2683
- if (isOpera) return "Opera";
2684
- if (isYandex) return "Yandex Browser";
2685
- return browserName;
2686
- }, [browserName, isEdge, isBrave, isArc, isVivaldi, isOpera, isYandex]);
2687
- return /* @__PURE__ */ jsx(Dialog$1, { open, onOpenChange: (isOpen) => !isOpen && onDismiss(), children: /* @__PURE__ */ jsxs(DialogContent$1, { className: "sm:max-w-md", children: [
2688
- /* @__PURE__ */ jsxs(DialogHeader$1, { className: "text-left", children: [
2689
- /* @__PURE__ */ jsxs(DialogTitle$1, { className: "flex items-center gap-2", children: [
2690
- /* @__PURE__ */ jsx(Monitor, { className: "w-5 h-5 text-primary" }),
2691
- "Install App on Desktop"
2692
- ] }),
2693
- /* @__PURE__ */ jsx(DialogDescription, { className: "text-left", children: isSafari ? "Safari on macOS has limited PWA support. For the best experience, use Chrome, Edge, or Brave." : `Install this app on ${displayName} for quick access from your desktop` })
2694
- ] }),
2695
- /* @__PURE__ */ jsx("div", { className: "space-y-3 py-4", children: steps3.map((step) => /* @__PURE__ */ jsx(StepCard, { step }, step.number)) }),
2696
- category === "chromium" && /* @__PURE__ */ jsxs("div", { className: "p-3 bg-muted/30 rounded-lg text-xs text-muted-foreground", children: [
2697
- '\u{1F4A1} Tip: You can also right-click the page and look for "Install" option in ',
2698
- displayName
2699
- ] }),
2700
- category === "firefox" && /* @__PURE__ */ jsx("div", { className: "p-3 bg-amber-500/10 rounded-lg text-xs text-amber-700 dark:text-amber-400", children: "\u2139\uFE0F Note: Firefox has limited PWA support. Some features may not work as expected." }),
2701
- /* @__PURE__ */ jsx(DialogFooter, { children: /* @__PURE__ */ jsxs(Button, { onClick: onDismiss, variant: "default", className: "w-full", children: [
2702
- /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 mr-2" }),
2703
- "Got It"
2704
- ] }) })
2705
- ] }) });
2706
- }
2707
- __name(DesktopGuide, "DesktopGuide");
2708
- var steps = [
2709
- {
2710
- number: 1,
2711
- title: "Tap Share",
2712
- icon: ArrowUpRight,
2713
- description: "At the bottom of Safari"
2714
- },
2715
- {
2716
- number: 2,
2717
- title: "Scroll & Tap",
2718
- icon: ArrowDown,
2719
- description: '"Add to Home Screen"'
2720
- },
2721
- {
2722
- number: 3,
2723
- title: "Confirm",
2724
- icon: CheckCircle,
2725
- description: 'Tap "Add" in top-right'
2726
- }
2727
- ];
2728
- function StepCard2({ step }) {
2729
- return /* @__PURE__ */ jsx(Card, { className: "border border-border", children: /* @__PURE__ */ jsx(CardContent, { className: "p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
2730
- /* @__PURE__ */ jsx(
2731
- "div",
2732
- {
2733
- className: "flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0",
2734
- style: { width: "32px", height: "32px" },
2735
- children: /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: step.number })
2736
- }
2737
- ),
2738
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2739
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-1", children: [
2740
- /* @__PURE__ */ jsx(step.icon, { className: "w-5 h-5 text-primary" }),
2741
- /* @__PURE__ */ jsx("h3", { className: "font-semibold text-foreground", children: step.title })
2742
- ] }),
2743
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: step.description })
2744
- ] })
2745
- ] }) }) });
2746
- }
2747
- __name(StepCard2, "StepCard");
2748
- function IOSGuideDrawer({ onDismiss, open = true }) {
2749
- return /* @__PURE__ */ jsx(Drawer, { open, onOpenChange: (isOpen) => !isOpen && onDismiss(), children: /* @__PURE__ */ jsxs(DrawerContent, { children: [
2750
- /* @__PURE__ */ jsxs(DrawerHeader, { className: "text-left", children: [
2751
- /* @__PURE__ */ jsxs(DrawerTitle, { className: "flex items-center gap-2", children: [
2752
- /* @__PURE__ */ jsx(Share, { className: "w-5 h-5 text-primary" }),
2753
- "Add to Home Screen"
2754
- ] }),
2755
- /* @__PURE__ */ jsx(DrawerDescription, { className: "text-left", children: "Install this app on your iPhone for quick access and a better experience" })
2756
- ] }),
2757
- /* @__PURE__ */ jsx("div", { className: "space-y-3 p-4", children: steps.map((step) => /* @__PURE__ */ jsx(StepCard2, { step }, step.number)) }),
2758
- /* @__PURE__ */ jsx("div", { className: "p-4 pt-0", children: /* @__PURE__ */ jsxs(Button, { onClick: onDismiss, variant: "default", className: "w-full", children: [
2759
- /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 mr-2" }),
2760
- "Got It"
2761
- ] }) })
2762
- ] }) });
2763
- }
2764
- __name(IOSGuideDrawer, "IOSGuideDrawer");
2765
- var steps2 = [
2766
- {
2767
- number: 1,
2768
- title: "Tap Share",
2769
- icon: ArrowUpRight,
2770
- description: "At the bottom of Safari"
2771
- },
2772
- {
2773
- number: 2,
2774
- title: "Scroll & Tap",
2775
- icon: ArrowDown,
2776
- description: '"Add to Home Screen"'
2777
- },
2778
- {
2779
- number: 3,
2780
- title: "Confirm",
2781
- icon: CheckCircle,
2782
- description: 'Tap "Add" in top-right'
2783
- }
2784
- ];
2785
- function StepCard3({ step }) {
2786
- return /* @__PURE__ */ jsx(Card, { className: "border border-border", children: /* @__PURE__ */ jsx(CardContent, { className: "p-4", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
2787
- /* @__PURE__ */ jsx(
2788
- "div",
2789
- {
2790
- className: "flex items-center justify-center rounded-full bg-primary text-primary-foreground flex-shrink-0",
2791
- style: { width: "32px", height: "32px" },
2792
- children: /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: step.number })
2793
- }
2794
- ),
2795
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2796
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mb-1", children: [
2797
- /* @__PURE__ */ jsx(step.icon, { className: "w-5 h-5 text-primary" }),
2798
- /* @__PURE__ */ jsx("h3", { className: "font-semibold text-foreground", children: step.title })
2799
- ] }),
2800
- /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: step.description })
2801
- ] })
2802
- ] }) }) });
2803
- }
2804
- __name(StepCard3, "StepCard");
2805
- function IOSGuideModal({ onDismiss, open = true }) {
2806
- return /* @__PURE__ */ jsx(Dialog$1, { open, onOpenChange: (isOpen) => !isOpen && onDismiss(), children: /* @__PURE__ */ jsxs(DialogContent$1, { className: "sm:max-w-md", children: [
2807
- /* @__PURE__ */ jsxs(DialogHeader$1, { className: "text-left", children: [
2808
- /* @__PURE__ */ jsxs(DialogTitle$1, { className: "flex items-center gap-2", children: [
2809
- /* @__PURE__ */ jsx(Share, { className: "w-5 h-5 text-primary" }),
2810
- "Add to Home Screen"
2811
- ] }),
2812
- /* @__PURE__ */ jsx(DialogDescription, { className: "text-left", children: "Install this app on your iPhone for quick access and a better experience" })
2813
- ] }),
2814
- /* @__PURE__ */ jsx("div", { className: "space-y-3 py-4", children: steps2.map((step) => /* @__PURE__ */ jsx(StepCard3, { step }, step.number)) }),
2815
- /* @__PURE__ */ jsx(DialogFooter, { children: /* @__PURE__ */ jsxs(Button, { onClick: onDismiss, variant: "default", className: "w-full", children: [
2816
- /* @__PURE__ */ jsx(Check, { className: "w-4 h-4 mr-2" }),
2817
- "Got It"
2818
- ] }) })
2819
- ] }) });
2820
- }
2821
- __name(IOSGuideModal, "IOSGuideModal");
2822
- function IOSGuide(props) {
2823
- const isMobile = useIsMobile();
2824
- if (isMobile) {
2825
- return /* @__PURE__ */ jsx(IOSGuideDrawer, { ...props });
2826
- }
2827
- return /* @__PURE__ */ jsx(IOSGuideModal, { ...props });
2828
- }
2829
- __name(IOSGuide, "IOSGuide");
2830
- var DEFAULT_RESET_DAYS = 3;
2831
- function A2HSHint({
2832
- className,
2833
- resetAfterDays = DEFAULT_RESET_DAYS,
2834
- delayMs = 3e3,
2835
- demo = false,
2836
- logo
2837
- } = {}) {
2838
- const { isIOS, isDesktop, isInstalled, canPrompt, install } = useInstall();
2839
- const [show, setShow] = useState(false);
2840
- const [showGuide, setShowGuide] = useState(false);
2841
- const [installing, setInstalling] = useState(false);
2842
- const shouldShow = demo ? !isInstalled : !isInstalled && (isIOS || canPrompt);
2843
- useEffect(() => {
2844
- if (!shouldShow) return;
2845
- if (!demo && typeof window !== "undefined") {
2846
- if (resetAfterDays === null) {
2847
- if (isA2HSDismissedRecently(Number.MAX_SAFE_INTEGER)) {
2848
- return;
2849
- }
2850
- } else if (isA2HSDismissedRecently(resetAfterDays)) {
2851
- return;
2852
- }
2853
- }
2854
- const timer = setTimeout(() => setShow(true), delayMs);
2855
- return () => clearTimeout(timer);
2856
- }, [shouldShow, resetAfterDays, delayMs, demo]);
2857
- const handleDismiss = /* @__PURE__ */ __name(() => {
2858
- setShow(false);
2859
- if (!demo) {
2860
- markA2HSDismissed();
2861
- }
2862
- }, "handleDismiss");
2863
- const handleGuideDismiss = /* @__PURE__ */ __name(() => {
2864
- setShowGuide(false);
2865
- if (!demo) {
2866
- handleDismiss();
2867
- }
2868
- }, "handleGuideDismiss");
2869
- const handleClick = /* @__PURE__ */ __name(async () => {
2870
- if (isIOS || isDesktop) {
2871
- setShowGuide(true);
2872
- } else if (canPrompt) {
2873
- setInstalling(true);
2874
- try {
2875
- await install();
2876
- handleDismiss();
2877
- } catch (error) {
2878
- pwaLogger.error("[A2HSHint] Install error:", error);
2879
- } finally {
2880
- setInstalling(false);
2881
- }
2882
- }
2883
- }, "handleClick");
2884
- if (!show) return null;
2885
- let title;
2886
- let subtitle;
2887
- if (isIOS) {
2888
- title = "Add to Home Screen";
2889
- subtitle = /* @__PURE__ */ jsxs(Fragment, { children: [
2890
- "Tap to learn how ",
2891
- /* @__PURE__ */ jsx(ChevronRight, { className: "w-3 h-3" })
2892
- ] });
2893
- } else if (isDesktop) {
2894
- title = "Install App";
2895
- subtitle = /* @__PURE__ */ jsxs(Fragment, { children: [
2896
- "Click to see desktop guide ",
2897
- /* @__PURE__ */ jsx(ChevronRight, { className: "w-3 h-3" })
2898
- ] });
2899
- } else {
2900
- title = "Install App";
2901
- subtitle = /* @__PURE__ */ jsxs(Fragment, { children: [
2902
- "Tap to install ",
2903
- /* @__PURE__ */ jsx(Download, { className: "w-3 h-3" })
2904
- ] });
2905
- }
2906
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2907
- /* @__PURE__ */ jsx("div", { className: cn(
2908
- "fixed bottom-4 left-4 right-4 z-50 animate-in slide-in-from-bottom-4 duration-300",
2909
- demo && "relative inset-auto z-auto",
2910
- // Demo mode: remove fixed positioning
2911
- className
2912
- ), children: /* @__PURE__ */ jsx(
2913
- "div",
2914
- {
2915
- role: "button",
2916
- tabIndex: 0,
2917
- onClick: handleClick,
2918
- onKeyDown: (e) => {
2919
- if (e.key === "Enter" || e.key === " ") {
2920
- e.preventDefault();
2921
- handleClick();
2922
- }
2923
- },
2924
- className: cn(
2925
- "w-full bg-zinc-900 border border-zinc-700 rounded-lg p-4 shadow-lg cursor-pointer hover:bg-zinc-800 transition-colors",
2926
- installing && "opacity-70 cursor-not-allowed"
2927
- ),
2928
- "aria-disabled": installing,
2929
- children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
2930
- /* @__PURE__ */ jsx("div", { className: "flex-shrink-0", children: logo ? /* @__PURE__ */ jsx("img", { src: logo, alt: "App logo", className: "w-10 h-10 rounded-lg" }) : /* @__PURE__ */ jsx(Share, { className: "w-5 h-5 text-blue-400" }) }),
2931
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
2932
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-white mb-1", children: title }),
2933
- /* @__PURE__ */ jsx("p", { className: "text-xs text-zinc-400 flex items-center gap-1", children: installing ? "Installing..." : subtitle })
2934
- ] }),
2935
- /* @__PURE__ */ jsx(
2936
- "button",
2937
- {
2938
- onClick: (e) => {
2939
- e.stopPropagation();
2940
- handleDismiss();
2941
- },
2942
- className: "flex-shrink-0 p-1 hover:bg-zinc-700 rounded transition-colors",
2943
- "aria-label": "Dismiss",
2944
- children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4 text-zinc-400" })
2945
- }
2946
- )
2947
- ] })
2948
- }
2949
- ) }),
2950
- isIOS && /* @__PURE__ */ jsx(IOSGuide, { open: showGuide, onDismiss: handleGuideDismiss }),
2951
- isDesktop && /* @__PURE__ */ jsx(DesktopGuide, { open: showGuide, onDismiss: handleGuideDismiss })
2952
- ] });
2953
- }
2954
- __name(A2HSHint, "A2HSHint");
2955
- var CACHE_KEY = "pwa_is_standalone";
2956
- function useIsPWA(options) {
2957
- const checkFunction = options?.reliable ? isStandaloneReliable : isStandalone;
2958
- const [isPWA, setIsPWA] = useState(() => {
2959
- if (typeof window !== "undefined") {
2960
- try {
2961
- const cached = sessionStorage.getItem(CACHE_KEY);
2962
- if (cached !== null) {
2963
- return cached === "true";
2964
- }
2965
- } catch {
2966
- }
2967
- }
2968
- return checkFunction();
2969
- });
2970
- useEffect(() => {
2971
- const isStandaloneMode = checkFunction();
2972
- setIsPWA(isStandaloneMode);
2973
- if (typeof window !== "undefined") {
2974
- try {
2975
- sessionStorage.setItem(CACHE_KEY, String(isStandaloneMode));
2976
- } catch {
2977
- }
2978
- }
2979
- const cleanup = onDisplayModeChange((newValue) => {
2980
- setIsPWA(newValue);
2981
- if (typeof window !== "undefined") {
2982
- try {
2983
- sessionStorage.setItem(CACHE_KEY, String(newValue));
2984
- } catch {
2985
- }
2986
- }
2987
- });
2988
- return cleanup;
2989
- }, [checkFunction]);
2990
- return isPWA;
2991
- }
2992
- __name(useIsPWA, "useIsPWA");
2993
- function clearIsPWACache() {
2994
- if (typeof window === "undefined") return;
2995
- try {
2996
- sessionStorage.removeItem(CACHE_KEY);
2997
- } catch {
2998
- }
2999
- }
3000
- __name(clearIsPWACache, "clearIsPWACache");
3001
- function isDebugEnabled2() {
3002
- if (typeof window === "undefined") return false;
3003
- try {
3004
- return localStorage.getItem("pwa_debug") === "true";
3005
- } catch {
3006
- return false;
3007
- }
3008
- }
3009
- __name(isDebugEnabled2, "isDebugEnabled");
3010
- var pwaLogger2 = {
3011
- /**
3012
- * Info level logging
3013
- * Only logs in development or when debug is enabled
3014
- */
3015
- info: /* @__PURE__ */ __name((...args) => {
3016
- {
3017
- consola.info(...args);
3018
- }
3019
- }, "info"),
3020
- /**
3021
- * Warning level logging
3022
- * Only logs in development or when debug is enabled
3023
- */
3024
- warn: /* @__PURE__ */ __name((...args) => {
3025
- {
3026
- consola.warn(...args);
3027
- }
3028
- }, "warn"),
3029
- /**
3030
- * Error level logging
3031
- * Always logs (production + development)
3032
- */
3033
- error: /* @__PURE__ */ __name((...args) => {
3034
- consola.error(...args);
3035
- }, "error"),
3036
- /**
3037
- * Debug level logging
3038
- * Only logs when debug is explicitly enabled
3039
- */
3040
- debug: /* @__PURE__ */ __name((...args) => {
3041
- if (isDebugEnabled2()) {
3042
- consola.debug(...args);
3043
- }
3044
- }, "debug"),
3045
- /**
3046
- * Success level logging
3047
- * Only logs in development or when debug is enabled
3048
- */
3049
- success: /* @__PURE__ */ __name((...args) => {
3050
- {
3051
- consola.success(...args);
3052
- }
3053
- }, "success")
3054
- };
3055
-
3056
- // src/snippets/PushNotifications/utils/platform.ts
3057
- function checkBrowserPushSupport() {
3058
- if (typeof window === "undefined") {
3059
- return { isSupported: false, browserName: "unknown", reason: "Server-side rendering" };
3060
- }
3061
- const ua = window.navigator.userAgent.toLowerCase();
3062
- if (ua.includes("fban") || ua.includes("fbav") || ua.includes("fb_iab")) {
3063
- return { isSupported: false, browserName: "Facebook In-App", reason: "In-app browsers do not support push notifications" };
3064
- }
3065
- if (ua.includes("instagram")) {
3066
- return { isSupported: false, browserName: "Instagram In-App", reason: "In-app browsers do not support push notifications" };
3067
- }
3068
- if (ua.includes("tiktok") || ua.includes("bytedancewebview") || ua.includes("bytelocale")) {
3069
- return { isSupported: false, browserName: "TikTok In-App", reason: "In-app browsers do not support push notifications" };
3070
- }
3071
- if (ua.includes("snapchat")) {
3072
- return { isSupported: false, browserName: "Snapchat In-App", reason: "In-app browsers do not support push notifications" };
3073
- }
3074
- if (ua.includes("micromessenger")) {
3075
- return { isSupported: false, browserName: "WeChat In-App", reason: "In-app browsers do not support push notifications" };
3076
- }
3077
- if (ua.includes("barcelona")) {
3078
- return { isSupported: false, browserName: "Threads In-App", reason: "In-app browsers do not support push notifications" };
3079
- }
3080
- if (ua.includes("pinterest")) {
3081
- return { isSupported: false, browserName: "Pinterest In-App", reason: "In-app browsers do not support push notifications" };
3082
- }
3083
- if (ua.includes("telegram")) {
3084
- return { isSupported: false, browserName: "Telegram In-App", reason: "In-app browsers do not support push notifications" };
3085
- }
3086
- if (ua.includes("line/")) {
3087
- return { isSupported: false, browserName: "Line In-App", reason: "In-app browsers do not support push notifications" };
3088
- }
3089
- if (ua.includes("kakaotalk")) {
3090
- return { isSupported: false, browserName: "KakaoTalk In-App", reason: "In-app browsers do not support push notifications" };
3091
- }
3092
- const isIOSDevice = ua.includes("iphone") || ua.includes("ipad") || ua.includes("ipod");
3093
- if (ua.includes("linkedinapp") && isIOSDevice) {
3094
- return { isSupported: false, browserName: "LinkedIn In-App", reason: "LinkedIn In-App on iOS does not support push notifications" };
3095
- }
3096
- if (ua.includes("twitter") && isIOSDevice) {
3097
- return { isSupported: false, browserName: "Twitter In-App", reason: "Twitter In-App on iOS does not support push notifications" };
3098
- }
3099
- if (ua.includes("opera mini") || ua.includes("opios")) {
3100
- return { isSupported: false, browserName: "Opera Mini", reason: "Opera Mini does not support service workers" };
3101
- }
3102
- if (ua.includes("msie") || ua.includes("trident/")) {
3103
- return { isSupported: false, browserName: "Internet Explorer", reason: "Internet Explorer does not support Push API" };
3104
- }
3105
- if (ua.includes("ucbrowser") || ua.includes("uc browser")) {
3106
- return { isSupported: false, browserName: "UC Browser", reason: "UC Browser has unreliable push notification support" };
3107
- }
3108
- const isWebView = ua.includes("wv)") || ua.includes("webview") || ua.includes("; wv") || ua.includes("iphone") && !ua.includes("safari") || ua.includes("ipad") && !ua.includes("safari");
3109
- if (isWebView) {
3110
- return { isSupported: false, browserName: "WebView", reason: "WebViews do not support push notifications" };
3111
- }
3112
- let browserName = "unknown";
3113
- if (ua.includes("comet") || ua.includes("perplexity")) browserName = "Comet";
3114
- else if (ua.includes("edg/") || ua.includes("edge/")) browserName = "Edge";
3115
- else if (window.navigator.brave) browserName = "Brave";
3116
- else if (ua.includes("arc/")) browserName = "Arc";
3117
- else if (ua.includes("vivaldi")) browserName = "Vivaldi";
3118
- else if (ua.includes("yabrowser")) browserName = "Yandex";
3119
- else if (ua.includes("samsungbrowser")) browserName = "Samsung Internet";
3120
- else if (ua.includes("opr/") || ua.includes("opera")) browserName = "Opera";
3121
- else if (ua.includes("firefox")) browserName = "Firefox";
3122
- else if (ua.includes("chrome")) browserName = "Chrome";
3123
- else if (ua.includes("safari") && ua.includes("version/")) browserName = "Safari";
3124
- return { isSupported: true, browserName };
3125
- }
3126
- __name(checkBrowserPushSupport, "checkBrowserPushSupport");
3127
- function isStandalone2() {
3128
- if (typeof window === "undefined") return false;
3129
- if (!window.matchMedia) {
3130
- const nav2 = navigator;
3131
- return nav2.standalone === true;
3132
- }
3133
- const isStandaloneDisplay = window.matchMedia("(display-mode: standalone)").matches;
3134
- const nav = navigator;
3135
- const isStandaloneNavigator = nav.standalone === true;
3136
- return isStandaloneDisplay || isStandaloneNavigator;
3137
- }
3138
- __name(isStandalone2, "isStandalone");
3139
-
3140
- // src/snippets/PushNotifications/utils/vapid.ts
3141
- var _VapidKeyError = class _VapidKeyError extends Error {
3142
- constructor(message, code) {
3143
- super(message);
3144
- this.code = code;
3145
- this.name = "VapidKeyError";
3146
- }
3147
- };
3148
- __name(_VapidKeyError, "VapidKeyError");
3149
- var VapidKeyError = _VapidKeyError;
3150
- function urlBase64ToUint8Array(base64String) {
3151
- if (!base64String) {
3152
- throw new VapidKeyError("VAPID public key is required", "VAPID_EMPTY");
3153
- }
3154
- if (typeof base64String !== "string") {
3155
- throw new VapidKeyError("VAPID public key must be a string", "VAPID_INVALID_TYPE");
3156
- }
3157
- const padding = "=".repeat((4 - base64String.length % 4) % 4);
3158
- const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
3159
- let rawData;
3160
- try {
3161
- rawData = window.atob(base64);
3162
- } catch (e) {
3163
- throw new VapidKeyError(
3164
- `Invalid base64url format: ${e instanceof Error ? e.message : String(e)}`,
3165
- "VAPID_INVALID_BASE64"
3166
- );
3167
- }
3168
- const outputArray = new Uint8Array(rawData.length);
3169
- for (let i = 0; i < rawData.length; i++) {
3170
- outputArray[i] = rawData.charCodeAt(i);
3171
- }
3172
- if (outputArray.length !== 65) {
3173
- throw new VapidKeyError(
3174
- `Invalid key length: expected 65 bytes (P-256 uncompressed), got ${outputArray.length} bytes`,
3175
- "VAPID_INVALID_LENGTH"
3176
- );
3177
- }
3178
- if (outputArray[0] !== 4) {
3179
- throw new VapidKeyError(
3180
- `Invalid key format: must start with 0x04 (uncompressed P-256 point), got 0x${outputArray[0].toString(16).padStart(2, "0")}`,
3181
- "VAPID_INVALID_FORMAT"
3182
- );
3183
- }
3184
- return outputArray;
3185
- }
3186
- __name(urlBase64ToUint8Array, "urlBase64ToUint8Array");
3187
- function isValidVapidKey(base64String) {
3188
- try {
3189
- urlBase64ToUint8Array(base64String);
3190
- return true;
3191
- } catch {
3192
- return false;
3193
- }
3194
- }
3195
- __name(isValidVapidKey, "isValidVapidKey");
3196
- function getVapidKeyInfo(base64String) {
3197
- try {
3198
- const key = urlBase64ToUint8Array(base64String);
3199
- return {
3200
- valid: true,
3201
- length: key.length,
3202
- firstByte: `0x${key[0].toString(16).padStart(2, "0")}`,
3203
- format: key[0] === 4 ? "P-256 uncompressed" : "Unknown"
3204
- };
3205
- } catch (e) {
3206
- if (e instanceof VapidKeyError) {
3207
- return {
3208
- valid: false,
3209
- error: e.message,
3210
- errorCode: e.code
3211
- };
3212
- }
3213
- return {
3214
- valid: false,
3215
- error: String(e)
3216
- };
3217
- }
3218
- }
3219
- __name(getVapidKeyInfo, "getVapidKeyInfo");
3220
- function safeUrlBase64ToUint8Array(base64String, onError) {
3221
- try {
3222
- return urlBase64ToUint8Array(base64String);
3223
- } catch (e) {
3224
- if (e instanceof VapidKeyError) {
3225
- onError?.(e);
3226
- }
3227
- return null;
3228
- }
3229
- }
3230
- __name(safeUrlBase64ToUint8Array, "safeUrlBase64ToUint8Array");
3231
-
3232
- // src/snippets/PushNotifications/hooks/usePushNotifications.ts
3233
- function usePushNotifications(options) {
3234
- const [state, setState] = useState({
3235
- isSupported: false,
3236
- permission: "default",
3237
- isSubscribed: false,
3238
- subscription: null
3239
- });
3240
- useEffect(() => {
3241
- if (typeof window === "undefined") return;
3242
- const browserSupport = checkBrowserPushSupport();
3243
- if (!browserSupport.isSupported) {
3244
- pwaLogger2.info(`[usePushNotifications] Browser does not support push: ${browserSupport.browserName} - ${browserSupport.reason}`);
3245
- setState((prev) => ({
3246
- ...prev,
3247
- isSupported: false,
3248
- permission: "denied"
3249
- }));
3250
- return;
3251
- }
3252
- const isSupported = "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
3253
- setState((prev) => ({
3254
- ...prev,
3255
- isSupported,
3256
- permission: isSupported ? Notification.permission : "denied"
3257
- }));
3258
- if (isSupported) {
3259
- navigator.serviceWorker.ready.then((registration) => registration.pushManager.getSubscription()).then((subscription) => {
3260
- setState((prev) => ({
3261
- ...prev,
3262
- isSubscribed: !!subscription,
3263
- subscription
3264
- }));
3265
- }).catch((error) => {
3266
- pwaLogger2.error("[usePushNotifications] Failed to get subscription:", error);
3267
- });
3268
- }
3269
- }, []);
3270
- const subscribe = /* @__PURE__ */ __name(async () => {
3271
- if (!state.isSupported) {
3272
- pwaLogger2.warn("[usePushNotifications] Push notifications not supported");
3273
- return null;
3274
- }
3275
- if (!options?.vapidPublicKey) {
3276
- pwaLogger2.error("[usePushNotifications] VAPID public key required");
3277
- return null;
3278
- }
3279
- try {
3280
- pwaLogger2.debug("[usePushNotifications] Running pre-flight checks...");
3281
- if (!navigator.onLine) {
3282
- pwaLogger2.error("[usePushNotifications] No internet connection");
3283
- throw new Error("No internet connection. Please check your network and try again.");
3284
- }
3285
- const permission = await Notification.requestPermission();
3286
- setState((prev) => ({ ...prev, permission }));
3287
- if (permission !== "granted") {
3288
- pwaLogger2.warn("[usePushNotifications] Permission not granted:", permission);
3289
- return null;
3290
- }
3291
- const registration = await navigator.serviceWorker.ready;
3292
- let applicationServerKey;
3293
- try {
3294
- pwaLogger2.debug("[usePushNotifications] Converting VAPID key...");
3295
- applicationServerKey = urlBase64ToUint8Array(options.vapidPublicKey);
3296
- pwaLogger2.info("[usePushNotifications] VAPID key validated successfully");
3297
- } catch (e) {
3298
- if (e instanceof VapidKeyError) {
3299
- pwaLogger2.error(`[usePushNotifications] Invalid VAPID key: ${e.message} (code: ${e.code})`);
3300
- } else {
3301
- pwaLogger2.error("[usePushNotifications] Failed to convert VAPID key:", e);
3302
- }
3303
- return null;
3304
- }
3305
- pwaLogger2.debug("[usePushNotifications] Service Worker state:", {
3306
- controller: navigator.serviceWorker.controller ? "active" : "none",
3307
- registrationActive: registration.active ? "yes" : "no",
3308
- permission: Notification.permission
3309
- });
3310
- const existingSub = await registration.pushManager.getSubscription();
3311
- if (existingSub) {
3312
- pwaLogger2.debug("[usePushNotifications] Unsubscribing from existing subscription...");
3313
- await existingSub.unsubscribe();
3314
- }
3315
- const subscribeOptions = {
3316
- userVisibleOnly: true,
3317
- applicationServerKey
3318
- };
3319
- pwaLogger2.debug("[usePushNotifications] Subscribing with VAPID key...");
3320
- const subscription = await registration.pushManager.subscribe(subscribeOptions);
3321
- if (options.subscribeEndpoint) {
3322
- await fetch(options.subscribeEndpoint, {
3323
- method: "POST",
3324
- headers: { "Content-Type": "application/json" },
3325
- body: JSON.stringify(subscription)
3326
- });
3327
- }
3328
- setState((prev) => ({
3329
- ...prev,
3330
- isSubscribed: true,
3331
- subscription
3332
- }));
3333
- pwaLogger2.success("[usePushNotifications] Successfully subscribed to push notifications");
3334
- return subscription;
3335
- } catch (error) {
3336
- pwaLogger2.error("[usePushNotifications] Subscribe failed:", error);
3337
- if (error.name === "AbortError" || error.message?.includes("push service error")) {
3338
- pwaLogger2.error("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
3339
- pwaLogger2.error("\u274C PUSH SERVICE ERROR - Cannot connect to FCM");
3340
- pwaLogger2.error("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
3341
- pwaLogger2.error("");
3342
- pwaLogger2.error("\u{1F50D} This is NOT a code bug - it's a network/security block.");
3343
- pwaLogger2.error("");
3344
- pwaLogger2.error("\u2705 Quick Fixes (try in order):");
3345
- pwaLogger2.error(" 1. Disable VPN/Proxy and refresh page");
3346
- pwaLogger2.error(" 2. Open Incognito window (Cmd+Shift+N)");
3347
- pwaLogger2.error(" 3. Try different browser (Safari, Firefox)");
3348
- pwaLogger2.error(" 4. Use mobile hotspot instead of WiFi");
3349
- pwaLogger2.error("");
3350
- pwaLogger2.error("\u{1F527} Technical Details:");
3351
- pwaLogger2.error(" \u2022 Browser tries to connect to FCM (ports 5228-5230)");
3352
- pwaLogger2.error(" \u2022 VPN/Firewall/Privacy settings may block these ports");
3353
- pwaLogger2.error(' \u2022 Check browser console for "AbortError" details');
3354
- pwaLogger2.error("");
3355
- pwaLogger2.error("\u{1F4DA} Learn more: https://web.dev/push-notifications-overview/");
3356
- pwaLogger2.error("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
3357
- } else if (error.message?.includes("No internet connection")) {
3358
- pwaLogger2.error("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
3359
- pwaLogger2.error("\u274C NO INTERNET CONNECTION");
3360
- pwaLogger2.error("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
3361
- pwaLogger2.error("Please check your network connection and try again.");
3362
- } else {
3363
- pwaLogger2.error("Unknown error:", error.name, error.message);
3364
- }
3365
- return null;
3366
- }
3367
- }, "subscribe");
3368
- const unsubscribe = /* @__PURE__ */ __name(async () => {
3369
- if (!state.subscription) {
3370
- pwaLogger2.warn("[usePushNotifications] No active subscription to unsubscribe");
3371
- return false;
3372
- }
3373
- try {
3374
- await state.subscription.unsubscribe();
3375
- setState((prev) => ({
3376
- ...prev,
3377
- isSubscribed: false,
3378
- subscription: null
3379
- }));
3380
- pwaLogger2.info("[usePushNotifications] Successfully unsubscribed from push notifications");
3381
- return true;
3382
- } catch (error) {
3383
- pwaLogger2.error("[usePushNotifications] Unsubscribe failed:", error);
3384
- return false;
3385
- }
3386
- }, "unsubscribe");
3387
- return {
3388
- ...state,
3389
- subscribe,
3390
- unsubscribe
3391
- };
3392
- }
3393
- __name(usePushNotifications, "usePushNotifications");
3394
-
3395
- // src/snippets/PushNotifications/hooks/useDjangoPush.ts
3396
- function useDjangoPush(options) {
3397
- const { onSubscribed, onSubscribeError, onUnsubscribed, ...pushOptions } = options;
3398
- const [isLoading, setIsLoading] = useState(false);
3399
- const [error, setError] = useState(null);
3400
- const { isAuthenticated } = useAuth();
3401
- const { openAuthDialog } = useAuthDialog();
3402
- const pushNotifications = usePushNotifications(pushOptions);
3403
- const subscribe = useCallback(async () => {
3404
- if (!isAuthenticated) {
3405
- openAuthDialog({
3406
- message: "Please sign in to enable push notifications"
3407
- });
3408
- return false;
3409
- }
3410
- setIsLoading(true);
3411
- setError(null);
3412
- try {
3413
- const subscription = await pushNotifications.subscribe();
3414
- if (!subscription) {
3415
- const err = new Error("Browser subscription failed");
3416
- setError(err);
3417
- onSubscribeError?.(err);
3418
- return false;
3419
- }
3420
- pwaLogger2.info("[useDjangoPush] Browser subscription created");
3421
- const result = await apiWebPush.web_push.webpushSubscribeCreate({
3422
- endpoint: subscription.endpoint,
3423
- keys: {
3424
- p256dh: arrayBufferToBase64(subscription.getKey("p256dh")),
3425
- auth: arrayBufferToBase64(subscription.getKey("auth"))
3426
- }
3427
- });
3428
- pwaLogger2.info("[useDjangoPush] Subscription saved to Django:", result);
3429
- onSubscribed?.(subscription);
3430
- toast.success("Push notifications enabled");
3431
- return true;
3432
- } catch (err) {
3433
- const error2 = err instanceof Error ? err : new Error(String(err));
3434
- pwaLogger2.error("[useDjangoPush] Subscribe failed:", error2);
3435
- setError(error2);
3436
- onSubscribeError?.(error2);
3437
- toast.error("Subscription Failed", {
3438
- description: "Could not save subscription to server. Please try again.",
3439
- duration: 5e3
3440
- });
3441
- return false;
3442
- } finally {
3443
- setIsLoading(false);
3444
- }
3445
- }, [isAuthenticated, openAuthDialog, pushNotifications.subscribe, onSubscribed, onSubscribeError]);
3446
- const unsubscribe = useCallback(async () => {
3447
- setIsLoading(true);
3448
- setError(null);
3449
- try {
3450
- const success = await pushNotifications.unsubscribe();
3451
- if (!success) {
3452
- return false;
3453
- }
3454
- pwaLogger2.info("[useDjangoPush] Browser unsubscribed");
3455
- onUnsubscribed?.();
3456
- toast.success("Push notifications disabled");
3457
- return true;
3458
- } catch (err) {
3459
- const error2 = err instanceof Error ? err : new Error(String(err));
3460
- pwaLogger2.error("[useDjangoPush] Unsubscribe failed:", error2);
3461
- setError(error2);
3462
- return false;
3463
- } finally {
3464
- setIsLoading(false);
3465
- }
3466
- }, [pushNotifications.unsubscribe, onUnsubscribed]);
3467
- const sendTestPush = useCallback(
3468
- async (message) => {
3469
- if (!pushNotifications.isSubscribed) {
3470
- const err = new Error("Not subscribed");
3471
- setError(err);
3472
- return false;
3473
- }
3474
- setIsLoading(true);
3475
- setError(null);
3476
- try {
3477
- const result = await apiWebPush.web_push.webpushSendCreate({
3478
- title: message.title,
3479
- body: message.body,
3480
- url: message.url || "/",
3481
- icon: "/icon.png"
3482
- });
3483
- pwaLogger2.info("[useDjangoPush] Test push sent:", result);
3484
- return result.success;
3485
- } catch (err) {
3486
- const error2 = err instanceof Error ? err : new Error(String(err));
3487
- pwaLogger2.error("[useDjangoPush] Send test push failed:", error2);
3488
- setError(error2);
3489
- return false;
3490
- } finally {
3491
- setIsLoading(false);
3492
- }
3493
- },
3494
- [pushNotifications.isSubscribed]
3495
- );
3496
- return {
3497
- // State from usePushNotifications
3498
- isSupported: pushNotifications.isSupported,
3499
- permission: pushNotifications.permission,
3500
- isSubscribed: pushNotifications.isSubscribed,
3501
- subscription: pushNotifications.subscription,
3502
- // Local state
3503
- isLoading,
3504
- error,
3505
- // Actions
3506
- subscribe,
3507
- unsubscribe,
3508
- sendTestPush
3509
- };
3510
- }
3511
- __name(useDjangoPush, "useDjangoPush");
3512
- function arrayBufferToBase64(buffer) {
3513
- if (!buffer) return "";
3514
- const bytes = new Uint8Array(buffer);
3515
- let binary = "";
3516
- for (let i = 0; i < bytes.byteLength; i++) {
3517
- binary += String.fromCharCode(bytes[i]);
3518
- }
3519
- return window.btoa(binary);
3520
- }
3521
- __name(arrayBufferToBase64, "arrayBufferToBase64");
3522
- var DjangoPushContext = createContext(void 0);
3523
- function DjangoPushProvider({
3524
- children,
3525
- vapidPublicKey,
3526
- autoSubscribe = false,
3527
- onSubscribed,
3528
- onSubscribeError,
3529
- onUnsubscribed
3530
- }) {
3531
- const djangoPush = useDjangoPush({
3532
- vapidPublicKey,
3533
- onSubscribed,
3534
- onSubscribeError,
3535
- onUnsubscribed
3536
- });
3537
- useEffect(() => {
3538
- if (autoSubscribe && djangoPush.isSupported && djangoPush.permission === "granted" && !djangoPush.isSubscribed && !djangoPush.isLoading) {
3539
- pwaLogger2.info("[DjangoPushProvider] Auto-subscribing (permission already granted)");
3540
- djangoPush.subscribe();
3541
- }
3542
- }, [autoSubscribe, djangoPush.isSupported, djangoPush.permission, djangoPush.isSubscribed, djangoPush.isLoading]);
3543
- const [pushes, setPushes] = useState([]);
3544
- useEffect(() => {
3545
- if (typeof window === "undefined" || !("serviceWorker" in navigator)) {
3546
- return;
3547
- }
3548
- const handleMessage = /* @__PURE__ */ __name((event) => {
3549
- if (event.data && event.data.type === "PUSH_RECEIVED") {
3550
- const push = {
3551
- id: crypto.randomUUID(),
3552
- timestamp: Date.now(),
3553
- ...event.data.notification
3554
- };
3555
- setPushes((prev) => [push, ...prev]);
3556
- pwaLogger2.info("[DjangoPushProvider] Push received:", push);
3557
- }
3558
- }, "handleMessage");
3559
- navigator.serviceWorker.addEventListener("message", handleMessage);
3560
- return () => navigator.serviceWorker.removeEventListener("message", handleMessage);
3561
- }, []);
3562
- const sendPush = useCallback(
3563
- async (message) => {
3564
- await djangoPush.sendTestPush({
3565
- title: message.title,
3566
- body: message.body,
3567
- url: message.data?.url
3568
- });
3569
- },
3570
- [djangoPush]
3571
- );
3572
- const clearPushes = useCallback(() => {
3573
- setPushes([]);
3574
- pwaLogger2.info("[DjangoPushProvider] Push history cleared");
3575
- }, []);
3576
- const removePush = useCallback((id) => {
3577
- setPushes((prev) => prev.filter((p) => p.id !== id));
3578
- pwaLogger2.info("[DjangoPushProvider] Push removed:", id);
3579
- }, []);
3580
- const value = {
3581
- ...djangoPush,
3582
- pushes,
3583
- sendPush,
3584
- clearPushes,
3585
- removePush
3586
- };
3587
- return /* @__PURE__ */ jsx(DjangoPushContext.Provider, { value, children });
3588
- }
3589
- __name(DjangoPushProvider, "DjangoPushProvider");
3590
- function useDjangoPushContext() {
3591
- const context = useContext(DjangoPushContext);
3592
- if (context === void 0) {
3593
- throw new Error("useDjangoPushContext must be used within DjangoPushProvider");
3594
- }
3595
- return context;
3596
- }
3597
- __name(useDjangoPushContext, "useDjangoPushContext");
3598
-
3599
- // src/snippets/PushNotifications/utils/localStorage.ts
3600
- var STORAGE_KEYS2 = {
3601
- PUSH_DISMISSED: "pwa_push_dismissed_at"
3602
- };
3603
- function isPushDismissedRecently(resetDays = 7) {
3604
- return isDismissedRecentlyHelper2(resetDays, STORAGE_KEYS2.PUSH_DISMISSED);
3605
- }
3606
- __name(isPushDismissedRecently, "isPushDismissedRecently");
3607
- function markPushDismissed() {
3608
- if (typeof window === "undefined") return;
3609
- try {
3610
- localStorage.setItem(STORAGE_KEYS2.PUSH_DISMISSED, Date.now().toString());
3611
- } catch {
3612
- }
3613
- }
3614
- __name(markPushDismissed, "markPushDismissed");
3615
- function isDismissedRecentlyHelper2(resetDays, key) {
3616
- if (typeof window === "undefined") return false;
3617
- try {
3618
- const dismissed = localStorage.getItem(key);
3619
- if (!dismissed) return false;
3620
- const dismissedAt = parseInt(dismissed, 10);
3621
- const daysSince = (Date.now() - dismissedAt) / (1e3 * 60 * 60 * 24);
3622
- return daysSince < resetDays;
3623
- } catch {
3624
- return false;
3625
- }
3626
- }
3627
- __name(isDismissedRecentlyHelper2, "isDismissedRecentlyHelper");
3628
- function clearAllPushData() {
3629
- if (typeof window === "undefined") return;
3630
- try {
3631
- localStorage.removeItem(STORAGE_KEYS2.PUSH_DISMISSED);
3632
- } catch {
3633
- }
3634
- }
3635
- __name(clearAllPushData, "clearAllPushData");
3636
- var DEFAULT_RESET_DAYS2 = 7;
3637
- function PushPrompt({
3638
- vapidPublicKey,
3639
- subscribeEndpoint = "/api/push/subscribe",
3640
- requirePWA = true,
3641
- delayMs = 5e3,
3642
- resetAfterDays = DEFAULT_RESET_DAYS2,
3643
- onEnabled,
3644
- onDismissed
3645
- }) {
3646
- const { isAuthenticated, isLoading: isAuthLoading } = useAuth();
3647
- const { isSupported, permission, isSubscribed, subscribe } = usePushNotifications({
3648
- vapidPublicKey,
3649
- subscribeEndpoint
3650
- });
3651
- const [show, setShow] = useState(false);
3652
- const [enabling, setEnabling] = useState(false);
3653
- useEffect(() => {
3654
- if (isAuthLoading || !isAuthenticated) {
3655
- return;
3656
- }
3657
- if (!isSupported || isSubscribed || permission === "denied") {
3658
- return;
3659
- }
3660
- if (requirePWA && !isStandalone2()) {
3661
- return;
3662
- }
3663
- if (typeof window !== "undefined") {
3664
- if (isPushDismissedRecently(resetAfterDays)) {
3665
- return;
3666
- }
3667
- }
3668
- const timer = setTimeout(() => setShow(true), delayMs);
3669
- return () => clearTimeout(timer);
3670
- }, [isAuthLoading, isAuthenticated, isSupported, isSubscribed, permission, requirePWA, resetAfterDays, delayMs]);
3671
- const handleEnable = /* @__PURE__ */ __name(async () => {
3672
- setEnabling(true);
3673
- try {
3674
- const success = await subscribe();
3675
- if (success) {
3676
- setShow(false);
3677
- onEnabled?.();
3678
- }
3679
- } catch (error) {
3680
- pwaLogger2.error("[PushPrompt] Enable failed:", error);
3681
- } finally {
3682
- setEnabling(false);
3683
- }
3684
- }, "handleEnable");
3685
- const handleDismiss = /* @__PURE__ */ __name(() => {
3686
- setShow(false);
3687
- markPushDismissed();
3688
- onDismissed?.();
3689
- }, "handleDismiss");
3690
- if (!show) return null;
3691
- return /* @__PURE__ */ jsx("div", { className: "fixed bottom-4 left-4 right-4 z-50 animate-in slide-in-from-bottom-4 duration-300", children: /* @__PURE__ */ jsx("div", { className: "bg-zinc-900 border border-zinc-700 rounded-lg p-4 shadow-lg", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
3692
- /* @__PURE__ */ jsx("div", { className: "flex-shrink-0", children: /* @__PURE__ */ jsx(Bell, { className: "w-5 h-5 text-blue-400" }) }),
3693
- /* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
3694
- /* @__PURE__ */ jsx("p", { className: "text-sm font-medium text-white mb-1", children: "Enable notifications" }),
3695
- /* @__PURE__ */ jsx("p", { className: "text-xs text-zinc-400 mb-3", children: "Stay updated with important updates and alerts" }),
3696
- /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
3697
- /* @__PURE__ */ jsx(
3698
- Button,
3699
- {
3700
- onClick: handleEnable,
3701
- loading: enabling,
3702
- size: "sm",
3703
- variant: "default",
3704
- children: "Enable"
3705
- }
3706
- ),
3707
- /* @__PURE__ */ jsx(
3708
- Button,
3709
- {
3710
- onClick: handleDismiss,
3711
- size: "sm",
3712
- variant: "ghost",
3713
- children: "Not now"
3714
- }
3715
- )
3716
- ] })
3717
- ] }),
3718
- /* @__PURE__ */ jsx(
3719
- Button,
3720
- {
3721
- onClick: handleDismiss,
3722
- size: "sm",
3723
- variant: "ghost",
3724
- className: "flex-shrink-0 p-1",
3725
- "aria-label": "Dismiss",
3726
- children: /* @__PURE__ */ jsx(X, { className: "w-4 h-4" })
3727
- }
3728
- )
3729
- ] }) }) });
3730
- }
3731
- __name(PushPrompt, "PushPrompt");
3732
-
3733
- // src/snippets/PushNotifications/config.ts
3734
- var DEFAULT_VAPID_PUBLIC_KEY = process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY || "";
3735
-
3736
- export { A2HSHint, AIChatProvider, AIChatWidget, AIMessageInput, AUTH_EVENTS, Analytics, AnalyticsCategory, AnalyticsEvent, AnalyticsProvider, AskAIButton, AuthDialog, ChatPanel, DEFAULT_VAPID_PUBLIC_KEY, DIALOG_EVENTS, DesktopGuide, DjangoPushProvider, IOSGuide, MessageBubble, PushPrompt, DjangoPushProvider as PushProvider, PwaProvider, VapidKeyError, clearAllPWAInstallData, clearAllPushData, clearIsPWACache, generateBreadcrumbsFromPath, getDisplayMode, getVapidKeyInfo, hasValidManifest, isMobileDevice, isStandalone, isStandaloneReliable, isValidVapidKey, onDisplayModeChange, safeUrlBase64ToUint8Array, urlBase64ToUint8Array, useAIChat, useAIChatContext, useAIChatContextOptional, useAnalytics, useAuthDialog, useChatLayout, useDjangoPush, useDjangoPushContext, useInstall, useIsPWA, useMcpChat, useDjangoPushContext as usePush, usePushNotifications };
3737
- //# sourceMappingURL=snippets.mjs.map
3738
- //# sourceMappingURL=snippets.mjs.map