2025-04-11 21:52:54 +00:00
|
|
|
/**
|
|
|
|
* Communication functions for LLM Chat
|
|
|
|
*/
|
|
|
|
import server from "../../services/server.js";
|
|
|
|
import type { SessionResponse } from "./types.js";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a new chat session
|
2025-06-02 00:56:19 +00:00
|
|
|
* @param currentNoteId - Optional current note ID for context
|
|
|
|
* @returns The noteId of the created chat note
|
2025-04-11 21:52:54 +00:00
|
|
|
*/
|
2025-06-02 00:56:19 +00:00
|
|
|
export async function createChatSession(currentNoteId?: string): Promise<string | null> {
|
2025-04-11 21:52:54 +00:00
|
|
|
try {
|
2025-04-16 21:43:19 +00:00
|
|
|
const resp = await server.post<SessionResponse>('llm/chat', {
|
2025-04-16 21:53:12 +00:00
|
|
|
title: 'Note Chat',
|
|
|
|
currentNoteId: currentNoteId // Pass the current note ID if available
|
2025-04-11 21:52:54 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (resp && resp.id) {
|
2025-06-02 00:56:19 +00:00
|
|
|
// Backend returns the chat note ID as 'id'
|
|
|
|
return resp.id;
|
2025-04-11 21:52:54 +00:00
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Failed to create chat session:', error);
|
|
|
|
}
|
|
|
|
|
2025-06-02 00:56:19 +00:00
|
|
|
return null;
|
2025-04-11 21:52:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-06-02 00:56:19 +00:00
|
|
|
* Check if a chat note exists
|
|
|
|
* @param noteId - The ID of the chat note
|
2025-04-11 21:52:54 +00:00
|
|
|
*/
|
2025-06-02 00:56:19 +00:00
|
|
|
export async function checkSessionExists(noteId: string): Promise<boolean> {
|
2025-04-11 21:52:54 +00:00
|
|
|
try {
|
2025-06-02 00:56:19 +00:00
|
|
|
const sessionCheck = await server.getWithSilentNotFound<any>(`llm/chat/${noteId}`);
|
2025-04-11 21:52:54 +00:00
|
|
|
return !!(sessionCheck && sessionCheck.id);
|
2025-04-13 21:43:58 +00:00
|
|
|
} catch (error: any) {
|
2025-06-02 00:56:19 +00:00
|
|
|
console.log(`Error checking chat note ${noteId}:`, error);
|
2025-04-11 21:52:54 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set up streaming response via WebSocket
|
2025-06-02 00:56:19 +00:00
|
|
|
* @param noteId - The ID of the chat note
|
|
|
|
* @param messageParams - Message parameters
|
|
|
|
* @param onContentUpdate - Callback for content updates
|
|
|
|
* @param onThinkingUpdate - Callback for thinking updates
|
|
|
|
* @param onToolExecution - Callback for tool execution
|
|
|
|
* @param onComplete - Callback for completion
|
|
|
|
* @param onError - Callback for errors
|
2025-04-11 21:52:54 +00:00
|
|
|
*/
|
|
|
|
export async function setupStreamingResponse(
|
2025-06-02 00:56:19 +00:00
|
|
|
noteId: string,
|
2025-04-11 21:52:54 +00:00
|
|
|
messageParams: any,
|
2025-04-13 21:16:18 +00:00
|
|
|
onContentUpdate: (content: string, isDone?: boolean) => void,
|
2025-04-11 21:52:54 +00:00
|
|
|
onThinkingUpdate: (thinking: string) => void,
|
|
|
|
onToolExecution: (toolData: any) => void,
|
|
|
|
onComplete: () => void,
|
|
|
|
onError: (error: Error) => void
|
|
|
|
): Promise<void> {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
let assistantResponse = '';
|
|
|
|
let receivedAnyContent = false;
|
|
|
|
let timeoutId: number | null = null;
|
|
|
|
let initialTimeoutId: number | null = null;
|
2025-04-15 22:22:31 +00:00
|
|
|
let cleanupTimeoutId: number | null = null;
|
2025-04-11 21:52:54 +00:00
|
|
|
let receivedAnyMessage = false;
|
|
|
|
let eventListener: ((event: Event) => void) | null = null;
|
2025-04-15 22:22:31 +00:00
|
|
|
let lastMessageTimestamp = 0;
|
2025-04-11 21:52:54 +00:00
|
|
|
|
|
|
|
// Create a unique identifier for this response process
|
|
|
|
const responseId = `llm-stream-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
|
2025-06-02 00:56:19 +00:00
|
|
|
console.log(`[${responseId}] Setting up WebSocket streaming for chat note ${noteId}`);
|
2025-04-11 21:52:54 +00:00
|
|
|
|
2025-04-16 21:53:12 +00:00
|
|
|
// Send the initial request to initiate streaming
|
|
|
|
(async () => {
|
|
|
|
try {
|
2025-06-02 00:56:19 +00:00
|
|
|
const streamResponse = await server.post<any>(`llm/chat/${noteId}/messages/stream`, {
|
2025-04-16 21:53:12 +00:00
|
|
|
content: messageParams.content,
|
2025-04-16 22:56:47 +00:00
|
|
|
useAdvancedContext: messageParams.useAdvancedContext,
|
|
|
|
showThinking: messageParams.showThinking,
|
2025-04-16 21:53:12 +00:00
|
|
|
options: {
|
|
|
|
temperature: 0.7,
|
|
|
|
maxTokens: 2000
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!streamResponse || !streamResponse.success) {
|
|
|
|
console.error(`[${responseId}] Failed to initiate streaming`);
|
|
|
|
reject(new Error('Failed to initiate streaming'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`[${responseId}] Streaming initiated successfully`);
|
|
|
|
} catch (error) {
|
|
|
|
console.error(`[${responseId}] Error initiating streaming:`, error);
|
|
|
|
reject(error);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
|
2025-04-15 22:22:31 +00:00
|
|
|
// Function to safely perform cleanup
|
|
|
|
const performCleanup = () => {
|
|
|
|
if (cleanupTimeoutId) {
|
|
|
|
window.clearTimeout(cleanupTimeoutId);
|
|
|
|
cleanupTimeoutId = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`[${responseId}] Performing final cleanup of event listener`);
|
|
|
|
cleanupEventListener(eventListener);
|
|
|
|
onComplete();
|
|
|
|
resolve();
|
|
|
|
};
|
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
// Set initial timeout to catch cases where no message is received at all
|
|
|
|
initialTimeoutId = window.setTimeout(() => {
|
|
|
|
if (!receivedAnyMessage) {
|
|
|
|
console.error(`[${responseId}] No initial message received within timeout`);
|
|
|
|
performCleanup();
|
|
|
|
reject(new Error('No response received from server'));
|
2025-04-15 22:22:31 +00:00
|
|
|
}
|
2025-06-02 23:25:15 +00:00
|
|
|
}, 10000);
|
2025-04-15 22:22:31 +00:00
|
|
|
|
2025-04-11 21:52:54 +00:00
|
|
|
// Create a message handler for CustomEvents
|
|
|
|
eventListener = (event: Event) => {
|
|
|
|
const customEvent = event as CustomEvent;
|
|
|
|
const message = customEvent.detail;
|
|
|
|
|
2025-04-16 21:43:19 +00:00
|
|
|
// Only process messages for our chat note
|
2025-06-02 00:56:19 +00:00
|
|
|
if (!message || message.chatNoteId !== noteId) {
|
2025-04-11 21:52:54 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-04-15 22:22:31 +00:00
|
|
|
// Update last message timestamp
|
|
|
|
lastMessageTimestamp = Date.now();
|
|
|
|
|
|
|
|
// Cancel any pending cleanup when we receive a new message
|
|
|
|
if (cleanupTimeoutId) {
|
|
|
|
console.log(`[${responseId}] Cancelling scheduled cleanup due to new message`);
|
|
|
|
window.clearTimeout(cleanupTimeoutId);
|
|
|
|
cleanupTimeoutId = null;
|
|
|
|
}
|
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
console.log(`[${responseId}] LLM Stream message received: content=${!!message.content}, contentLength=${message.content?.length || 0}, thinking=${!!message.thinking}, toolExecution=${!!message.toolExecution}, done=${!!message.done}`);
|
2025-04-11 21:52:54 +00:00
|
|
|
|
|
|
|
// Mark first message received
|
|
|
|
if (!receivedAnyMessage) {
|
|
|
|
receivedAnyMessage = true;
|
2025-06-02 00:56:19 +00:00
|
|
|
console.log(`[${responseId}] First message received for chat note ${noteId}`);
|
2025-04-11 21:52:54 +00:00
|
|
|
|
|
|
|
// Clear the initial timeout since we've received a message
|
|
|
|
if (initialTimeoutId !== null) {
|
|
|
|
window.clearTimeout(initialTimeoutId);
|
|
|
|
initialTimeoutId = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
// Handle error
|
|
|
|
if (message.error) {
|
|
|
|
console.error(`[${responseId}] Stream error: ${message.error}`);
|
|
|
|
performCleanup();
|
|
|
|
reject(new Error(message.error));
|
|
|
|
return;
|
2025-04-13 20:12:17 +00:00
|
|
|
}
|
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
// Handle thinking updates - only show if showThinking is enabled
|
|
|
|
if (message.thinking && messageParams.showThinking) {
|
|
|
|
console.log(`[${responseId}] Received thinking: ${message.thinking.substring(0, 100)}...`);
|
|
|
|
onThinkingUpdate(message.thinking);
|
2025-04-13 20:12:17 +00:00
|
|
|
}
|
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
// Handle tool execution updates
|
|
|
|
if (message.toolExecution) {
|
|
|
|
console.log(`[${responseId}] Tool execution update:`, message.toolExecution);
|
|
|
|
onToolExecution(message.toolExecution);
|
2025-04-13 20:12:17 +00:00
|
|
|
}
|
|
|
|
|
2025-04-11 21:52:54 +00:00
|
|
|
// Handle content updates
|
|
|
|
if (message.content) {
|
2025-06-02 23:25:15 +00:00
|
|
|
// Simply append the new content - no complex deduplication
|
|
|
|
assistantResponse += message.content;
|
2025-04-11 21:52:54 +00:00
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
// Update the UI immediately with each chunk
|
|
|
|
onContentUpdate(assistantResponse, message.done || false);
|
2025-04-15 22:22:31 +00:00
|
|
|
receivedAnyContent = true;
|
2025-04-11 21:52:54 +00:00
|
|
|
|
|
|
|
// Reset timeout since we got content
|
|
|
|
if (timeoutId !== null) {
|
|
|
|
window.clearTimeout(timeoutId);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set new timeout
|
|
|
|
timeoutId = window.setTimeout(() => {
|
2025-06-02 00:56:19 +00:00
|
|
|
console.warn(`[${responseId}] Stream timeout for chat note ${noteId}`);
|
2025-04-15 22:22:31 +00:00
|
|
|
performCleanup();
|
2025-04-11 21:52:54 +00:00
|
|
|
reject(new Error('Stream timeout'));
|
|
|
|
}, 30000);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Handle completion
|
|
|
|
if (message.done) {
|
2025-06-02 23:25:15 +00:00
|
|
|
console.log(`[${responseId}] Stream completed for chat note ${noteId}, final response: ${assistantResponse.length} chars`);
|
2025-04-11 21:52:54 +00:00
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
// Clear all timeouts
|
2025-04-11 21:52:54 +00:00
|
|
|
if (timeoutId !== null) {
|
|
|
|
window.clearTimeout(timeoutId);
|
|
|
|
timeoutId = null;
|
|
|
|
}
|
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
// Schedule cleanup after a brief delay to ensure all processing is complete
|
|
|
|
cleanupTimeoutId = window.setTimeout(() => {
|
|
|
|
performCleanup();
|
|
|
|
}, 100);
|
2025-04-11 21:52:54 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
// Register the event listener for WebSocket messages
|
|
|
|
window.addEventListener('llm-stream-message', eventListener);
|
2025-04-11 21:52:54 +00:00
|
|
|
|
2025-06-02 23:25:15 +00:00
|
|
|
console.log(`[${responseId}] Event listener registered, waiting for messages...`);
|
2025-04-11 21:52:54 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clean up an event listener
|
|
|
|
*/
|
|
|
|
function cleanupEventListener(listener: ((event: Event) => void) | null): void {
|
|
|
|
if (listener) {
|
|
|
|
try {
|
|
|
|
window.removeEventListener('llm-stream-message', listener);
|
|
|
|
console.log(`Successfully removed event listener`);
|
|
|
|
} catch (err) {
|
|
|
|
console.error(`Error removing event listener:`, err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-04-16 21:43:19 +00:00
|
|
|
* Get a direct response from the server without streaming
|
2025-04-11 21:52:54 +00:00
|
|
|
*/
|
2025-06-02 00:56:19 +00:00
|
|
|
export async function getDirectResponse(noteId: string, messageParams: any): Promise<any> {
|
2025-04-16 21:43:19 +00:00
|
|
|
try {
|
2025-06-02 00:56:19 +00:00
|
|
|
const postResponse = await server.post<any>(`llm/chat/${noteId}/messages`, {
|
2025-04-16 21:43:19 +00:00
|
|
|
message: messageParams.content,
|
|
|
|
includeContext: messageParams.useAdvancedContext,
|
|
|
|
options: {
|
|
|
|
temperature: 0.7,
|
|
|
|
maxTokens: 2000
|
|
|
|
}
|
|
|
|
});
|
2025-04-11 21:52:54 +00:00
|
|
|
|
2025-04-16 21:43:19 +00:00
|
|
|
return postResponse;
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error getting direct response:', error);
|
|
|
|
throw error;
|
|
|
|
}
|
2025-04-11 21:52:54 +00:00
|
|
|
}
|
|
|
|
|