Skip to content
This repository was archived by the owner on Jun 22, 2025. It is now read-only.

Commit e6c2f72

Browse files
committed
Merge branch 'develop' into moveupdown
2 parents a23b0c5 + ca4b8fa commit e6c2f72

File tree

19 files changed

+593
-428
lines changed

19 files changed

+593
-428
lines changed

apps/client/src/services/glob.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,18 @@ function setupGlobs() {
2626
window.onerror = function (msg, url, lineNo, columnNo, error) {
2727
const string = String(msg).toLowerCase();
2828

29+
let errorObjectString = "";
30+
try {
31+
errorObjectString = JSON.stringify(error);
32+
} catch (e: any) {
33+
errorObjectString = e.toString();
34+
}
2935
let message = "Uncaught error: ";
3036

3137
if (string.includes("script error")) {
3238
message += "No details available";
3339
} else {
34-
message += [`Message: ${msg}`, `URL: ${url}`, `Line: ${lineNo}`, `Column: ${columnNo}`, `Error object: ${JSON.stringify(error)}`, `Stack: ${error && error.stack}`].join(", ");
40+
message += [`Message: ${msg}`, `URL: ${url}`, `Line: ${lineNo}`, `Column: ${columnNo}`, `Error object: ${errorObjectString}`, `Stack: ${error && error.stack}`].join(", ");
3541
}
3642

3743
ws.logError(message);

apps/client/src/translations/en/translation.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,7 @@
12601260
},
12611261
"create_new_ai_chat": "Create new AI Chat",
12621262
"configuration_warnings": "There are some issues with your AI configuration. Please check your settings.",
1263+
"experimental_warning": "The LLM feature is currently experimental - you have been warned.",
12631264
"selected_provider": "Selected Provider",
12641265
"selected_provider_description": "Choose the AI provider for chat and completion features",
12651266
"select_model": "Select model...",

apps/client/src/widgets/llm_chat/llm_chat_panel.ts

Lines changed: 124 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,115 @@ export default class LlmChatPanel extends BasicWidget {
350350
}
351351
}
352352

