Skip to content

Commit 9b7bf49

Browse files
authored
Merge pull request #86 from Open-STEM/ng-AI
Ng ai added session IDs
2 parents 0a98eba + 9385ec6 commit 9b7bf49

File tree

2 files changed

+89
-12
lines changed

2 files changed

+89
-12
lines changed

src/components/chat/ai-chat.tsx

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,65 @@ export default function AIChat() {
1313
const [streamingMessage, setStreamingMessage] = useState<ChatMessage | null>(null);
1414
const [contextStatus, setContextStatus] = useState<'idle' | 'loading' | 'loaded' | 'error'>('idle');
1515
const [textQueue, setTextQueue] = useState<string>('');
16+
const [sessionId, setSessionId] = useState<string>('');
1617

1718
const messagesEndRef = useRef<HTMLDivElement>(null);
1819
const textareaRef = useRef<HTMLTextAreaElement>(null);
1920
const geminiClient = useRef<GeminiClient | null>(null);
2021
const contextLoader = useRef<GeminiContextLoader | null>(null);
2122
const abortController = useRef<AbortController | null>(null);
2223

23-
// Initialize client and context loader on component mount
24+
// Initialize client, context loader, and session on component mount
2425
useEffect(() => {
2526
geminiClient.current = new GeminiClient();
2627
contextLoader.current = createContextLoader();
27-
28+
29+
// Generate a unique session ID for this chat session
30+
const newSessionId = uuidv4();
31+
console.log(`[AIChat] Generated new session ID: ${newSessionId}`);
32+
setSessionId(newSessionId);
33+
2834
// Ensure documentation is loaded via backend when chat opens
2935
const initDocs = async () => {
30-
if (!geminiClient.current) return;
36+
if (!geminiClient.current || !newSessionId) return;
3137
setContextStatus('loading');
32-
const status = await geminiClient.current.getDocsStatus();
38+
const status = await geminiClient.current.getDocsStatus(newSessionId);
3339
if (!status.loaded) {
34-
const res = await geminiClient.current.loadDocs();
40+
const res = await geminiClient.current.loadDocs(newSessionId);
3541
setContextStatus(res.success ? 'loaded' : 'error');
3642
} else {
3743
setContextStatus('loaded');
3844
}
3945
};
4046
initDocs();
47+
48+
// Cleanup function when component unmounts (user closes chat or leaves IDE)
49+
return () => {
50+
if (geminiClient.current && newSessionId) {
51+
console.log(`[AIChat] Component unmounting, cleaning up session ${newSessionId.substring(0, 8)}...`);
52+
geminiClient.current.cleanupSession(newSessionId);
53+
}
54+
};
4155
}, []);
4256

57+
// Cleanup session when user closes browser tab or navigates away
58+
useEffect(() => {
59+
const handleBeforeUnload = () => {
60+
if (sessionId) {
61+
// Use sendBeacon for more reliable cleanup on page unload (uses POST)
62+
const url = `/api/session/${sessionId}`;
63+
navigator.sendBeacon(url);
64+
console.log(`[AIChat] Page unloading, sent cleanup beacon for session ${sessionId.substring(0, 8)}...`);
65+
}
66+
};
67+
68+
window.addEventListener('beforeunload', handleBeforeUnload);
69+
70+
return () => {
71+
window.removeEventListener('beforeunload', handleBeforeUnload);
72+
};
73+
}, [sessionId]);
74+
4375
// Scroll to bottom when messages change
4476
useEffect(() => {
4577
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
@@ -118,7 +150,9 @@ export default function AIChat() {
118150
const terminalContext = contextLoader.current?.getCurrentTerminalContext() || '';
119151

120152
// Use the new simplified chat API - all teaching guidelines are now in backend
153+
console.log(`[AIChat] Sending message with session ${sessionId.substring(0, 8)}... (history: ${messages.length} messages)`);
121154
await geminiClient.current.chatWithContext(
155+
sessionId,
122156
userMessage.content,
123157
messages, // Conversation history
124158
editorContext,

src/utils/gemini-client.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ export class GeminiClient {
1212
}
1313

1414
/**
15-
* Check if combined documentation has been loaded on the backend
15+
* Check if combined documentation has been loaded on the backend for this session
1616
*/
17-
async getDocsStatus(): Promise<{ loaded: boolean; uri?: string }> {
17+
async getDocsStatus(sessionId: string): Promise<{ loaded: boolean; uri?: string }> {
1818
try {
19-
const response = await fetch(`${this.backendUrl}/docs/status`);
19+
const response = await fetch(`${this.backendUrl}/docs/status`, {
20+
method: 'POST',
21+
headers: {
22+
'Content-Type': 'application/json',
23+
},
24+
body: JSON.stringify({ session_id: sessionId })
25+
});
2026
if (!response.ok) {
2127
throw new Error(`Failed to get docs status: ${response.statusText}`);
2228
}
@@ -29,12 +35,16 @@ export class GeminiClient {
2935
}
3036

3137
/**
32-
* Request backend to load combined documentation into model context
38+
* Request backend to load combined documentation into model context for this session
3339
*/
34-
async loadDocs(): Promise<{ success: boolean; status: string; uri?: string }> {
40+
async loadDocs(sessionId: string): Promise<{ success: boolean; status: string; uri?: string }> {
3541
try {
3642
const response = await fetch(`${this.backendUrl}/docs/load`, {
37-
method: 'POST'
43+
method: 'POST',
44+
headers: {
45+
'Content-Type': 'application/json',
46+
},
47+
body: JSON.stringify({ session_id: sessionId })
3848
});
3949
if (!response.ok) {
4050
const err = await response.json().catch(() => ({}));
@@ -65,10 +75,30 @@ export class GeminiClient {
6575
return 'XRPCode Buddy'; // Fallback
6676
}
6777

78+
/**
79+
* Clean up a session on the backend
80+
*/
81+
async cleanupSession(sessionId: string): Promise<void> {
82+
try {
83+
const response = await fetch(`${this.backendUrl}/session/${sessionId}`, {
84+
method: 'DELETE'
85+
});
86+
if (response.ok) {
87+
const data = await response.json();
88+
console.log(`Session ${sessionId.substring(0, 8)}... cleaned up:`, data.message);
89+
} else {
90+
console.warn(`Failed to cleanup session ${sessionId.substring(0, 8)}...`);
91+
}
92+
} catch (error) {
93+
console.warn('Failed to cleanup session on backend:', error);
94+
}
95+
}
96+
6897
/**
6998
* Send a simplified chat request with user message and context
7099
*/
71100
async chatWithContext(
101+
sessionId: string,
72102
userMessage: string,
73103
conversationHistory: ChatMessage[] = [],
74104
editorContext: string = '',
@@ -84,6 +114,7 @@ export class GeminiClient {
84114

85115
// Prepare simplified request payload
86116
const payload = {
117+
session_id: sessionId,
87118
user_message: userMessage,
88119
conversation_history: conversationHistory.map(msg => ({
89120
role: msg.role === 'assistant' ? 'model' : msg.role,
@@ -232,15 +263,27 @@ export class GeminiClient {
232263
signal?: AbortSignal
233264
): Promise<string> {
234265
console.warn('chatCompletion is deprecated, use chatWithContext instead');
235-
266+
236267
if (messages.length === 0) {
237268
throw new Error('No messages provided');
238269
}
239270

271+
// Generate a temporary session ID for legacy calls
272+
const randomArray = new Uint32Array(2);
273+
(typeof window !== 'undefined' && window.crypto
274+
? window.crypto.getRandomValues(randomArray)
275+
: (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function'
276+
? crypto.getRandomValues(randomArray)
277+
: (() => { throw new Error('No secure random generator available'); })()
278+
)
279+
);
280+
const randomString = Array.from(randomArray).map(n => n.toString(36)).join('').substr(0, 9);
281+
const tempSessionId = `legacy-${Date.now()}-${randomString}`;
240282
const userMessage = messages[messages.length - 1].content;
241283
const conversationHistory = messages.slice(0, -1);
242284

243285
return this.chatWithContext(
286+
tempSessionId,
244287
userMessage,
245288
conversationHistory,
246289
'', // No editor context in legacy mode

0 commit comments

Comments
 (0)