@djangocfg/layouts 2.1.226 → 2.1.228
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.
- package/README.md +3 -17
- package/package.json +18 -18
- package/src/components/errors/ErrorLayout.tsx +2 -2
- package/src/components/errors/ErrorsTracker/index.ts +1 -0
- package/src/components/errors/ErrorsTracker/utils/formatters.ts +23 -1
- package/src/hooks/useLogout.ts +9 -12
- package/src/layouts/AppLayout/AppLayout.tsx +20 -8
- package/src/layouts/AppLayout/BaseApp.tsx +5 -28
- package/src/layouts/AuthLayout/AuthLayout.tsx +51 -22
- package/src/layouts/AuthLayout/README.md +78 -0
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +2 -2
- package/src/layouts/AuthLayout/components/shared/AuthError.tsx +10 -2
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +2 -2
- package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +3 -2
- package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +4 -1
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +2 -2
- package/src/layouts/AuthLayout/components/shared/index.ts +0 -2
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +25 -80
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +8 -13
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +2 -2
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +2 -2
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +2 -2
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +61 -42
- package/src/layouts/AuthLayout/context.tsx +0 -2
- package/src/layouts/AuthLayout/index.ts +9 -6
- package/src/layouts/AuthLayout/styles/auth.css +265 -120
- package/src/layouts/AuthLayout/types.ts +60 -7
- package/src/layouts/ProfileLayout/.claude/.sidecar/activity.jsonl +2 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/history/2026-03-15.md +35 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/review.md +35 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/scan.log +3 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-001.md +18 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-002.md +19 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-003.md +18 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-004.md +18 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/tasks/T-005.md +18 -0
- package/src/layouts/ProfileLayout/.claude/.sidecar/usage.json +5 -0
- package/src/layouts/ProfileLayout/ProfileLayout.tsx +52 -403
- package/src/layouts/ProfileLayout/components/ActionButton.tsx +38 -0
- package/src/layouts/ProfileLayout/components/DeleteAccountSection.tsx +109 -148
- package/src/layouts/ProfileLayout/components/EditableField.tsx +119 -0
- package/src/layouts/ProfileLayout/components/Section.tsx +22 -0
- package/src/layouts/ProfileLayout/components/index.ts +4 -1
- package/src/layouts/ProfileLayout/context.tsx +31 -0
- package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +2 -2
- package/src/layouts/PublicLayout/components/PublicNavigation.tsx +2 -2
- package/src/layouts/_components/UserMenu.tsx +2 -2
- package/src/layouts/types/README.md +0 -20
- package/src/layouts/types/index.ts +2 -2
- package/src/layouts/types/layout.types.ts +2 -5
- package/src/layouts/types/providers.types.ts +0 -27
- package/src/snippets/AuthDialog/AuthDialog.tsx +2 -2
- package/src/snippets/Breadcrumbs.tsx +2 -2
- package/src/snippets/index.ts +0 -67
- package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +0 -56
- package/src/snippets/McpChat/README.md +0 -441
- package/src/snippets/McpChat/components/AIChatWidget.tsx +0 -361
- package/src/snippets/McpChat/components/AskAIButton.tsx +0 -92
- package/src/snippets/McpChat/components/ChatMessages.tsx +0 -138
- package/src/snippets/McpChat/components/ChatPanel.tsx +0 -131
- package/src/snippets/McpChat/components/ChatSidebar.tsx +0 -156
- package/src/snippets/McpChat/components/ChatWidget.tsx +0 -115
- package/src/snippets/McpChat/components/MessageBubble.tsx +0 -142
- package/src/snippets/McpChat/components/MessageInput.tsx +0 -140
- package/src/snippets/McpChat/components/index.ts +0 -24
- package/src/snippets/McpChat/config.ts +0 -94
- package/src/snippets/McpChat/context/AIChatContext.tsx +0 -327
- package/src/snippets/McpChat/context/ChatContext.tsx +0 -361
- package/src/snippets/McpChat/context/index.ts +0 -7
- package/src/snippets/McpChat/hooks/index.ts +0 -6
- package/src/snippets/McpChat/hooks/useAIChat.ts +0 -503
- package/src/snippets/McpChat/hooks/useChatLayout.ts +0 -442
- package/src/snippets/McpChat/hooks/useMcpChat.ts +0 -90
- package/src/snippets/McpChat/index.ts +0 -79
- package/src/snippets/McpChat/types.ts +0 -189
- package/src/snippets/PWAInstall/@docs/README.md +0 -92
- package/src/snippets/PWAInstall/@docs/research/ios-android-install-flows.md +0 -576
- package/src/snippets/PWAInstall/README.md +0 -235
- package/src/snippets/PWAInstall/components/A2HSHint.tsx +0 -236
- package/src/snippets/PWAInstall/components/DesktopGuide.tsx +0 -234
- package/src/snippets/PWAInstall/components/IOSGuide.tsx +0 -29
- package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +0 -103
- package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +0 -103
- package/src/snippets/PWAInstall/components/PWAPageResumeManager.tsx +0 -33
- package/src/snippets/PWAInstall/context/InstallContext.tsx +0 -102
- package/src/snippets/PWAInstall/hooks/useInstallPrompt.ts +0 -168
- package/src/snippets/PWAInstall/hooks/useIsPWA.ts +0 -116
- package/src/snippets/PWAInstall/hooks/usePWAPageResume.ts +0 -163
- package/src/snippets/PWAInstall/index.ts +0 -80
- package/src/snippets/PWAInstall/types/components.ts +0 -95
- package/src/snippets/PWAInstall/types/config.ts +0 -29
- package/src/snippets/PWAInstall/types/index.ts +0 -26
- package/src/snippets/PWAInstall/types/install.ts +0 -38
- package/src/snippets/PWAInstall/types/platform.ts +0 -29
- package/src/snippets/PWAInstall/utils/localStorage.ts +0 -181
- package/src/snippets/PWAInstall/utils/logger.ts +0 -149
- package/src/snippets/PWAInstall/utils/platform.ts +0 -151
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
-
|
|
5
|
-
import { useLocalStorage } from '@djangocfg/ui-core/hooks';
|
|
6
|
-
|
|
7
|
-
import { fabConfig, sidebarConfig, storageKeys } from '../config';
|
|
8
|
-
|
|
9
|
-
import type { ChatDisplayMode } from '../types';
|
|
10
|
-
// Re-export for convenience
|
|
11
|
-
export const MIN_SIDEBAR_WIDTH = sidebarConfig.minWidth;
|
|
12
|
-
export const MAX_SIDEBAR_WIDTH = sidebarConfig.maxWidth;
|
|
13
|
-
export const DEFAULT_SIDEBAR_WIDTH = sidebarConfig.defaultWidth;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Configuration for chat layout management
|
|
17
|
-
*/
|
|
18
|
-
export interface ChatLayoutConfig {
|
|
19
|
-
/** Initial width of sidebar in pixels */
|
|
20
|
-
initialWidth?: number;
|
|
21
|
-
/** Animation duration in ms */
|
|
22
|
-
animationDuration?: number;
|
|
23
|
-
/** Element to push (defaults to body) */
|
|
24
|
-
pushTarget?: 'body' | 'main' | string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Return type for useChatLayout hook
|
|
29
|
-
*/
|
|
30
|
-
export interface UseChatLayoutReturn {
|
|
31
|
-
/** Current sidebar width */
|
|
32
|
-
sidebarWidth: number;
|
|
33
|
-
/** Apply layout changes for mode */
|
|
34
|
-
applyLayout: (mode: ChatDisplayMode) => void;
|
|
35
|
-
/** Reset layout to default */
|
|
36
|
-
resetLayout: () => void;
|
|
37
|
-
/** Update sidebar width (for resize) */
|
|
38
|
-
updateWidth: (width: number) => void;
|
|
39
|
-
/** Start resize operation */
|
|
40
|
-
startResize: (e: React.MouseEvent) => void;
|
|
41
|
-
/** Whether currently resizing */
|
|
42
|
-
isResizing: boolean;
|
|
43
|
-
/** Get CSS for sidebar container */
|
|
44
|
-
getSidebarStyles: () => React.CSSProperties;
|
|
45
|
-
/** Get CSS for floating container */
|
|
46
|
-
getFloatingStyles: (position: 'bottom-right' | 'bottom-left') => React.CSSProperties;
|
|
47
|
-
/** Get CSS for FAB button */
|
|
48
|
-
getFabStyles: (position: 'bottom-right' | 'bottom-left') => React.CSSProperties;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const DEFAULT_CONFIG: Required<ChatLayoutConfig> = {
|
|
52
|
-
initialWidth: sidebarConfig.defaultWidth,
|
|
53
|
-
animationDuration: sidebarConfig.animationDuration,
|
|
54
|
-
pushTarget: 'body',
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** Stored original styles for fixed elements */
|
|
58
|
-
interface FixedElementOriginalStyles {
|
|
59
|
-
element: HTMLElement;
|
|
60
|
-
right: string;
|
|
61
|
-
transition: string;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Hook for managing chat layout embedding modes
|
|
66
|
-
*
|
|
67
|
-
* Handles:
|
|
68
|
-
* - Sidebar mode: pushes content left by adding margin to target element
|
|
69
|
-
* AND automatically adjusts all position:fixed elements with right:0
|
|
70
|
-
* - Floating mode: positions chat at bottom-right/left
|
|
71
|
-
* - Closed mode: just shows FAB button
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* ```tsx
|
|
75
|
-
* const { applyLayout, getSidebarStyles, getFloatingStyles } = useChatLayout({
|
|
76
|
-
* sidebarWidth: 400,
|
|
77
|
-
* });
|
|
78
|
-
*
|
|
79
|
-
* useEffect(() => {
|
|
80
|
-
* applyLayout(displayMode);
|
|
81
|
-
* }, [displayMode]);
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export function useChatLayout(config?: ChatLayoutConfig): UseChatLayoutReturn {
|
|
85
|
-
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
86
|
-
const { initialWidth, animationDuration, pushTarget } = mergedConfig;
|
|
87
|
-
|
|
88
|
-
// Sidebar width with localStorage persistence
|
|
89
|
-
const [storedWidth, setStoredWidth] = useLocalStorage<number>(storageKeys.sidebarWidth, initialWidth);
|
|
90
|
-
|
|
91
|
-
// Clamp stored width to valid range
|
|
92
|
-
const sidebarWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, storedWidth));
|
|
93
|
-
const sidebarWidthRef = useRef(sidebarWidth);
|
|
94
|
-
|
|
95
|
-
// Resizing state (runtime only, not persisted)
|
|
96
|
-
const [isResizing, setIsResizing] = useState(false);
|
|
97
|
-
|
|
98
|
-
// Keep ref in sync
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
sidebarWidthRef.current = sidebarWidth;
|
|
101
|
-
}, [sidebarWidth]);
|
|
102
|
-
|
|
103
|
-
// Store original styles for cleanup
|
|
104
|
-
const originalStylesRef = useRef<{
|
|
105
|
-
marginRight?: string;
|
|
106
|
-
overflowX?: string;
|
|
107
|
-
transition?: string;
|
|
108
|
-
} | null>(null);
|
|
109
|
-
|
|
110
|
-
// Store original styles for fixed elements
|
|
111
|
-
const fixedElementsRef = useRef<FixedElementOriginalStyles[]>([]);
|
|
112
|
-
|
|
113
|
-
// Current mode for cleanup
|
|
114
|
-
const currentModeRef = useRef<ChatDisplayMode>('closed');
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Get the target element to push
|
|
118
|
-
*/
|
|
119
|
-
const getTargetElement = useCallback((): HTMLElement | null => {
|
|
120
|
-
if (typeof window === 'undefined') return null;
|
|
121
|
-
|
|
122
|
-
if (pushTarget === 'body') {
|
|
123
|
-
return document.body;
|
|
124
|
-
} else if (pushTarget === 'main') {
|
|
125
|
-
return document.querySelector('main');
|
|
126
|
-
} else {
|
|
127
|
-
return document.querySelector(pushTarget);
|
|
128
|
-
}
|
|
129
|
-
}, [pushTarget]);
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Find all fixed/sticky elements that need right adjustment
|
|
133
|
-
*/
|
|
134
|
-
const getFixedElements = useCallback((): HTMLElement[] => {
|
|
135
|
-
if (typeof window === 'undefined') return [];
|
|
136
|
-
|
|
137
|
-
const elements: HTMLElement[] = [];
|
|
138
|
-
const allElements = document.querySelectorAll('*');
|
|
139
|
-
|
|
140
|
-
allElements.forEach((el) => {
|
|
141
|
-
if (!(el instanceof HTMLElement)) return;
|
|
142
|
-
// Skip chat sidebar itself
|
|
143
|
-
if (el.closest('[data-chat-sidebar-panel]')) return;
|
|
144
|
-
|
|
145
|
-
const style = window.getComputedStyle(el);
|
|
146
|
-
const position = style.position;
|
|
147
|
-
const right = style.right;
|
|
148
|
-
|
|
149
|
-
// Check for fixed/sticky elements with right: 0
|
|
150
|
-
if ((position === 'fixed' || position === 'sticky') && right === '0px') {
|
|
151
|
-
elements.push(el);
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
return elements;
|
|
156
|
-
}, []);
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Save original styles before modification
|
|
160
|
-
*/
|
|
161
|
-
const saveOriginalStyles = useCallback((element: HTMLElement) => {
|
|
162
|
-
if (!originalStylesRef.current) {
|
|
163
|
-
originalStylesRef.current = {
|
|
164
|
-
marginRight: element.style.marginRight,
|
|
165
|
-
overflowX: element.style.overflowX,
|
|
166
|
-
transition: element.style.transition,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
}, []);
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Restore original styles
|
|
173
|
-
*/
|
|
174
|
-
const restoreOriginalStyles = useCallback((element: HTMLElement) => {
|
|
175
|
-
if (originalStylesRef.current) {
|
|
176
|
-
element.style.marginRight = originalStylesRef.current.marginRight || '';
|
|
177
|
-
element.style.overflowX = originalStylesRef.current.overflowX || '';
|
|
178
|
-
element.style.transition = originalStylesRef.current.transition || '';
|
|
179
|
-
element.removeAttribute('data-chat-sidebar');
|
|
180
|
-
originalStylesRef.current = null;
|
|
181
|
-
}
|
|
182
|
-
}, []);
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Adjust fixed elements for sidebar
|
|
186
|
-
*/
|
|
187
|
-
const adjustFixedElements = useCallback(
|
|
188
|
-
(open: boolean) => {
|
|
189
|
-
const currentWidth = sidebarWidthRef.current;
|
|
190
|
-
if (open) {
|
|
191
|
-
// Save and adjust fixed elements
|
|
192
|
-
const fixedElements = getFixedElements();
|
|
193
|
-
fixedElementsRef.current = fixedElements.map((el) => ({
|
|
194
|
-
element: el,
|
|
195
|
-
right: el.style.right,
|
|
196
|
-
transition: el.style.transition,
|
|
197
|
-
}));
|
|
198
|
-
|
|
199
|
-
fixedElements.forEach((el) => {
|
|
200
|
-
el.style.transition = `right ${animationDuration}ms ease`;
|
|
201
|
-
el.style.right = `${currentWidth}px`;
|
|
202
|
-
});
|
|
203
|
-
} else {
|
|
204
|
-
// Restore fixed elements
|
|
205
|
-
fixedElementsRef.current.forEach(({ element, right, transition }) => {
|
|
206
|
-
element.style.transition = `right ${animationDuration}ms ease`;
|
|
207
|
-
element.style.right = '0px';
|
|
208
|
-
|
|
209
|
-
// Restore original after animation
|
|
210
|
-
setTimeout(() => {
|
|
211
|
-
element.style.right = right;
|
|
212
|
-
element.style.transition = transition;
|
|
213
|
-
}, animationDuration);
|
|
214
|
-
});
|
|
215
|
-
fixedElementsRef.current = [];
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
[getFixedElements, animationDuration]
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Apply sidebar mode layout
|
|
223
|
-
*/
|
|
224
|
-
const applySidebarLayout = useCallback(() => {
|
|
225
|
-
const target = getTargetElement();
|
|
226
|
-
if (!target) return;
|
|
227
|
-
|
|
228
|
-
const currentWidth = sidebarWidthRef.current;
|
|
229
|
-
|
|
230
|
-
saveOriginalStyles(target);
|
|
231
|
-
|
|
232
|
-
// Add smooth transition
|
|
233
|
-
target.style.transition = `margin-right ${animationDuration}ms ease`;
|
|
234
|
-
target.style.marginRight = `${currentWidth}px`;
|
|
235
|
-
target.style.overflowX = 'hidden';
|
|
236
|
-
target.setAttribute('data-chat-sidebar', 'open');
|
|
237
|
-
|
|
238
|
-
// Adjust fixed elements (header, etc.)
|
|
239
|
-
adjustFixedElements(true);
|
|
240
|
-
|
|
241
|
-
currentModeRef.current = 'sidebar';
|
|
242
|
-
}, [getTargetElement, saveOriginalStyles, animationDuration, adjustFixedElements]);
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Apply floating/closed mode layout (reset sidebar push)
|
|
246
|
-
*/
|
|
247
|
-
const applyDefaultLayout = useCallback(
|
|
248
|
-
(mode: ChatDisplayMode) => {
|
|
249
|
-
const target = getTargetElement();
|
|
250
|
-
if (!target) return;
|
|
251
|
-
|
|
252
|
-
// Only restore if we were in sidebar mode
|
|
253
|
-
if (currentModeRef.current === 'sidebar') {
|
|
254
|
-
// Add transition for smooth animation
|
|
255
|
-
target.style.transition = `margin-right ${animationDuration}ms ease`;
|
|
256
|
-
target.style.marginRight = '0px';
|
|
257
|
-
|
|
258
|
-
// Restore fixed elements
|
|
259
|
-
adjustFixedElements(false);
|
|
260
|
-
|
|
261
|
-
// Remove styles after animation completes
|
|
262
|
-
setTimeout(() => {
|
|
263
|
-
restoreOriginalStyles(target);
|
|
264
|
-
}, animationDuration);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
currentModeRef.current = mode;
|
|
268
|
-
},
|
|
269
|
-
[getTargetElement, restoreOriginalStyles, animationDuration, adjustFixedElements]
|
|
270
|
-
);
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Apply layout changes for given mode
|
|
274
|
-
*/
|
|
275
|
-
const applyLayout = useCallback(
|
|
276
|
-
(mode: ChatDisplayMode) => {
|
|
277
|
-
if (mode === 'sidebar') {
|
|
278
|
-
applySidebarLayout();
|
|
279
|
-
} else {
|
|
280
|
-
applyDefaultLayout(mode);
|
|
281
|
-
}
|
|
282
|
-
},
|
|
283
|
-
[applySidebarLayout, applyDefaultLayout]
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* Reset layout to default (cleanup)
|
|
288
|
-
*/
|
|
289
|
-
const resetLayout = useCallback(() => {
|
|
290
|
-
const target = getTargetElement();
|
|
291
|
-
if (target && originalStylesRef.current) {
|
|
292
|
-
restoreOriginalStyles(target);
|
|
293
|
-
}
|
|
294
|
-
// Restore fixed elements immediately on cleanup
|
|
295
|
-
fixedElementsRef.current.forEach(({ element, right, transition }) => {
|
|
296
|
-
element.style.right = right;
|
|
297
|
-
element.style.transition = transition;
|
|
298
|
-
});
|
|
299
|
-
fixedElementsRef.current = [];
|
|
300
|
-
currentModeRef.current = 'closed';
|
|
301
|
-
}, [getTargetElement, restoreOriginalStyles]);
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Update width during resize (no animation)
|
|
305
|
-
*/
|
|
306
|
-
const updateWidthImmediate = useCallback(
|
|
307
|
-
(newWidth: number) => {
|
|
308
|
-
const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, newWidth));
|
|
309
|
-
|
|
310
|
-
// Update body margin
|
|
311
|
-
const target = getTargetElement();
|
|
312
|
-
if (target && currentModeRef.current === 'sidebar') {
|
|
313
|
-
target.style.transition = 'none';
|
|
314
|
-
target.style.marginRight = `${clampedWidth}px`;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
// Update fixed elements
|
|
318
|
-
fixedElementsRef.current.forEach(({ element }) => {
|
|
319
|
-
element.style.transition = 'none';
|
|
320
|
-
element.style.right = `${clampedWidth}px`;
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
return clampedWidth;
|
|
324
|
-
},
|
|
325
|
-
[getTargetElement]
|
|
326
|
-
);
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Update sidebar width (for resize)
|
|
330
|
-
*/
|
|
331
|
-
const updateWidth = useCallback(
|
|
332
|
-
(newWidth: number) => {
|
|
333
|
-
const clampedWidth = updateWidthImmediate(newWidth);
|
|
334
|
-
setStoredWidth(clampedWidth);
|
|
335
|
-
},
|
|
336
|
-
[updateWidthImmediate, setStoredWidth]
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Start resize operation
|
|
341
|
-
*/
|
|
342
|
-
const startResize = useCallback(
|
|
343
|
-
(e: React.MouseEvent) => {
|
|
344
|
-
e.preventDefault();
|
|
345
|
-
setIsResizing(true);
|
|
346
|
-
|
|
347
|
-
const startX = e.clientX;
|
|
348
|
-
const startWidth = sidebarWidthRef.current;
|
|
349
|
-
|
|
350
|
-
const handleMouseMove = (moveEvent: MouseEvent) => {
|
|
351
|
-
// Calculate new width (dragging left increases width)
|
|
352
|
-
const deltaX = startX - moveEvent.clientX;
|
|
353
|
-
const newWidth = startWidth + deltaX;
|
|
354
|
-
const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, newWidth));
|
|
355
|
-
|
|
356
|
-
// Update DOM immediately for smooth feel
|
|
357
|
-
updateWidthImmediate(clampedWidth);
|
|
358
|
-
sidebarWidthRef.current = clampedWidth;
|
|
359
|
-
|
|
360
|
-
// Also update React state so sidebar visually resizes
|
|
361
|
-
setStoredWidth(clampedWidth);
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
const handleMouseUp = () => {
|
|
365
|
-
setIsResizing(false);
|
|
366
|
-
document.removeEventListener('mousemove', handleMouseMove);
|
|
367
|
-
document.removeEventListener('mouseup', handleMouseUp);
|
|
368
|
-
document.body.style.cursor = '';
|
|
369
|
-
document.body.style.userSelect = '';
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
document.addEventListener('mousemove', handleMouseMove);
|
|
373
|
-
document.addEventListener('mouseup', handleMouseUp);
|
|
374
|
-
document.body.style.cursor = 'ew-resize';
|
|
375
|
-
document.body.style.userSelect = 'none';
|
|
376
|
-
},
|
|
377
|
-
[updateWidthImmediate, setStoredWidth]
|
|
378
|
-
);
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Get CSS styles for sidebar container
|
|
382
|
-
*/
|
|
383
|
-
const getSidebarStyles = useCallback((): React.CSSProperties => {
|
|
384
|
-
return {
|
|
385
|
-
position: 'fixed',
|
|
386
|
-
top: 0,
|
|
387
|
-
right: 0,
|
|
388
|
-
bottom: 0,
|
|
389
|
-
width: `${sidebarWidth}px`,
|
|
390
|
-
zIndex: sidebarConfig.zIndex,
|
|
391
|
-
};
|
|
392
|
-
}, [sidebarWidth]);
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Get CSS styles for floating container
|
|
396
|
-
*/
|
|
397
|
-
const getFloatingStyles = useCallback(
|
|
398
|
-
(position: 'bottom-right' | 'bottom-left'): React.CSSProperties => {
|
|
399
|
-
return {
|
|
400
|
-
position: 'fixed',
|
|
401
|
-
zIndex: sidebarConfig.zIndex - 50,
|
|
402
|
-
bottom: fabConfig.bottom,
|
|
403
|
-
...(position === 'bottom-right' ? { right: fabConfig.right } : { left: fabConfig.right }),
|
|
404
|
-
};
|
|
405
|
-
},
|
|
406
|
-
[]
|
|
407
|
-
);
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* Get CSS styles for FAB button
|
|
411
|
-
*/
|
|
412
|
-
const getFabStyles = useCallback(
|
|
413
|
-
(position: 'bottom-right' | 'bottom-left'): React.CSSProperties => {
|
|
414
|
-
return {
|
|
415
|
-
position: 'fixed',
|
|
416
|
-
zIndex: sidebarConfig.zIndex - 50,
|
|
417
|
-
bottom: fabConfig.bottom,
|
|
418
|
-
...(position === 'bottom-right' ? { right: fabConfig.right } : { left: fabConfig.right }),
|
|
419
|
-
};
|
|
420
|
-
},
|
|
421
|
-
[]
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
// Cleanup on unmount
|
|
425
|
-
useEffect(() => {
|
|
426
|
-
return () => {
|
|
427
|
-
resetLayout();
|
|
428
|
-
};
|
|
429
|
-
}, [resetLayout]);
|
|
430
|
-
|
|
431
|
-
return {
|
|
432
|
-
sidebarWidth,
|
|
433
|
-
applyLayout,
|
|
434
|
-
resetLayout,
|
|
435
|
-
updateWidth,
|
|
436
|
-
startResize,
|
|
437
|
-
isResizing,
|
|
438
|
-
getSidebarStyles,
|
|
439
|
-
getFloatingStyles,
|
|
440
|
-
getFabStyles,
|
|
441
|
-
};
|
|
442
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useCallback } from 'react';
|
|
4
|
-
|
|
5
|
-
import type { McpChatEventDetail, UseMcpChatReturn } from '../types';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Hook to send messages to MCP Chat from anywhere in the app
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```tsx
|
|
12
|
-
* function ErrorBoundary({ error }) {
|
|
13
|
-
* const { sendToChat } = useMcpChat();
|
|
14
|
-
*
|
|
15
|
-
* const explainError = () => {
|
|
16
|
-
* sendToChat({
|
|
17
|
-
* message: `Explain this error: ${error.message}`,
|
|
18
|
-
* context: {
|
|
19
|
-
* type: 'error',
|
|
20
|
-
* data: { error: error.stack },
|
|
21
|
-
* source: 'ErrorBoundary'
|
|
22
|
-
* }
|
|
23
|
-
* });
|
|
24
|
-
* };
|
|
25
|
-
*
|
|
26
|
-
* return <button onClick={explainError}>Explain Error</button>;
|
|
27
|
-
* }
|
|
28
|
-
* ```
|
|
29
|
-
*/
|
|
30
|
-
export function useMcpChat(): UseMcpChatReturn {
|
|
31
|
-
/**
|
|
32
|
-
* Send message to chat via CustomEvent
|
|
33
|
-
*/
|
|
34
|
-
const sendToChat = useCallback((detail: McpChatEventDetail) => {
|
|
35
|
-
if (typeof window === 'undefined') {
|
|
36
|
-
console.error('[useMcpChat] Cannot send message: window is not available');
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Create custom event
|
|
41
|
-
const event = new CustomEvent('mcp:chat:send', {
|
|
42
|
-
detail,
|
|
43
|
-
bubbles: true,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
// Set up handler confirmation listener
|
|
47
|
-
let handled = false;
|
|
48
|
-
const handleConfirmation = () => {
|
|
49
|
-
handled = true;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
window.addEventListener('mcp:chat:handled', handleConfirmation, { once: true });
|
|
53
|
-
|
|
54
|
-
// Dispatch event
|
|
55
|
-
window.dispatchEvent(event);
|
|
56
|
-
|
|
57
|
-
// Check if event was handled
|
|
58
|
-
setTimeout(() => {
|
|
59
|
-
window.removeEventListener('mcp:chat:handled', handleConfirmation);
|
|
60
|
-
|
|
61
|
-
if (!handled) {
|
|
62
|
-
const errorMessage = 'AI Chat is not available. Please make sure the chat component is loaded.';
|
|
63
|
-
console.error('[useMcpChat]', errorMessage);
|
|
64
|
-
|
|
65
|
-
// Import consola dynamically if available
|
|
66
|
-
if (typeof window !== 'undefined' && (window as any).consola) {
|
|
67
|
-
(window as any).consola.error('[useMcpChat] Chat not available');
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Show user-friendly alert
|
|
71
|
-
alert(errorMessage);
|
|
72
|
-
}
|
|
73
|
-
}, 100);
|
|
74
|
-
}, []);
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Check if chat is available
|
|
78
|
-
*/
|
|
79
|
-
const isChatAvailable = useCallback(() => {
|
|
80
|
-
if (typeof window === 'undefined') return false;
|
|
81
|
-
|
|
82
|
-
// Check if chat registered itself
|
|
83
|
-
return (window as any).__MCP_CHAT_AVAILABLE__ === true;
|
|
84
|
-
}, []);
|
|
85
|
-
|
|
86
|
-
return {
|
|
87
|
-
sendToChat,
|
|
88
|
-
isChatAvailable,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @djangocfg/mcp-chat
|
|
5
|
-
*
|
|
6
|
-
* React chat components for DjangoCFG documentation assistant.
|
|
7
|
-
* Works with @djangocfg/mcp for semantic search.
|
|
8
|
-
*
|
|
9
|
-
* @example Basic usage (standalone widget)
|
|
10
|
-
* ```tsx
|
|
11
|
-
* import { ChatWidget } from '@djangocfg/mcp-chat';
|
|
12
|
-
*
|
|
13
|
-
* export default function Layout({ children }) {
|
|
14
|
-
* return (
|
|
15
|
-
* <>
|
|
16
|
-
* {children}
|
|
17
|
-
* <ChatWidget apiEndpoint="/api/chat" />
|
|
18
|
-
* </>
|
|
19
|
-
* );
|
|
20
|
-
* }
|
|
21
|
-
* ```
|
|
22
|
-
*
|
|
23
|
-
* @example With provider (for accessing chat from multiple components)
|
|
24
|
-
* ```tsx
|
|
25
|
-
* import { ChatProvider, ChatWidget, useChatContext } from '@djangocfg/mcp-chat';
|
|
26
|
-
*
|
|
27
|
-
* function OpenChatButton() {
|
|
28
|
-
* const { openChat } = useChatContext();
|
|
29
|
-
* return <button onClick={openChat}>Ask AI</button>;
|
|
30
|
-
* }
|
|
31
|
-
*
|
|
32
|
-
* export default function Layout({ children }) {
|
|
33
|
-
* return (
|
|
34
|
-
* <ChatProvider apiEndpoint="/api/chat">
|
|
35
|
-
* {children}
|
|
36
|
-
* <OpenChatButton />
|
|
37
|
-
* <ChatWidget />
|
|
38
|
-
* </ChatProvider>
|
|
39
|
-
* );
|
|
40
|
-
* }
|
|
41
|
-
* ```
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
// Components
|
|
45
|
-
export { ChatWidget, AIChatWidget, ChatPanel, MessageBubble, AIMessageInput, AskAIButton } from './components';
|
|
46
|
-
export type {
|
|
47
|
-
ChatWidgetProps,
|
|
48
|
-
AIChatWidgetProps,
|
|
49
|
-
MessageBubbleProps,
|
|
50
|
-
AIMessageInputProps,
|
|
51
|
-
AskAIButtonProps,
|
|
52
|
-
} from './components';
|
|
53
|
-
|
|
54
|
-
// Context
|
|
55
|
-
export { AIChatProvider, useAIChatContext, useAIChatContextOptional } from './context';
|
|
56
|
-
export type {
|
|
57
|
-
AIChatContextState,
|
|
58
|
-
AIChatContextActions,
|
|
59
|
-
AIChatContextValue,
|
|
60
|
-
AIChatProviderProps,
|
|
61
|
-
} from './context';
|
|
62
|
-
|
|
63
|
-
// Hooks
|
|
64
|
-
export { useAIChat, useChatLayout, useMcpChat } from './hooks';
|
|
65
|
-
export type { ChatLayoutConfig, UseChatLayoutReturn } from './hooks';
|
|
66
|
-
|
|
67
|
-
// Types
|
|
68
|
-
export type {
|
|
69
|
-
AIChatMessage,
|
|
70
|
-
AIChatSource,
|
|
71
|
-
AIMessageRole,
|
|
72
|
-
AIChatApiResponse,
|
|
73
|
-
UseAIChatOptions,
|
|
74
|
-
UseAIChatReturn,
|
|
75
|
-
ChatWidgetConfig,
|
|
76
|
-
McpChatContextType,
|
|
77
|
-
McpChatEventDetail,
|
|
78
|
-
UseMcpChatReturn,
|
|
79
|
-
} from './types';
|