353+
/**
354+
* Save current chat data to a specific note ID
355+
*/
356+
async saveCurrentDataToSpecificNote(targetNoteId: string | null) {
357+
if (!this.onSaveData || !targetNoteId) {
358+
console.warn('Cannot save chat data: no saveData callback or no targetNoteId available');
359+
return;
360+
}
361+
362+
try {
363+
// Extract current tool execution steps if any exist
364+
const toolSteps = extractInChatToolSteps(this.noteContextChatMessages);
365+
366+
// Get tool executions from both UI and any cached executions in metadata
367+
let toolExecutions: Array<{
368+
id: string;
369+
name: string;
370+
arguments: any;
371+
result: any;
372+
error?: string;
373+
timestamp: string;
374+
}> = [];
375+
376+
// First include any tool executions already in metadata (from streaming events)
377+
if (this.metadata?.toolExecutions && Array.isArray(this.metadata.toolExecutions)) {
378+
toolExecutions = [...this.metadata.toolExecutions];
379+
console.log(`Including ${toolExecutions.length} tool executions from metadata`);
380+
}
381+
382+
// Also extract any visible tool steps from the UI
383+
const extractedExecutions = toolSteps.map(step => {
384+
// Parse tool execution information
385+
if (step.type === 'tool-execution') {
386+
try {
387+
const content = JSON.parse(step.content);
388+
return {
389+
id: content.toolCallId || `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
390+
name: content.tool || 'unknown',
391+
arguments: content.args || {},
392+
result: content.result || {},
393+
error: content.error,
394+
timestamp: new Date().toISOString()
395+
};
396+
} catch (e) {
397+
// If we can't parse it, create a basic record
398+
return {
399+
id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
400+
name: 'unknown',
401+
arguments: {},
402+
result: step.content,
403+
timestamp: new Date().toISOString()
404+
};
405+
}
406+
} else if (step.type === 'result' && step.name) {
407+
// Handle result steps with a name
408+
return {
409+
id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
410+
name: step.name,
411+
arguments: {},
412+
result: step.content,
413+
timestamp: new Date().toISOString()
414+
};
415+
}
416+
return {
417+
id: `tool-${Date.now()}-${Math.random().toString(36).substring(2, 7)}`,
418+
name: 'unknown',
419+
arguments: {},
420+
result: 'Unrecognized tool step',
421+
timestamp: new Date().toISOString()
422+
};
423+
});
424+
425+
// Merge the tool executions, keeping only unique IDs
426+
const existingIds = new Set(toolExecutions.map((t: {id: string}) => t.id));
427+
for (const exec of extractedExecutions) {
428+
if (!existingIds.has(exec.id)) {
429+
toolExecutions.push(exec);
430+
existingIds.add(exec.id);
431+
}
432+
}
433+
434+
const dataToSave = {
435+
messages: this.messages,
436+
noteId: targetNoteId,
437+
chatNoteId: targetNoteId, // For backward compatibility
438+
toolSteps: toolSteps,
439+
// Add sources if we have them
440+
sources: this.sources || [],
441+
// Add metadata
442+
metadata: {
443+
model: this.metadata?.model || undefined,
444+
provider: this.metadata?.provider || undefined,
445+
temperature: this.metadata?.temperature || 0.7,
446+
lastUpdated: new Date().toISOString(),
447+
// Add tool executions
448+
toolExecutions: toolExecutions
449+
}
450+
};
451+
452+
console.log(`Saving chat data to specific note ${targetNoteId}, ${toolSteps.length} tool steps, ${this.sources?.length || 0} sources, ${toolExecutions.length} tool executions`);
453+
454+
// Save the data to the note attribute via the callback
455+
// This is the ONLY place we should save data, letting the container widget handle persistence
456+
await this.onSaveData(dataToSave);
457+
} catch (error) {
458+
console.error('Error saving chat data to specific note:', error);
459+
}
460+
}
461+
353462
/**
354463
* Load saved chat data from the note attribute
355464
*/
@@ -867,8 +976,8 @@ export default class LlmChatPanel extends BasicWidget {
867976
this.showSources(postResponse.sources);
868977
}
869978

870-
// Process the assistant response
871-
this.processAssistantResponse(postResponse.content, postResponse);
979+
// Process the assistant response with original chat note ID
980+
this.processAssistantResponse(postResponse.content, postResponse, this.noteId);
872981

873982
hideLoadingIndicator(this.loadingIndicator);
874983
return true;
@@ -884,7 +993,7 @@ export default class LlmChatPanel extends BasicWidget {
884993
/**
885994
* Process an assistant response - add to UI and save
886995
*/
887-
private async processAssistantResponse(content: string, fullResponse?: any) {
996+
private async processAssistantResponse(content: string, fullResponse?: any, originalChatNoteId?: string | null) {
888997
// Add the response to the chat UI
889998
this.addMessageToChat('assistant', content);
890999

@@ -910,8 +1019,8 @@ export default class LlmChatPanel extends BasicWidget {
9101019
];
9111020
}
9121021

913-
// Save to note
914-
this.saveCurrentData().catch(err => {
1022+
// Save to note - use original chat note ID if provided
1023+
this.saveCurrentDataToSpecificNote(originalChatNoteId || this.noteId).catch(err => {
9151024
console.error("Failed to save assistant response to note:", err);
9161025
});
9171026
}
@@ -936,12 +1045,15 @@ export default class LlmChatPanel extends BasicWidget {
9361045
timestamp: string;
9371046
}> = [];
9381047

1048+
// Store the original chat note ID to ensure we save to the correct note even if user switches
1049+
const originalChatNoteId = this.noteId;
1050+
9391051
return setupStreamingResponse(
9401052
this.noteId,
9411053
messageParams,
9421054
// Content update handler
9431055
(content: string, isDone: boolean = false) => {
944-
this.updateStreamingUI(content, isDone);
1056+
this.updateStreamingUI(content, isDone, originalChatNoteId);
9451057

9461058
// Update session data with additional metadata when streaming is complete
9471059
if (isDone) {
@@ -1067,13 +1179,13 @@ export default class LlmChatPanel extends BasicWidget {
10671179
/**
10681180
* Update the UI with streaming content
10691181
*/
1070-
private updateStreamingUI(assistantResponse: string, isDone: boolean = false) {
1182+
private updateStreamingUI(assistantResponse: string, isDone: boolean = false, originalChatNoteId?: string | null) {
10711183
// Track if we have a streaming message in progress
10721184
const hasStreamingMessage = !!this.noteContextChatMessages.querySelector('.assistant-message.streaming');
1073-
1185+
10741186
// Create a new message element or use the existing streaming one
10751187
let assistantMessageEl: HTMLElement;
1076-
1188+
10771189
if (hasStreamingMessage) {
10781190
// Use the existing streaming message
10791191
assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message.streaming')!;
@@ -1103,7 +1215,7 @@ export default class LlmChatPanel extends BasicWidget {
11031215
if (isDone) {
11041216
// Remove the streaming class to mark this message as complete
11051217
assistantMessageEl.classList.remove('streaming');
1106-
1218+
11071219
// Apply syntax highlighting
11081220
formatCodeBlocks($(assistantMessageEl as HTMLElement));
11091221

@@ -1118,8 +1230,8 @@ export default class LlmChatPanel extends BasicWidget {
11181230
timestamp: new Date()
11191231
});
11201232

1121-
// Save the updated message list
1122-
this.saveCurrentData();
1233+
// Save the updated message list to the original chat note
1234+
this.saveCurrentDataToSpecificNote(originalChatNoteId || this.noteId);
11231235
}
11241236

11251237
// Scroll to bottom

apps/client/src/widgets/llm_chat/validation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Validation functions for LLM Chat
33
*/
44
import options from "../../services/options.js";
5+
import { t } from "../../services/i18n.js";
56

67
/**
78
* Validate providers configuration
@@ -37,6 +38,9 @@ export async function validateProviders(validationWarning: HTMLElement): Promise
3738
// Check for configuration issues with providers in the precedence list
3839
const configIssues: string[] = [];
3940

41+
// Always add experimental warning as the first item
42+
configIssues.push(t("ai_llm.experimental_warning"));
43+
4044
// Check each provider in the precedence list for proper configuration
4145
for (const provider of precedenceList) {
4246
if (provider === 'openai') {

apps/client/src/widgets/type_widgets/ai_chat.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,17 +182,30 @@ export default class AiChatTypeWidget extends TypeWidget {
182182

183183
// Save chat data to the note
184184
async saveData(data: any) {
185-
if (!this.note) {
185+
// If we have a noteId in the data, that's the AI Chat note we should save to
186+
// This happens when the chat panel is saving its conversation
187+
const targetNoteId = data.noteId;
188+
189+
// If no noteId in data, use the current note (for new chats)
190+
const noteIdToUse = targetNoteId || this.note?.noteId;
191+
192+
if (!noteIdToUse) {
193+
console.warn("Cannot save AI Chat data: no note ID available");
186194
return;
187195
}
188196

189197
try {
190-
console.log(`AiChatTypeWidget: Saving data for note ${this.note.noteId}`);
198+
console.log(`AiChatTypeWidget: Saving data for note ${noteIdToUse} (current note: ${this.note?.noteId}, data.noteId: ${data.noteId})`);
199+
200+
// Safety check: if we have both IDs and they don't match, warn about it
201+
if (targetNoteId && this.note?.noteId && targetNoteId !== this.note.noteId) {
202+
console.warn(`Note ID mismatch: saving to ${targetNoteId} but current note is ${this.note.noteId}`);
203+
}
191204

192205
// Format the data properly - this is the canonical format of the data
193206
const formattedData = {
194207
messages: data.messages || [],
195-
noteId: this.note.noteId, // Always use the note's own ID
208+
noteId: noteIdToUse, // Always preserve the correct note ID
196209
toolSteps: data.toolSteps || [],
197210
sources: data.sources || [],
198211
metadata: {
@@ -201,8 +214,8 @@ export default class AiChatTypeWidget extends TypeWidget {
201214
}
202215
};
203216

204-
// Save the data to the note
205-
await server.put(`notes/${this.note.noteId}/data`, {
217+
// Save the data to the correct note
218+
await server.put(`notes/${noteIdToUse}/data`, {
206219
content: JSON.stringify(formattedData, null, 2)
207220
});
208221
} catch (e) {

apps/client/src/widgets/type_widgets/editable_text.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
166166
// is shorter than minimumNonErrorTimePeriod, the watchdog changes
167167
// its state to crashedPermanently, and it stops restarting the editor.
168168
// This prevents an infinite restart loop.
169-
crashNumberLimit: 3,
169+
crashNumberLimit: 10,
170170
// A minimum number of milliseconds between saving the editor data internally (defaults to 5000).
171171
// Note that for large documents, this might impact the editor performance.
172172
saveInterval: 5000
@@ -181,8 +181,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
181181
return;
182182
}
183183

184-
logInfo(`CKEditor crash logs: ${JSON.stringify(this.watchdog.crashes)}`);
185-
this.watchdog.crashes.forEach((crashInfo) => console.log(crashInfo));
184+
logError(`CKEditor crash logs: ${JSON.stringify(this.watchdog.crashes, null, 4)}`);
186185

187186
if (currentState === "crashedPermanently") {
188187
dialogService.info(`Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report.`);
@@ -191,7 +190,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
191190
}
192191
});
193192

194-
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
193+
this.watchdog.setCreator(async (_, editorConfig) => {
195194
logInfo("Creating new CKEditor");
196195

197196
const finalConfig = {
@@ -221,7 +220,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
221220
}
222221

223222
//@ts-ignore
224-
const editor = await editorClass.create(elementOrData, finalConfig);
223+
const editor = await editorClass.create(this.$editor[0], finalConfig);
225224

226225
const notificationsPlugin = editor.plugins.get("Notification");
227226
notificationsPlugin.on("show:warning", (evt, data) => {
@@ -337,6 +336,11 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
337336
}
338337

339338
getData() {
339+
if (!this.watchdog.editor) {
340+
// There is nothing to save, most likely a result of the editor crashing and reinitializing.
341+
return;
342+
}
343+
340344
const content = this.watchdog.editor?.getData() ?? "";
341345

342346
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty,
@@ -375,7 +379,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
375379
}
376380
}
377381

378-
insertDateTimeToTextCommand() {
382+
insertDateTimeToTextCommand() {
379383
const date = new Date();
380384
const customDateTimeFormat = options.get("customDateTimeFormat");
381385
const dateString = utils.formatDateTime(date, customDateTimeFormat);

apps/client/src/widgets/type_widgets/options/ai_settings/ai_settings_widget.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default class AiSettingsWidget extends OptionsWidget {
4848
if (optionName === 'aiEnabled') {
4949
try {
5050
const isEnabled = value === 'true';
51-
51+
5252
if (isEnabled) {
5353
toastService.showMessage(t("ai_llm.ai_enabled") || "AI features enabled");
5454
} else {
@@ -203,6 +203,11 @@ export default class AiSettingsWidget extends OptionsWidget {
203203
// Get selected provider
204204
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
205205

206+
// Start with experimental warning
207+
const allWarnings = [
208+
t("ai_llm.experimental_warning")
209+
];
210+
206211
// Check for selected provider configuration
207212
const providerWarnings: string[] = [];
208213
if (selectedProvider === 'openai') {
@@ -222,10 +227,8 @@ export default class AiSettingsWidget extends OptionsWidget {
222227
}
223228
}
224229

225-
// Combine all warnings
226-
const allWarnings = [
227-
...providerWarnings
228-
];
230+
// Add provider warnings to all warnings
231+
allWarnings.push(...providerWarnings);
229232

230233
// Show or hide warnings
231234
if (allWarnings.length > 0) {

apps/server/.serve-nodir.env

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
TRILIUM_ENV=dev
2+
TRILIUM_RESOURCE_DIR=./apps/server/dist
3+
TRILIUM_PUBLIC_SERVER=http://localhost:4200

0 commit comments

Comments
 (0)