@djangocfg/layouts 2.0.6 → 2.0.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -92,28 +92,29 @@
92
92
  "check": "tsc --noEmit"
93
93
  },
94
94
  "peerDependencies": {
95
- "@djangocfg/api": "^1.4.36",
96
- "@djangocfg/centrifugo": "^1.4.36",
97
- "@djangocfg/ui": "^1.4.36",
95
+ "@djangocfg/api": "^1.4.37",
96
+ "@djangocfg/centrifugo": "^1.4.37",
97
+ "@djangocfg/ui": "^1.4.37",
98
98
  "@hookform/resolvers": "^5.2.0",
99
99
  "consola": "^3.4.2",
100
- "lucide-react": "^0.468.0",
101
- "next": "^15.4.4",
100
+ "lucide-react": "^0.545.0",
101
+ "next": ">=15.0.0",
102
102
  "p-retry": "^7.0.0",
103
- "react": "^19.1.0",
104
- "react-dom": "^19.1.0",
103
+ "react": "^19.2.0",
104
+ "react-dom": "^19.2.0",
105
105
  "react-hook-form": "7.65.0",
106
- "sonner": "2.0.6",
106
+ "sonner": "2.0.7",
107
107
  "swr": "^2.3.0",
108
- "tailwindcss": "^4.0.0",
108
+ "tailwindcss": "^4.1.14",
109
109
  "tailwindcss-animate": "^1.0.7",
110
- "zod": "^4.0.10"
110
+ "zod": "^4.1.13"
111
111
  },
112
112
  "dependencies": {
113
- "react-ga4": "^2.1.0"
113
+ "react-ga4": "^2.1.0",
114
+ "uuid": "^11.1.0"
114
115
  },
