mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-12 20:02:28 +08:00
properly manage "saving" LLM chats
This commit is contained in:
parent
def28b1dcd
commit
5d3cfcd0fc
@ -93,6 +93,11 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
private validationWarning!: HTMLElement;
|
private validationWarning!: HTMLElement;
|
||||||
private sessionId: string | null = null;
|
private sessionId: string | null = null;
|
||||||
private currentNoteId: string | null = null;
|
private currentNoteId: string | null = null;
|
||||||
|
|
||||||
|
// Callbacks for data persistence
|
||||||
|
private onSaveData: ((data: any) => Promise<void>) | null = null;
|
||||||
|
private onGetData: (() => Promise<any>) | null = null;
|
||||||
|
private messages: Array<{role: string; content: string; timestamp?: Date}> = [];
|
||||||
|
|
||||||
doRender() {
|
doRender() {
|
||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
@ -126,6 +131,77 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
return this.$widget;
|
return this.$widget;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the callbacks for data persistence
|
||||||
|
*/
|
||||||
|
setDataCallbacks(
|
||||||
|
saveDataCallback: (data: any) => Promise<void>,
|
||||||
|
getDataCallback: () => Promise<any>
|
||||||
|
) {
|
||||||
|
this.onSaveData = saveDataCallback;
|
||||||
|
this.onGetData = getDataCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load saved chat data from the note
|
||||||
|
*/
|
||||||
|
async loadSavedData() {
|
||||||
|
if (!this.onGetData) {
|
||||||
|
console.log("No getData callback available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.onGetData();
|
||||||
|
console.log("Loaded chat data:", data);
|
||||||
|
|
||||||
|
if (data && data.messages && Array.isArray(data.messages)) {
|
||||||
|
// Clear existing messages in the UI
|
||||||
|
this.noteContextChatMessages.innerHTML = '';
|
||||||
|
this.messages = [];
|
||||||
|
|
||||||
|
// Add each message to the UI
|
||||||
|
data.messages.forEach((message: {role: string; content: string}) => {
|
||||||
|
if (message.role === 'user' || message.role === 'assistant') {
|
||||||
|
this.addMessageToChat(message.role, message.content);
|
||||||
|
// Track messages in our local array too
|
||||||
|
this.messages.push(message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll to bottom
|
||||||
|
this.chatContainer.scrollTop = this.chatContainer.scrollHeight;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error loading saved chat data:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current chat data to the note
|
||||||
|
*/
|
||||||
|
async saveCurrentData() {
|
||||||
|
if (!this.onSaveData) {
|
||||||
|
console.log("No saveData callback available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.onSaveData({
|
||||||
|
messages: this.messages,
|
||||||
|
lastUpdated: new Date()
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error saving chat data:", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
if (!this.isVisible()) {
|
if (!this.isVisible()) {
|
||||||
return;
|
return;
|
||||||
@ -136,8 +212,12 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
|
|
||||||
// Get current note context if needed
|
// Get current note context if needed
|
||||||
this.currentNoteId = appContext.tabManager.getActiveContext()?.note?.noteId || null;
|
this.currentNoteId = appContext.tabManager.getActiveContext()?.note?.noteId || null;
|
||||||
|
|
||||||
if (!this.sessionId) {
|
// Try to load saved data
|
||||||
|
const hasSavedData = await this.loadSavedData();
|
||||||
|
|
||||||
|
// Only create a new session if we don't have saved data
|
||||||
|
if (!this.sessionId || !hasSavedData) {
|
||||||
// Create a new chat session
|
// Create a new chat session
|
||||||
await this.createChatSession();
|
await this.createChatSession();
|
||||||
}
|
}
|
||||||
@ -171,6 +251,19 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
|
|
||||||
// Add user message to the chat
|
// Add user message to the chat
|
||||||
this.addMessageToChat('user', content);
|
this.addMessageToChat('user', content);
|
||||||
|
|
||||||
|
// Add to our local message array too
|
||||||
|
this.messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to note
|
||||||
|
this.saveCurrentData().catch(err => {
|
||||||
|
console.error("Failed to save user message to note:", err);
|
||||||
|
});
|
||||||
|
|
||||||
this.noteContextChatInput.value = '';
|
this.noteContextChatInput.value = '';
|
||||||
this.showLoadingIndicator();
|
this.showLoadingIndicator();
|
||||||
this.hideSources();
|
this.hideSources();
|
||||||
@ -196,6 +289,18 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
// If the POST request returned content directly, display it
|
// If the POST request returned content directly, display it
|
||||||
if (postResponse && postResponse.content) {
|
if (postResponse && postResponse.content) {
|
||||||
this.addMessageToChat('assistant', postResponse.content);
|
this.addMessageToChat('assistant', postResponse.content);
|
||||||
|
|
||||||
|
// Add to our local message array too
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: postResponse.content,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to note
|
||||||
|
this.saveCurrentData().catch(err => {
|
||||||
|
console.error("Failed to save assistant response to note:", err);
|
||||||
|
});
|
||||||
|
|
||||||
// If there are sources, show them
|
// If there are sources, show them
|
||||||
if (postResponse.sources && postResponse.sources.length > 0) {
|
if (postResponse.sources && postResponse.sources.length > 0) {
|
||||||
@ -220,7 +325,21 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
// If we haven't received any content after a reasonable timeout (10 seconds),
|
// If we haven't received any content after a reasonable timeout (10 seconds),
|
||||||
// add a fallback message and close the stream
|
// add a fallback message and close the stream
|
||||||
this.hideLoadingIndicator();
|
this.hideLoadingIndicator();
|
||||||
this.addMessageToChat('assistant', 'I\'m having trouble generating a response right now. Please try again later.');
|
const errorMessage = 'I\'m having trouble generating a response right now. Please try again later.';
|
||||||
|
this.addMessageToChat('assistant', errorMessage);
|
||||||
|
|
||||||
|
// Add to our local message array too
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: errorMessage,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to note
|
||||||
|
this.saveCurrentData().catch(err => {
|
||||||
|
console.error("Failed to save assistant error response to note:", err);
|
||||||
|
});
|
||||||
|
|
||||||
source.close();
|
source.close();
|
||||||
}
|
}
|
||||||
}, 10000);
|
}, 10000);
|
||||||
@ -240,7 +359,32 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
// If we didn't receive any content but the stream completed normally,
|
// If we didn't receive any content but the stream completed normally,
|
||||||
// display a message to the user
|
// display a message to the user
|
||||||
if (!receivedAnyContent) {
|
if (!receivedAnyContent) {
|
||||||
this.addMessageToChat('assistant', 'I processed your request, but I don\'t have any specific information to share at the moment.');
|
const defaultMessage = 'I processed your request, but I don\'t have any specific information to share at the moment.';
|
||||||
|
this.addMessageToChat('assistant', defaultMessage);
|
||||||
|
|
||||||
|
// Add to our local message array too
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: defaultMessage,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to note
|
||||||
|
this.saveCurrentData().catch(err => {
|
||||||
|
console.error("Failed to save assistant response to note:", err);
|
||||||
|
});
|
||||||
|
} else if (assistantResponse) {
|
||||||
|
// Save the completed streaming response to the message array
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: assistantResponse,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to note
|
||||||
|
this.saveCurrentData().catch(err => {
|
||||||
|
console.error("Failed to save assistant response to note:", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -293,7 +437,20 @@ export default class LlmChatPanel extends BasicWidget {
|
|||||||
|
|
||||||
// Only show error message if we haven't received any content yet
|
// Only show error message if we haven't received any content yet
|
||||||
if (!receivedAnyContent) {
|
if (!receivedAnyContent) {
|
||||||
this.addMessageToChat('assistant', 'Error connecting to the LLM service. Please try again.');
|
const connectionError = 'Error connecting to the LLM service. Please try again.';
|
||||||
|
this.addMessageToChat('assistant', connectionError);
|
||||||
|
|
||||||
|
// Add to our local message array too
|
||||||
|
this.messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: connectionError,
|
||||||
|
timestamp: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save to note
|
||||||
|
this.saveCurrentData().catch(err => {
|
||||||
|
console.error("Failed to save connection error to note:", err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,6 +13,12 @@ export default class AiChatTypeWidget extends TypeWidget {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.llmChatPanel = new LlmChatPanel();
|
this.llmChatPanel = new LlmChatPanel();
|
||||||
|
|
||||||
|
// Connect the data callbacks
|
||||||
|
this.llmChatPanel.setDataCallbacks(
|
||||||
|
(data) => this.saveData(data),
|
||||||
|
() => this.getData()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static getType() {
|
static getType() {
|
||||||
@ -38,9 +44,30 @@ export default class AiChatTypeWidget extends TypeWidget {
|
|||||||
if (!this.isInitialized) {
|
if (!this.isInitialized) {
|
||||||
console.log("Initializing AI Chat Panel for note:", note?.noteId);
|
console.log("Initializing AI Chat Panel for note:", note?.noteId);
|
||||||
|
|
||||||
|
// Initialize the note content first
|
||||||
|
if (note) {
|
||||||
|
try {
|
||||||
|
const content = await note.getContent();
|
||||||
|
// Check if content is empty
|
||||||
|
if (!content || content === '{}') {
|
||||||
|
// Initialize with empty chat history
|
||||||
|
await this.saveData({
|
||||||
|
messages: [],
|
||||||
|
title: note.title
|
||||||
|
});
|
||||||
|
console.log("Initialized empty chat history for new note");
|
||||||
|
} else {
|
||||||
|
console.log("Note already has content, will load in LlmChatPanel.refresh()");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error initializing AI Chat note content:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a promise to track initialization
|
// Create a promise to track initialization
|
||||||
this.initPromise = (async () => {
|
this.initPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
|
// This will load saved data via the getData callback
|
||||||
await this.llmChatPanel.refresh();
|
await this.llmChatPanel.refresh();
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -52,23 +79,6 @@ export default class AiChatTypeWidget extends TypeWidget {
|
|||||||
await this.initPromise;
|
await this.initPromise;
|
||||||
this.initPromise = null;
|
this.initPromise = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a newly created note, we can initialize the content
|
|
||||||
if (note) {
|
|
||||||
try {
|
|
||||||
const content = await note.getContent();
|
|
||||||
// Check if content is empty
|
|
||||||
if (!content || content === '{}') {
|
|
||||||
// Initialize with empty chat history
|
|
||||||
await this.saveData({
|
|
||||||
messages: [],
|
|
||||||
title: note.title
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error initializing AI Chat note content:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error in doRefresh:", e);
|
console.error("Error in doRefresh:", e);
|
||||||
toastService.showError("Error refreshing chat. Please try again.");
|
toastService.showError("Error refreshing chat. Please try again.");
|
||||||
@ -107,7 +117,7 @@ export default class AiChatTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await server.put(`notes/${this.note.noteId}/content`, {
|
await server.put(`notes/${this.note.noteId}/data`, {
|
||||||
content: JSON.stringify(data, null, 2)
|
content: JSON.stringify(data, null, 2)
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user