@djangocfg/layouts 2.0.6 → 2.0.8
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 +65 -6
- package/package.json +14 -13
- package/src/auth/hooks/index.ts +1 -0
- package/src/auth/hooks/useGithubAuth.ts +183 -0
- package/src/layouts/AuthLayout/AuthContext.tsx +2 -0
- package/src/layouts/AuthLayout/AuthLayout.tsx +22 -5
- package/src/layouts/AuthLayout/IdentifierForm.tsx +4 -0
- package/src/layouts/AuthLayout/OAuthCallback.tsx +172 -0
- package/src/layouts/AuthLayout/OAuthProviders.tsx +85 -0
- package/src/layouts/AuthLayout/index.ts +4 -0
- package/src/layouts/AuthLayout/types.ts +4 -0
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +3 -22
- package/src/layouts/SupportLayout/components/MessageList.tsx +1 -1
- package/src/layouts/SupportLayout/components/TicketList.tsx +1 -1
- package/src/snippets/Analytics/events.ts +5 -0
- package/src/snippets/Chat/components/MessageList.tsx +1 -1
- package/src/snippets/Chat/components/SessionList.tsx +1 -1
- package/src/snippets/McpChat/components/AIChatWidget.tsx +268 -0
- package/src/snippets/McpChat/components/ChatMessages.tsx +151 -0
- package/src/snippets/McpChat/components/ChatPanel.tsx +126 -0
- package/src/snippets/McpChat/components/ChatSidebar.tsx +119 -0
- package/src/snippets/McpChat/components/ChatWidget.tsx +134 -0
- package/src/snippets/McpChat/components/MessageBubble.tsx +125 -0
- package/src/snippets/McpChat/components/MessageInput.tsx +139 -0
- package/src/snippets/McpChat/components/index.ts +22 -0
- package/src/snippets/McpChat/config.ts +35 -0
- package/src/snippets/McpChat/context/AIChatContext.tsx +245 -0
- package/src/snippets/McpChat/context/ChatContext.tsx +350 -0
- package/src/snippets/McpChat/context/index.ts +7 -0
- package/src/snippets/McpChat/hooks/index.ts +5 -0
- package/src/snippets/McpChat/hooks/useAIChat.ts +487 -0
- package/src/snippets/McpChat/hooks/useChatLayout.ts +329 -0
- package/src/snippets/McpChat/index.ts +76 -0
- package/src/snippets/McpChat/types.ts +141 -0
- package/src/snippets/index.ts +32 -0
- package/src/utils/index.ts +0 -1
- package/src/utils/og-image.ts +0 -169
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useCallback, useMemo, useEffect, type ReactNode } from 'react';
|
|
4
|
+
import { useLocalStorage, useIsMobile } from '@djangocfg/ui/hooks';
|
|
5
|
+
import { useAIChat } from '../hooks/useAIChat';
|
|
6
|
+
import { mcpEndpoints, type AIChatMessage, type ChatWidgetConfig, type ChatDisplayMode } from '../types';
|
|
7
|
+
|
|
8
|
+
const STORAGE_KEY_MODE = 'djangocfg-ai-chat-mode';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AI Chat context state
|
|
12
|
+
*/
|
|
13
|
+
export interface AIChatContextState {
|
|
14
|
+
/** All chat messages */
|
|
15
|
+
messages: AIChatMessage[];
|
|
16
|
+
/** Whether a request is in progress */
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
/** Last error if any */
|
|
19
|
+
error: Error | null;
|
|
20
|
+
/** Whether chat panel is open */
|
|
21
|
+
isOpen: boolean;
|
|
22
|
+
/** Whether chat is minimized */
|
|
23
|
+
isMinimized: boolean;
|
|
24
|
+
/** Configuration */
|
|
25
|
+
config: ChatWidgetConfig;
|
|
26
|
+
/** Current display mode */
|
|
27
|
+
displayMode: ChatDisplayMode;
|
|
28
|
+
/** Is on mobile device */
|
|
29
|
+
isMobile: boolean;
|
|
30
|
+
/** Thread ID for conversation */
|
|
31
|
+
threadId: string;
|
|
32
|
+
/** User ID for conversation */
|
|
33
|
+
userId: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* AI Chat context actions
|
|
38
|
+
*/
|
|
39
|
+
export interface AIChatContextActions {
|
|
40
|
+
/** Send a message */
|
|
41
|
+
sendMessage: (content: string) => Promise<void>;
|
|
42
|
+
/** Clear all messages */
|
|
43
|
+
clearMessages: () => void;
|
|
44
|
+
/** Open chat panel */
|
|
45
|
+
openChat: () => void;
|
|
46
|
+
/** Close chat panel */
|
|
47
|
+
closeChat: () => void;
|
|
48
|
+
/** Toggle chat panel */
|
|
49
|
+
toggleChat: () => void;
|
|
50
|
+
/** Minimize/restore chat */
|
|
51
|
+
toggleMinimize: () => void;
|
|
52
|
+
/** Set display mode */
|
|
53
|
+
setDisplayMode: (mode: ChatDisplayMode) => void;
|
|
54
|
+
/** Stop streaming response */
|
|
55
|
+
stopStreaming: () => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type AIChatContextValue = AIChatContextState & AIChatContextActions;
|
|
59
|
+
|
|
60
|
+
const AIChatContext = createContext<AIChatContextValue | null>(null);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* AI Chat provider props
|
|
64
|
+
*/
|
|
65
|
+
export interface AIChatProviderProps {
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
/** API endpoint for AI chat (default: /api/ai/chat) */
|
|
68
|
+
apiEndpoint?: string;
|
|
69
|
+
/** Widget configuration */
|
|
70
|
+
config?: Partial<ChatWidgetConfig>;
|
|
71
|
+
/** Callback on error */
|
|
72
|
+
onError?: (error: Error) => void;
|
|
73
|
+
/** Enable streaming (default: true) */
|
|
74
|
+
enableStreaming?: boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* AI Chat provider component
|
|
79
|
+
* Uses useAIChat hook with server-side persistence
|
|
80
|
+
*/
|
|
81
|
+
export function AIChatProvider({
|
|
82
|
+
children,
|
|
83
|
+
apiEndpoint = mcpEndpoints.chat,
|
|
84
|
+
config: userConfig = {},
|
|
85
|
+
onError,
|
|
86
|
+
enableStreaming = true,
|
|
87
|
+
}: AIChatProviderProps) {
|
|
88
|
+
// Use AI chat hook
|
|
89
|
+
const {
|
|
90
|
+
messages,
|
|
91
|
+
isLoading,
|
|
92
|
+
error,
|
|
93
|
+
threadId,
|
|
94
|
+
userId,
|
|
95
|
+
sendMessage: sendAIMessage,
|
|
96
|
+
clearMessages: clearAIMessages,
|
|
97
|
+
stopStreaming,
|
|
98
|
+
} = useAIChat({
|
|
99
|
+
apiEndpoint,
|
|
100
|
+
onError,
|
|
101
|
+
enableStreaming,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const [isMinimized, setIsMinimized] = useState(false);
|
|
105
|
+
|
|
106
|
+
// Display mode with localStorage persistence
|
|
107
|
+
const [storedMode, setStoredMode] = useLocalStorage<ChatDisplayMode>(STORAGE_KEY_MODE, 'closed');
|
|
108
|
+
|
|
109
|
+
const isMobile = useIsMobile();
|
|
110
|
+
|
|
111
|
+
// On mobile, sidebar mode is not available - fallback to floating
|
|
112
|
+
const displayMode: ChatDisplayMode = useMemo(() => {
|
|
113
|
+
if (isMobile && storedMode === 'sidebar') {
|
|
114
|
+
return 'floating';
|
|
115
|
+
}
|
|
116
|
+
return storedMode;
|
|
117
|
+
}, [isMobile, storedMode]);
|
|
118
|
+
|
|
119
|
+
// Derived state: isOpen is true when not in 'closed' mode
|
|
120
|
+
const isOpen = displayMode !== 'closed';
|
|
121
|
+
|
|
122
|
+
const config: ChatWidgetConfig = useMemo(
|
|
123
|
+
() => ({
|
|
124
|
+
apiEndpoint,
|
|
125
|
+
title: 'DjangoCFG AI Assistant',
|
|
126
|
+
placeholder: 'Ask about DjangoCFG...',
|
|
127
|
+
greeting:
|
|
128
|
+
"Hi! I'm your DjangoCFG AI assistant powered by GPT. Ask me anything about configuration, features, or how to use the library.",
|
|
129
|
+
position: 'bottom-right',
|
|
130
|
+
variant: 'default',
|
|
131
|
+
...userConfig,
|
|
132
|
+
}),
|
|
133
|
+
[apiEndpoint, userConfig]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const sendMessage = useCallback(
|
|
137
|
+
async (content: string) => {
|
|
138
|
+
await sendAIMessage(content);
|
|
139
|
+
},
|
|
140
|
+
[sendAIMessage]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const clearMessages = useCallback(() => {
|
|
144
|
+
clearAIMessages();
|
|
145
|
+
}, [clearAIMessages]);
|
|
146
|
+
|
|
147
|
+
const openChat = useCallback(() => {
|
|
148
|
+
setStoredMode('floating');
|
|
149
|
+
setIsMinimized(false);
|
|
150
|
+
}, [setStoredMode]);
|
|
151
|
+
|
|
152
|
+
const closeChat = useCallback(() => {
|
|
153
|
+
setStoredMode('closed');
|
|
154
|
+
setIsMinimized(false);
|
|
155
|
+
}, [setStoredMode]);
|
|
156
|
+
|
|
157
|
+
const toggleChat = useCallback(() => {
|
|
158
|
+
if (displayMode === 'closed') {
|
|
159
|
+
setStoredMode('floating');
|
|
160
|
+
setIsMinimized(false);
|
|
161
|
+
} else {
|
|
162
|
+
setStoredMode('closed');
|
|
163
|
+
}
|
|
164
|
+
}, [displayMode, setStoredMode]);
|
|
165
|
+
|
|
166
|
+
const toggleMinimize = useCallback(() => {
|
|
167
|
+
setIsMinimized((prev) => !prev);
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const setDisplayMode = useCallback(
|
|
171
|
+
(mode: ChatDisplayMode) => {
|
|
172
|
+
// On mobile, sidebar is not available
|
|
173
|
+
if (isMobile && mode === 'sidebar') {
|
|
174
|
+
setStoredMode('floating');
|
|
175
|
+
} else {
|
|
176
|
+
setStoredMode(mode);
|
|
177
|
+
}
|
|
178
|
+
setIsMinimized(false);
|
|
179
|
+
},
|
|
180
|
+
[isMobile, setStoredMode]
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const value = useMemo<AIChatContextValue>(
|
|
184
|
+
() => ({
|
|
185
|
+
messages,
|
|
186
|
+
isLoading,
|
|
187
|
+
error,
|
|
188
|
+
isOpen,
|
|
189
|
+
isMinimized,
|
|
190
|
+
config,
|
|
191
|
+
displayMode,
|
|
192
|
+
isMobile,
|
|
193
|
+
threadId,
|
|
194
|
+
userId,
|
|
195
|
+
sendMessage,
|
|
196
|
+
clearMessages,
|
|
197
|
+
openChat,
|
|
198
|
+
closeChat,
|
|
199
|
+
toggleChat,
|
|
200
|
+
toggleMinimize,
|
|
201
|
+
setDisplayMode,
|
|
202
|
+
stopStreaming,
|
|
203
|
+
}),
|
|
204
|
+
[
|
|
205
|
+
messages,
|
|
206
|
+
isLoading,
|
|
207
|
+
error,
|
|
208
|
+
isOpen,
|
|
209
|
+
isMinimized,
|
|
210
|
+
config,
|
|
211
|
+
displayMode,
|
|
212
|
+
isMobile,
|
|
213
|
+
threadId,
|
|
214
|
+
userId,
|
|
215
|
+
sendMessage,
|
|
216
|
+
clearMessages,
|
|
217
|
+
openChat,
|
|
218
|
+
closeChat,
|
|
219
|
+
toggleChat,
|
|
220
|
+
toggleMinimize,
|
|
221
|
+
setDisplayMode,
|
|
222
|
+
stopStreaming,
|
|
223
|
+
]
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return <AIChatContext.Provider value={value}>{children}</AIChatContext.Provider>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Hook to access AI chat context
|
|
231
|
+
*/
|
|
232
|
+
export function useAIChatContext(): AIChatContextValue {
|
|
233
|
+
const context = useContext(AIChatContext);
|
|
234
|
+
if (!context) {
|
|
235
|
+
throw new Error('useAIChatContext must be used within an AIChatProvider');
|
|
236
|
+
}
|
|
237
|
+
return context;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Hook to check if AI chat context is available
|
|
242
|
+
*/
|
|
243
|
+
export function useAIChatContextOptional(): AIChatContextValue | null {
|
|
244
|
+
return useContext(AIChatContext);
|
|
245
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useState, useCallback, useMemo, useEffect, useRef, type ReactNode } from 'react';
|
|
4
|
+
import { useLocalStorage, useIsMobile } from '@djangocfg/ui/hooks';
|
|
5
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
6
|
+
import type { AIChatMessage, ChatApiResponse, AIChatSource, ChatWidgetConfig, ChatDisplayMode } from '../types';
|
|
7
|
+
|
|
8
|
+
const STORAGE_KEY_MODE = 'djangocfg-chat-mode';
|
|
9
|
+
const STORAGE_KEY_USER_ID = 'djangocfg-chat-user-id';
|
|
10
|
+
const STORAGE_KEY_MESSAGES = 'djangocfg-chat-messages';
|
|
11
|
+
const MAX_STORED_MESSAGES = 50;
|
|
12
|
+
|
|
13
|
+
function generateMessageId(): string {
|
|
14
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Serialized message format for localStorage
|
|
19
|
+
*/
|
|
20
|
+
interface SerializedMessage {
|
|
21
|
+
id: string;
|
|
22
|
+
role: 'user' | 'assistant' | 'system';
|
|
23
|
+
content: string;
|
|
24
|
+
timestamp: string; // ISO string
|
|
25
|
+
sources?: AIChatSource[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Chat context state
|
|
30
|
+
*/
|
|
31
|
+
export interface ChatContextState {
|
|
32
|
+
/** All chat messages */
|
|
33
|
+
messages: AIChatMessage[];
|
|
34
|
+
/** Whether a request is in progress */
|
|
35
|
+
isLoading: boolean;
|
|
36
|
+
/** Last error if any */
|
|
37
|
+
error: Error | null;
|
|
38
|
+
/** Whether chat panel is open */
|
|
39
|
+
isOpen: boolean;
|
|
40
|
+
/** Whether chat is minimized */
|
|
41
|
+
isMinimized: boolean;
|
|
42
|
+
/** Configuration */
|
|
43
|
+
config: ChatWidgetConfig;
|
|
44
|
+
/** Current display mode */
|
|
45
|
+
displayMode: ChatDisplayMode;
|
|
46
|
+
/** Is on mobile device */
|
|
47
|
+
isMobile: boolean;
|
|
48
|
+
/** User ID for this session */
|
|
49
|
+
userId: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Chat context actions
|
|
54
|
+
*/
|
|
55
|
+
export interface ChatContextActions {
|
|
56
|
+
/** Send a message */
|
|
57
|
+
sendMessage: (content: string) => Promise<void>;
|
|
58
|
+
/** Clear all messages */
|
|
59
|
+
clearMessages: () => void;
|
|
60
|
+
/** Open chat panel */
|
|
61
|
+
openChat: () => void;
|
|
62
|
+
/** Close chat panel */
|
|
63
|
+
closeChat: () => void;
|
|
64
|
+
/** Toggle chat panel */
|
|
65
|
+
toggleChat: () => void;
|
|
66
|
+
/** Minimize/restore chat */
|
|
67
|
+
toggleMinimize: () => void;
|
|
68
|
+
/** Set display mode */
|
|
69
|
+
setDisplayMode: (mode: ChatDisplayMode) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type ChatContextValue = ChatContextState & ChatContextActions;
|
|
73
|
+
|
|
74
|
+
const ChatContext = createContext<ChatContextValue | null>(null);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Chat provider props
|
|
78
|
+
*/
|
|
79
|
+
export interface ChatProviderProps {
|
|
80
|
+
children: ReactNode;
|
|
81
|
+
/** API endpoint for chat */
|
|
82
|
+
apiEndpoint?: string;
|
|
83
|
+
/** Widget configuration */
|
|
84
|
+
config?: Partial<ChatWidgetConfig>;
|
|
85
|
+
/** Callback on error */
|
|
86
|
+
onError?: (error: Error) => void;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Chat provider component
|
|
91
|
+
*/
|
|
92
|
+
export function ChatProvider({
|
|
93
|
+
children,
|
|
94
|
+
apiEndpoint = '/api/chat',
|
|
95
|
+
config: userConfig = {},
|
|
96
|
+
onError,
|
|
97
|
+
}: ChatProviderProps) {
|
|
98
|
+
const [messages, setMessages] = useState<AIChatMessage[]>([]);
|
|
99
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
100
|
+
const [error, setError] = useState<Error | null>(null);
|
|
101
|
+
const [isMinimized, setIsMinimized] = useState(false);
|
|
102
|
+
const isHydratedRef = useRef(false);
|
|
103
|
+
|
|
104
|
+
// Display mode with localStorage persistence
|
|
105
|
+
const [storedMode, setStoredMode] = useLocalStorage<ChatDisplayMode>(STORAGE_KEY_MODE, 'closed');
|
|
106
|
+
|
|
107
|
+
// User ID with localStorage persistence
|
|
108
|
+
const [userId, setUserId] = useLocalStorage<string>(STORAGE_KEY_USER_ID, '');
|
|
109
|
+
|
|
110
|
+
// Messages storage (serialized)
|
|
111
|
+
const [storedMessages, setStoredMessages] = useLocalStorage<SerializedMessage[]>(STORAGE_KEY_MESSAGES, []);
|
|
112
|
+
|
|
113
|
+
const isMobile = useIsMobile();
|
|
114
|
+
|
|
115
|
+
// Generate user ID if not exists
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!userId) {
|
|
118
|
+
setUserId(uuidv4());
|
|
119
|
+
}
|
|
120
|
+
}, [userId, setUserId]);
|
|
121
|
+
|
|
122
|
+
// Load messages from localStorage on mount (once)
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (isHydratedRef.current) return;
|
|
125
|
+
isHydratedRef.current = true;
|
|
126
|
+
|
|
127
|
+
if (storedMessages.length > 0) {
|
|
128
|
+
const hydratedMessages: AIChatMessage[] = storedMessages.map((msg) => ({
|
|
129
|
+
...msg,
|
|
130
|
+
timestamp: new Date(msg.timestamp),
|
|
131
|
+
}));
|
|
132
|
+
setMessages(hydratedMessages);
|
|
133
|
+
}
|
|
134
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
135
|
+
}, [storedMessages]); // Depend on storedMessages to wait for localStorage
|
|
136
|
+
|
|
137
|
+
// Save messages to localStorage when they change
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!isHydratedRef.current) return;
|
|
140
|
+
|
|
141
|
+
const toStore: SerializedMessage[] = messages.slice(-MAX_STORED_MESSAGES).map((msg) => ({
|
|
142
|
+
id: msg.id,
|
|
143
|
+
role: msg.role,
|
|
144
|
+
content: msg.content,
|
|
145
|
+
timestamp: msg.timestamp.toISOString(),
|
|
146
|
+
sources: msg.sources,
|
|
147
|
+
}));
|
|
148
|
+
setStoredMessages(toStore);
|
|
149
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
150
|
+
}, [messages]);
|
|
151
|
+
|
|
152
|
+
// On mobile, sidebar mode is not available - fallback to floating
|
|
153
|
+
const displayMode: ChatDisplayMode = useMemo(() => {
|
|
154
|
+
if (isMobile && storedMode === 'sidebar') {
|
|
155
|
+
return 'floating';
|
|
156
|
+
}
|
|
157
|
+
return storedMode;
|
|
158
|
+
}, [isMobile, storedMode]);
|
|
159
|
+
|
|
160
|
+
// Derived state: isOpen is true when not in 'closed' mode
|
|
161
|
+
const isOpen = displayMode !== 'closed';
|
|
162
|
+
|
|
163
|
+
const config: ChatWidgetConfig = useMemo(
|
|
164
|
+
() => ({
|
|
165
|
+
apiEndpoint,
|
|
166
|
+
title: 'DjangoCFG Assistant',
|
|
167
|
+
placeholder: 'Ask about DjangoCFG...',
|
|
168
|
+
greeting:
|
|
169
|
+
"Hi! I'm your DjangoCFG documentation assistant. Ask me anything about configuration, features, or how to use the library.",
|
|
170
|
+
position: 'bottom-right',
|
|
171
|
+
variant: 'default',
|
|
172
|
+
...userConfig,
|
|
173
|
+
}),
|
|
174
|
+
[apiEndpoint, userConfig]
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const sendMessage = useCallback(
|
|
178
|
+
async (content: string) => {
|
|
179
|
+
if (!content.trim() || isLoading) return;
|
|
180
|
+
|
|
181
|
+
const userMessage: AIChatMessage = {
|
|
182
|
+
id: generateMessageId(),
|
|
183
|
+
role: 'user',
|
|
184
|
+
content: content.trim(),
|
|
185
|
+
timestamp: new Date(),
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
setMessages((prev) => [...prev, userMessage]);
|
|
189
|
+
setIsLoading(true);
|
|
190
|
+
setError(null);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const response = await fetch(config.apiEndpoint || apiEndpoint, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({
|
|
197
|
+
query: content,
|
|
198
|
+
userId: userId || undefined,
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
throw new Error(`HTTP error: ${response.status}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const data: ChatApiResponse = await response.json();
|
|
207
|
+
|
|
208
|
+
if (!data.success) {
|
|
209
|
+
throw new Error(data.error || 'Failed to get response');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const sources: AIChatSource[] =
|
|
213
|
+
data.results?.map((r) => ({
|
|
214
|
+
title: r.chunk.title,
|
|
215
|
+
path: r.chunk.path,
|
|
216
|
+
url: r.chunk.url,
|
|
217
|
+
section: r.chunk.section,
|
|
218
|
+
score: r.score,
|
|
219
|
+
})) || [];
|
|
220
|
+
|
|
221
|
+
const assistantMessage: AIChatMessage = {
|
|
222
|
+
id: generateMessageId(),
|
|
223
|
+
role: 'assistant',
|
|
224
|
+
content: data.answer || 'I found some relevant documentation.',
|
|
225
|
+
timestamp: new Date(),
|
|
226
|
+
sources,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
setMessages((prev) => [...prev, assistantMessage]);
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const error = err instanceof Error ? err : new Error('Unknown error');
|
|
232
|
+
setError(error);
|
|
233
|
+
onError?.(error);
|
|
234
|
+
|
|
235
|
+
const errorMessage: AIChatMessage = {
|
|
236
|
+
id: generateMessageId(),
|
|
237
|
+
role: 'assistant',
|
|
238
|
+
content: `Sorry, I encountered an error: ${error.message}. Please try again.`,
|
|
239
|
+
timestamp: new Date(),
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
setMessages((prev) => [...prev, errorMessage]);
|
|
243
|
+
} finally {
|
|
244
|
+
setIsLoading(false);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
[apiEndpoint, config.apiEndpoint, isLoading, onError, userId]
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const clearMessages = useCallback(() => {
|
|
251
|
+
setMessages([]);
|
|
252
|
+
setStoredMessages([]);
|
|
253
|
+
setError(null);
|
|
254
|
+
}, [setStoredMessages]);
|
|
255
|
+
|
|
256
|
+
const openChat = useCallback(() => {
|
|
257
|
+
setStoredMode('floating');
|
|
258
|
+
setIsMinimized(false);
|
|
259
|
+
}, [setStoredMode]);
|
|
260
|
+
|
|
261
|
+
const closeChat = useCallback(() => {
|
|
262
|
+
setStoredMode('closed');
|
|
263
|
+
setIsMinimized(false);
|
|
264
|
+
}, [setStoredMode]);
|
|
265
|
+
|
|
266
|
+
const toggleChat = useCallback(() => {
|
|
267
|
+
if (displayMode === 'closed') {
|
|
268
|
+
setStoredMode('floating');
|
|
269
|
+
setIsMinimized(false);
|
|
270
|
+
} else {
|
|
271
|
+
setStoredMode('closed');
|
|
272
|
+
}
|
|
273
|
+
}, [displayMode, setStoredMode]);
|
|
274
|
+
|
|
275
|
+
const toggleMinimize = useCallback(() => {
|
|
276
|
+
setIsMinimized((prev) => !prev);
|
|
277
|
+
}, []);
|
|
278
|
+
|
|
279
|
+
const setDisplayMode = useCallback(
|
|
280
|
+
(mode: ChatDisplayMode) => {
|
|
281
|
+
// On mobile, sidebar is not available
|
|
282
|
+
if (isMobile && mode === 'sidebar') {
|
|
283
|
+
setStoredMode('floating');
|
|
284
|
+
} else {
|
|
285
|
+
setStoredMode(mode);
|
|
286
|
+
}
|
|
287
|
+
setIsMinimized(false);
|
|
288
|
+
},
|
|
289
|
+
[isMobile, setStoredMode]
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const value = useMemo<ChatContextValue>(
|
|
293
|
+
() => ({
|
|
294
|
+
messages,
|
|
295
|
+
isLoading,
|
|
296
|
+
error,
|
|
297
|
+
isOpen,
|
|
298
|
+
isMinimized,
|
|
299
|
+
config,
|
|
300
|
+
displayMode,
|
|
301
|
+
isMobile,
|
|
302
|
+
userId: userId || '',
|
|
303
|
+
sendMessage,
|
|
304
|
+
clearMessages,
|
|
305
|
+
openChat,
|
|
306
|
+
closeChat,
|
|
307
|
+
toggleChat,
|
|
308
|
+
toggleMinimize,
|
|
309
|
+
setDisplayMode,
|
|
310
|
+
}),
|
|
311
|
+
[
|
|
312
|
+
messages,
|
|
313
|
+
isLoading,
|
|
314
|
+
error,
|
|
315
|
+
isOpen,
|
|
316
|
+
isMinimized,
|
|
317
|
+
config,
|
|
318
|
+
displayMode,
|
|
319
|
+
isMobile,
|
|
320
|
+
userId,
|
|
321
|
+
sendMessage,
|
|
322
|
+
clearMessages,
|
|
323
|
+
openChat,
|
|
324
|
+
closeChat,
|
|
325
|
+
toggleChat,
|
|
326
|
+
toggleMinimize,
|
|
327
|
+
setDisplayMode,
|
|
328
|
+
]
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Hook to access chat context
|
|
336
|
+
*/
|
|
337
|
+
export function useChatContext(): ChatContextValue {
|
|
338
|
+
const context = useContext(ChatContext);
|
|
339
|
+
if (!context) {
|
|
340
|
+
throw new Error('useChatContext must be used within a ChatProvider');
|
|
341
|
+
}
|
|
342
|
+
return context;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Hook to check if chat context is available
|
|
347
|
+
*/
|
|
348
|
+
export function useChatContextOptional(): ChatContextValue | null {
|
|
349
|
+
return useContext(ChatContext);
|
|
350
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export { ChatProvider, useChatContext, useChatContextOptional } from './ChatContext';
|
|
4
|
+
export type { ChatContextState, ChatContextActions, ChatContextValue, ChatProviderProps } from './ChatContext';
|
|
5
|
+
|
|
6
|
+
export { AIChatProvider, useAIChatContext, useAIChatContextOptional } from './AIChatContext';
|
|
7
|
+
export type { AIChatContextState, AIChatContextActions, AIChatContextValue, AIChatProviderProps } from './AIChatContext';
|