115
116
  "devDependencies": {
116
- "@djangocfg/typescript-config": "^1.4.36",
117
+ "@djangocfg/typescript-config": "^1.4.37",
117
118
  "@types/node": "^24.7.2",
118
119
  "@types/react": "19.2.2",
119
120
  "@types/react-dom": "19.2.1",
@@ -15,8 +15,9 @@ import {
15
15
  DialogTitle,
16
16
  Button,
17
17
  TokenIcon,
18
+ CopyButton,
18
19
  } from '@djangocfg/ui';
19
- import { Copy, ExternalLink, CheckCircle2, Clock, XCircle, AlertCircle, RefreshCw } from 'lucide-react';
20
+ import { ExternalLink, CheckCircle2, Clock, XCircle, AlertCircle, RefreshCw } from 'lucide-react';
20
21
  import { Hooks, api } from '@djangocfg/api';
21
22
  import type { API } from '@djangocfg/api';
22
23
  import { PAYMENT_EVENTS } from '../events';
@@ -24,7 +25,6 @@ import { PAYMENT_EVENTS } from '../events';
24
25
  export const PaymentDetailsDialog: React.FC = () => {
25
26
  const [open, setOpen] = useState(false);
26
27
  const [paymentId, setPaymentId] = useState<string | null>(null);
27
- const [copied, setCopied] = useState(false);
28
28
  const [timeLeft, setTimeLeft] = useState<string>('');
29
29
 
30
30
  // Load payment data by ID using hook
@@ -61,14 +61,6 @@ export const PaymentDetailsDialog: React.FC = () => {
61
61
  setPaymentId(null);
62
62
  };
63
63
 
64
- const handleCopyAddress = async () => {
65
- if (payment?.pay_address) {
66
- await navigator.clipboard.writeText(payment.pay_address);
67
- setCopied(true);
68
- setTimeout(() => setCopied(false), 2000);
69
- }
70
- };
71
-
72
64
  // Calculate time left until expiration
73
65
  useEffect(() => {
74
66
  if (!payment?.expires_at) return;
@@ -237,18 +229,7 @@ export const PaymentDetailsDialog: React.FC = () => {
237
229
  <div className="flex-1 p-3 bg-muted rounded-sm font-mono text-sm break-all">
238
230
  {payment.pay_address}
239
231
  </div>
240
- <Button
241
- variant="outline"
242
- size="icon"
243
- onClick={handleCopyAddress}
244
- className="shrink-0"
245
- >
246
- {copied ? (
247
- <CheckCircle2 className="h-4 w-4 text-green-500" />
248
- ) : (
249
- <Copy className="h-4 w-4" />
250
- )}
251
- </Button>
232
+ <CopyButton value={payment.pay_address} variant="outline" />
252
233
  </div>
253
234
  </div>
254
235
  )}
@@ -230,7 +230,7 @@ export const MessageList: React.FC = () => {
230
230
  }
231
231
 
232
232
  return (
233
- <ScrollArea className="h-full bg-muted/50" ref={scrollAreaRef}>
233
+ <ScrollArea className="h-full bg-muted/50" viewportRef={scrollAreaRef}>
234
234
  <div className="p-6 space-y-4" ref={scrollRef}>
235
235
  {/* Load more trigger at the top */}
236
236
  <div ref={loadMoreRef} className="h-2" />
@@ -97,7 +97,7 @@ export const TicketList: React.FC = () => {
97
97
  }
98
98
 
99
99
  return (
100
- <ScrollArea className="h-full" ref={scrollRef}>
100
+ <ScrollArea className="h-full" viewportRef={scrollRef}>
101
101
  <div className="p-4 space-y-2">
102
102
  {tickets.map((ticket, index) => (
103
103
  <div
@@ -43,7 +43,7 @@ export const MessageList: React.FC<MessageListProps> = ({
43
43
  };
44
44
 
45
45
  return (
46
- <ScrollArea className={`h-full bg-muted/50 ${className}`} ref={scrollRef}>
46
+ <ScrollArea className={`h-full bg-muted/50 ${className}`} viewportRef={scrollRef}>
47
47
  <div className="space-y-4 p-4">
48
48
  {messages.length === 0 && !isLoading ? (
49
49
  <div className="flex flex-col items-center justify-center h-full text-center py-12">
@@ -82,7 +82,7 @@ export const SessionList: React.FC<SessionListProps> = ({
82
82
  </SheetDescription>
83
83
  </SheetHeader>
84
84
 
85
- <ScrollArea className="h-[calc(100vh-120px)] mt-6" ref={scrollRef}>
85
+ <ScrollArea className="h-[calc(100vh-120px)] mt-6" viewportRef={scrollRef}>
86
86
  {isLoading ? (
87
87
  <div className="flex items-center justify-center py-12">
88
88
  <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
@@ -0,0 +1,268 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Button, Portal } from '@djangocfg/ui';
5
+ import { Zap } from 'lucide-react';
6
+ import { ChatPanel } from './ChatPanel';
7
+ import { ChatSidebar } from './ChatSidebar';
8
+ import { useAIChatContext, useAIChatContextOptional, AIChatProvider } from '../context/AIChatContext';
9
+ import { useChatLayout } from '../hooks/useChatLayout';
10
+ import { mcpEndpoints, type ChatWidgetConfig } from '../types';
11
+
12
+ // CSS for mysterious rotating border animation with multi-color ethereal effects
13
+ const fabAnimationStyles = `
14
+ @keyframes rotate-gradient {
15
+ 0% { transform: rotate(0deg); }
16
+ 100% { transform: rotate(360deg); }
17
+ }
18
+ @keyframes color-shift-glow {
19
+ 0%, 100% {
20
+ box-shadow:
21
+ 0 0 15px rgba(251, 191, 36, 0.4),
22
+ 0 0 30px rgba(168, 85, 247, 0.2),
23
+ 0 0 45px rgba(20, 184, 166, 0.1);
24
+ }
25
+ 25% {
26
+ box-shadow:
27
+ 0 0 18px rgba(168, 85, 247, 0.4),
28
+ 0 0 35px rgba(20, 184, 166, 0.25),
29
+ 0 0 50px rgba(251, 191, 36, 0.1);
30
+ }
31
+ 50% {
32
+ box-shadow:
33
+ 0 0 20px rgba(20, 184, 166, 0.4),
34
+ 0 0 40px rgba(251, 191, 36, 0.2),
35
+ 0 0 55px rgba(168, 85, 247, 0.15);
36
+ }
37
+ 75% {
38
+ box-shadow:
39
+ 0 0 17px rgba(236, 72, 153, 0.35),
40
+ 0 0 32px rgba(168, 85, 247, 0.25),
41
+ 0 0 48px rgba(20, 184, 166, 0.1);
42
+ }
43
+ }
44
+ @keyframes icon-pulse {
45
+ 0%, 100% {
46
+ opacity: 1;
47
+ transform: scale(1);
48
+ filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.5));
49
+ }
50
+ 50% {
51
+ opacity: 0.85;
52
+ transform: scale(0.95);
53
+ filter: drop-shadow(0 0 6px rgba(251, 191, 36, 0.8));
54
+ }
55
+ }
56
+ `;
57
+
58
+ export interface AIChatWidgetProps extends ChatWidgetConfig {
59
+ /** Custom class name for the container */
60
+ className?: string;
61
+ /** Enable streaming responses (default: true) */
62
+ enableStreaming?: boolean;
63
+ }
64
+
65
+ /**
66
+ * Internal AI chat widget that uses context
67
+ */
68
+ const AIChatWidgetInternal = React.memo<{ className?: string }>(({ className }) => {
69
+ const {
70
+ messages,
71
+ isLoading,
72
+ isMinimized,
73
+ config,
74
+ displayMode,
75
+ isMobile,
76
+ sendMessage,
77
+ openChat,
78
+ closeChat,
79
+ toggleMinimize,
80
+ setDisplayMode,
81
+ stopStreaming,
82
+ } = useAIChatContext();
83
+
84
+ // Use layout hook for consistent positioning
85
+ const { getFabStyles, getFloatingStyles } = useChatLayout();
86
+
87
+ const position = config.position || 'bottom-right';
88
+ const fabStyles = getFabStyles(position);
89
+ const floatingStyles = getFloatingStyles(position);
90
+
91
+ // Mode: closed - just show FAB with multi-color rotating border animation
92
+ if (displayMode === 'closed') {
93
+ return (
94
+ <Portal>
95
+ <style>{fabAnimationStyles}</style>
96
+ <div style={fabStyles} className={className || ''}>
97
+ {/* Outer glow container with color-shifting animation */}
98
+ <div
99
+ className="relative rounded-full"
100
+ style={{
101
+ width: '64px',
102
+ height: '64px',
103
+ animation: 'color-shift-glow 6s ease-in-out infinite',
104
+ }}
105
+ >
106
+ {/* Border container - clips the rotating gradient */}
107
+ <div
108
+ className="absolute rounded-full group"
109
+ style={{
110
+ inset: '0',
111
+ overflow: 'hidden',
112
+ }}
113
+ >
114
+ {/* Rotating conic gradient - multi-color organic glow */}
115
+ <div
116
+ className="absolute"
117
+ style={{
118
+ width: '200%',
119
+ height: '200%',
120
+ top: '-50%',
121
+ left: '-50%',
122
+ background: `conic-gradient(
123
+ from 0deg,
124
+ #fbbf24 0%,
125
+ transparent 8%,
126
+ #a855f7 18%,
127
+ transparent 28%,
128
+ #14b8a6 38%,
129
+ transparent 52%,
130
+ #ec4899 62%,
131
+ transparent 75%,
132
+ #fbbf24 88%,
133
+ transparent 95%,
134
+ #a855f7 100%
135
+ )`,
136
+ animation: 'rotate-gradient 5s linear infinite',
137
+ opacity: 0.9,
138
+ }}
139
+ />
140
+ {/* Inner mask - creates the thin organic border */}
141
+ <div
142
+ className="absolute rounded-full bg-background"
143
+ style={{ inset: '1.5px' }}
144
+ />
145
+ {/* Main FAB button */}
146
+ <Button
147
+ onClick={openChat}
148
+ variant="ghost"
149
+ className="absolute rounded-full hover:scale-105 transition-all duration-300 bg-background hover:bg-background/95 border-0"
150
+ style={{
151
+ inset: '1.5px',
152
+ width: 'auto',
153
+ height: 'auto',
154
+ }}
155
+ >
156
+ <Zap
157
+ className="h-6 w-6"
158
+ style={{
159
+ animation: 'icon-pulse 2s ease-in-out infinite',
160
+ color: '#fbbf24',
161
+ fill: '#fbbf24',
162
+ }}
163
+ />
164
+ </Button>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </Portal>
169
+ );
170
+ }
171
+
172
+ // Mode: sidebar - full-height panel on the right (desktop only)
173
+ if (displayMode === 'sidebar') {
174
+ return (
175
+ <Portal>
176
+ <ChatSidebar
177
+ messages={messages}
178
+ isLoading={isLoading}
179
+ onSendMessage={sendMessage}
180
+ onClose={closeChat}
181
+ onModeChange={setDisplayMode}
182
+ onStopStreaming={stopStreaming}
183
+ title={config.title}
184
+ placeholder={config.placeholder}
185
+ greeting={config.greeting}
186
+ />
187
+ </Portal>
188
+ );
189
+ }
190
+
191
+ // Mode: floating - floating panel
192
+ return (
193
+ <Portal>
194
+ <div style={floatingStyles} className={className || ''}>
195
+ <ChatPanel
196
+ messages={messages}
197
+ isLoading={isLoading}
198
+ onSendMessage={sendMessage}
199
+ onClose={closeChat}
200
+ onMinimize={toggleMinimize}
201
+ onModeChange={setDisplayMode}
202
+ onStopStreaming={stopStreaming}
203
+ isMinimized={isMinimized}
204
+ isMobile={isMobile}
205
+ title={config.title}
206
+ placeholder={config.placeholder}
207
+ greeting={config.greeting}
208
+ />
209
+ </div>
210
+ </Portal>
211
+ );
212
+ });
213
+
214
+ AIChatWidgetInternal.displayName = 'AIChatWidgetInternal';
215
+
216
+ /**
217
+ * AI Chat Widget component
218
+ *
219
+ * AI-powered documentation assistant with streaming support.
220
+ * Uses Mastra agent backend for intelligent responses.
221
+ *
222
+ * Can be used in two ways:
223
+ * 1. Standalone (wraps itself in AIChatProvider)
224
+ * 2. Inside an AIChatProvider (uses context directly)
225
+ *
226
+ * @example
227
+ * ```tsx
228
+ * // Standalone usage
229
+ * <AIChatWidget apiEndpoint="/api/ai/chat" />
230
+ *
231
+ * // With provider for custom control
232
+ * <AIChatProvider apiEndpoint="/api/ai/chat">
233
+ * <MyApp />
234
+ * <AIChatWidget />
235
+ * </AIChatProvider>
236
+ * ```
237
+ */
238
+ export const AIChatWidget: React.FC<AIChatWidgetProps> = ({
239
+ apiEndpoint = mcpEndpoints.chat,
240
+ title = 'DjangoCFG AI Assistant',
241
+ placeholder = 'Ask about DjangoCFG...',
242
+ greeting = "Hi! I'm your DjangoCFG AI assistant powered by GPT. Ask me anything about configuration, features, or how to use the library.",
243
+ position = 'bottom-right',
244
+ variant = 'default',
245
+ className,
246
+ enableStreaming = true,
247
+ }) => {
248
+ // Check if we're inside an AIChatProvider
249
+ const existingContext = useAIChatContextOptional();
250
+
251
+ // If already in context, use internal widget directly
252
+ if (existingContext) {
253
+ return <AIChatWidgetInternal className={className} />;
254
+ }
255
+
256
+ // Otherwise, wrap in provider
257
+ return (
258
+ <AIChatProvider
259
+ apiEndpoint={apiEndpoint}
260
+ config={{ title, placeholder, greeting, position, variant }}
261
+ enableStreaming={enableStreaming}
262
+ >
263
+ <AIChatWidgetInternal className={className} />
264
+ </AIChatProvider>
265
+ );
266
+ };
267
+
268
+ AIChatWidget.displayName = 'AIChatWidget';
@@ -0,0 +1,151 @@
1
+ 'use client';
2
+
3
+ import React, { useRef, useEffect, useCallback, useImperativeHandle, forwardRef } from 'react';
4
+ import { ScrollArea, Button, type ScrollAreaHandle } from '@djangocfg/ui';
5
+ import { Bot, MessageSquare, StopCircle } from 'lucide-react';
6
+ import { MessageBubble } from './MessageBubble';
7
+ import type { AIChatMessage } from '../types';
8
+
9
+ /**
10
+ * ChatMessages imperative handle
11
+ */
12
+ export interface ChatMessagesHandle {
13
+ scrollToBottom: (instant?: boolean) => void;
14
+ }
15
+
16
+ export interface ChatMessagesProps {
17
+ /** Messages to display */
18
+ messages: AIChatMessage[];
19
+ /** Whether loading/streaming is in progress */
20
+ isLoading: boolean;
21
+ /** Greeting to show when no messages */
22
+ greeting?: string;
23
+ /** Callback to stop streaming */
24
+ onStopStreaming?: () => void;
25
+ /** Use compact layout (smaller bubbles) */
26
+ isCompact?: boolean;
27
+ /** Use larger icon for greeting (sidebar mode) */
28
+ largeGreetingIcon?: boolean;
29
+ /** Custom greeting icon */
30
+ greetingIcon?: 'bot' | 'message';
31
+ /** Custom greeting title */
32
+ greetingTitle?: string;
33
+ }
34
+
35
+ /**
36
+ * ChatMessages - Shared component for displaying chat messages
37
+ *
38
+ * Handles:
39
+ * - Messages list rendering
40
+ * - Greeting display when empty
41
+ * - Loading indicator with stop button
42
+ * - Auto-scroll on new messages
43
+ * - Initial scroll on history load
44
+ */
45
+ export const ChatMessages = forwardRef<ChatMessagesHandle, ChatMessagesProps>(
46
+ (
47
+ {
48
+ messages,
49
+ isLoading,
50
+ greeting,
51
+ onStopStreaming,
52
+ isCompact = false,
53
+ largeGreetingIcon = false,
54
+ greetingIcon = 'bot',
55
+ greetingTitle,
56
+ },
57
+ ref
58
+ ) => {
59
+ const scrollAreaRef = useRef<ScrollAreaHandle>(null);
60
+ const hasScrolledOnLoad = useRef(false);
61
+
62
+ // Scroll to bottom helper
63
+ const scrollToBottom = useCallback((instant = false) => {
64
+ if (scrollAreaRef.current) {
65
+ scrollAreaRef.current.scrollToBottom(instant ? 'instant' : 'smooth');
66
+ }
67
+ }, []);
68
+
69
+ // Expose scrollToBottom via ref
70
+ useImperativeHandle(ref, () => ({
71
+ scrollToBottom,
72
+ }), [scrollToBottom]);
73
+
74
+ // Initial scroll when history loads (instant, no animation)
75
+ useEffect(() => {
76
+ if (!isLoading && messages.length > 0 && !hasScrolledOnLoad.current) {
77
+ requestAnimationFrame(() => {
78
+ scrollToBottom(true);
79
+ hasScrolledOnLoad.current = true;
80
+ });
81
+ }
82
+ }, [isLoading, messages.length, scrollToBottom]);
83
+
84
+ // Scroll to bottom on new messages (smooth animation)
85
+ useEffect(() => {
86
+ if (hasScrolledOnLoad.current && messages.length > 0) {
87
+ scrollToBottom(false);
88
+ }
89
+ }, [messages, scrollToBottom]);
90
+
91
+ const GreetingIcon = greetingIcon === 'message' ? MessageSquare : Bot;
92
+ const iconSize = largeGreetingIcon ? { container: '64px', icon: 'h-8 w-8' } : { container: '48px', icon: 'h-6 w-6' };
93
+ const padding = largeGreetingIcon ? 'py-12' : 'py-8';
94
+
95
+ return (
96
+ <ScrollArea ref={scrollAreaRef} className="h-full">
97
+ <div className={`${isCompact ? 'p-3' : 'p-4'} space-y-4`}>
98
+ {/* Greeting */}
99
+ {messages.length === 0 && greeting && (
100
+ <div className={`text-center ${padding}`}>
101
+ <div
102
+ className="mx-auto mb-4 rounded-full bg-primary/10 flex items-center justify-center"
103
+ style={{ width: iconSize.container, height: iconSize.container }}
104
+ >
105
+ <GreetingIcon className={`${iconSize.icon} text-primary`} />
106
+ </div>
107
+ {greetingTitle && (
108
+ <h4 className="font-medium mb-2">{greetingTitle}</h4>
109
+ )}
110
+ <p className={`text-sm text-muted-foreground ${largeGreetingIcon ? 'max-w-[300px]' : 'max-w-[280px]'} mx-auto`}>
111
+ {greeting}
112
+ </p>
113
+ </div>
114
+ )}
115
+
116
+ {/* Messages */}
117
+ {messages.map((message) => (
118
+ <MessageBubble key={message.id} message={message} isCompact={isCompact} />
119
+ ))}
120
+
121
+ {/* Loading indicator with stop button */}
122
+ {isLoading && messages.length > 0 && (
123
+ <div className="flex items-center justify-between text-muted-foreground text-sm">
124
+ <div className="flex items-center gap-2">
125
+ <div className="flex gap-1">
126
+ <span className="animate-bounce" style={{ animationDelay: '0ms' }}>.</span>
127
+ <span className="animate-bounce" style={{ animationDelay: '150ms' }}>.</span>
128
+ <span className="animate-bounce" style={{ animationDelay: '300ms' }}>.</span>
129
+ </div>
130
+ <span>Generating response...</span>
131
+ </div>
132
+ {onStopStreaming && (
133
+ <Button
134
+ variant="ghost"
135
+ size="sm"
136
+ onClick={onStopStreaming}
137
+ className="h-6 px-2 text-xs"
138
+ >
139
+ <StopCircle className="h-3 w-3 mr-1" />
140
+ Stop
141
+ </Button>
142
+ )}
143
+ </div>
144
+ )}
145
+ </div>
146
+ </ScrollArea>
147
+ );
148
+ }
149
+ );
150
+
151
+ ChatMessages.displayName = 'ChatMessages';
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+ import { Card, CardHeader, CardContent, CardFooter, Button } from '@djangocfg/ui';
5
+ import { X, Minimize2, PanelRight, Bot } from 'lucide-react';
6
+ import type { ChatDisplayMode, AIChatMessage } from '../types';
7
+ import { ChatMessages } from './ChatMessages';
8
+ import { AIMessageInput } from './MessageInput';
9
+
10
+ export interface ChatPanelProps {
11
+ messages: AIChatMessage[];
12
+ isLoading: boolean;
13
+ onSendMessage: (content: string) => void;
14
+ onClose?: () => void;
15
+ onMinimize?: () => void;
16
+ onModeChange?: (mode: ChatDisplayMode) => void;
17
+ onStopStreaming?: () => void;
18
+ isMinimized?: boolean;
19
+ isMobile?: boolean;
20
+ title?: string;
21
+ placeholder?: string;
22
+ greeting?: string;
23
+ }
24
+
25
+ export const ChatPanel = React.memo<ChatPanelProps>(
26
+ ({
27
+ messages,
28
+ isLoading,
29
+ onSendMessage,
30
+ onClose,
31
+ onMinimize,
32
+ onModeChange,
33
+ onStopStreaming,
34
+ isMinimized = false,
35
+ isMobile = false,
36
+ title = 'DjangoCFG Assistant',
37
+ placeholder,
38
+ greeting,
39
+ }) => {
40
+ if (isMinimized) {
41
+ return (
42
+ <Button
43
+ onClick={onMinimize}
44
+ className="rounded-full shadow-lg"
45
+ style={{ width: '56px', height: '56px' }}
46
+ >
47
+ <Bot className="h-6 w-6" />
48
+ </Button>
49
+ );
50
+ }
51
+
52
+ return (
53
+ <Card
54
+ className="flex flex-col shadow-2xl border-border/50"
55
+ style={{
56
+ width: '380px',
57
+ height: '520px',
58
+ maxHeight: 'calc(100vh - 100px)',
59
+ }}
60
+ >
61
+ {/* Header */}
62
+ <CardHeader className="flex flex-row items-center justify-between p-3 border-b">
63
+ <div className="flex items-center gap-2">
64
+ <div
65
+ className="rounded-full bg-primary/10 flex items-center justify-center"
66
+ style={{ width: '32px', height: '32px' }}
67
+ >
68
+ <Bot className="h-4 w-4 text-primary" />
69
+ </div>
70
+ <div>
71
+ <h3 className="font-semibold text-sm">{title}</h3>
72
+ <p className="text-xs text-muted-foreground">AI-powered documentation assistant</p>
73
+ </div>
74
+ </div>
75
+ <div className="flex gap-1">
76
+ {/* Sidebar mode button - only on desktop */}
77
+ {onModeChange && !isMobile && (
78
+ <Button
79
+ variant="ghost"
80
+ size="icon"
81
+ className="h-8 w-8"
82
+ onClick={() => onModeChange('sidebar')}
83
+ title="Switch to sidebar mode"
84
+ >
85
+ <PanelRight className="h-4 w-4" />
86
+ </Button>
87
+ )}
88
+ {onMinimize && (
89
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onMinimize} title="Minimize">
90
+ <Minimize2 className="h-4 w-4" />
91
+ </Button>
92
+ )}
93
+ {onClose && (
94
+ <Button variant="ghost" size="icon" className="h-8 w-8" onClick={onClose} title="Close">
95
+ <X className="h-4 w-4" />
96
+ </Button>
97
+ )}
98
+ </div>
99
+ </CardHeader>
100
+
101
+ {/* Messages */}
102
+ <CardContent className="flex-1 p-0 overflow-hidden">
103
+ <ChatMessages
104
+ messages={messages}
105
+ isLoading={isLoading}
106
+ greeting={greeting}
107
+ onStopStreaming={onStopStreaming}
108
+ isCompact
109
+ greetingIcon="bot"
110
+ />
111
+ </CardContent>
112
+
113
+ {/* Input */}
114
+ <CardFooter className="p-3 border-t">
115
+ <AIMessageInput
116
+ onSend={onSendMessage}
117
+ isLoading={isLoading}
118
+ placeholder={placeholder}
119
+ />
120
+ </CardFooter>
121
+ </Card>
122
+ );
123
+ }
124
+ );
125
+
126
+ ChatPanel.displayName = 'ChatPanel';