chore(llm): address requested changes

This commit is contained in:
Elian Doran
2026-03-30 22:20:44 +03:00
parent 841c58ca8c
commit a2b6bc0493
7 changed files with 41 additions and 47 deletions

View File

@@ -81,7 +81,6 @@
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@prefresh/vite": "2.4.12",
"@types/bootstrap": "5.2.10",
"@types/dompurify": "3.2.0",
"@types/jquery": "4.0.0",
"@types/leaflet": "1.9.21",
"@types/leaflet-gpx": "1.3.8",

View File

@@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import dateNoteService, { type RecentLlmChat } from "../../services/date_notes.js";
import { t } from "../../services/i18n.js";
import server from "../../services/server.js";
import { formatDateTime } from "../../utils/formatters";
import ActionButton from "../react/ActionButton.js";
import Dropdown from "../react/Dropdown.js";
import { FormListItem } from "../react/FormList.js";
@@ -80,6 +81,13 @@ export default function SidebarChat() {
console.error("Failed to save chat:", err);
}
}, 500);
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = undefined;
}
};
}, [shouldSave, chatNoteId, chat]);
// Load the most recent chat on mount (runs once)
@@ -254,7 +262,7 @@ export default function SidebarChat() {
? <strong>{chatItem.title}</strong>
: <span>{chatItem.title}</span>}
<span className="sidebar-chat-history-date">
{new Date(chatItem.dateModified).toLocaleDateString()}
{formatDateTime(new Date(chatItem.dateModified), "short", "short")}
</span>
</div>
</FormListItem>

View File

@@ -1,6 +1,6 @@
import "./LlmChat.css";
import { marked } from "marked";
import { Marked } from "marked";
import { useMemo } from "preact/hooks";
import { t } from "../../../services/i18n.js";
@@ -15,14 +15,14 @@ function shortenNumber(n: number): string {
}
// Configure marked for safe rendering
marked.setOptions({
const markedInstance = new Marked({
breaks: true, // Convert \n to <br>
gfm: true // GitHub Flavored Markdown
});
/** Parse markdown to HTML. Sanitization is handled by SanitizedHtml. */
function renderMarkdown(markdown: string): string {
return marked.parse(markdown) as string;
return markedInstance.parse(markdown) as string;
}
interface Props {

View File

@@ -1,10 +1,11 @@
import type { LlmCitation, LlmMessage, LlmModelInfo, LlmUsage } from "@triliumnext/commons";
import { RefObject } from "preact";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../../services/i18n.js";
import { getAvailableModels, streamChatCompletion } from "../../../services/llm_chat.js";
import { randomString } from "../../../services/utils.js";
import type { ContentBlock, LlmChatContent, StoredMessage, ToolCall } from "./llm_chat_types.js";
import type { ContentBlock, LlmChatContent, StoredMessage } from "./llm_chat_types.js";
export interface ModelOption extends LlmModelInfo {
costDescription?: string;
@@ -37,8 +38,8 @@ export interface UseLlmChatReturn {
enableExtendedThinking: boolean;
contextNoteId: string | undefined;
lastPromptTokens: number;
messagesEndRef: React.RefObject<HTMLDivElement>;
textareaRef: React.RefObject<HTMLTextAreaElement>;
messagesEndRef: RefObject<HTMLDivElement>;
textareaRef: RefObject<HTMLTextAreaElement>;
/** Whether a provider is configured and available */
hasProvider: boolean;
/** Whether we're still checking for providers */

View File

@@ -1,9 +1,11 @@
import type { Request, Response } from "express";
import type { LlmMessage } from "@triliumnext/commons";
import type { Request, Response } from "express";
import { generateChatTitle } from "../../services/llm/chat_title.js";
import { getProviderByType, hasConfiguredProviders, type LlmProviderConfig } from "../../services/llm/index.js";
import { streamToChunks } from "../../services/llm/stream.js";
import { generateChatTitle } from "../../services/llm/chat_title.js";
import log from "../../services/log.js";
import { safeExtractMessageAndStackFromError } from "../../services/utils.js";
interface ChatRequest {
messages: LlmMessage[];
@@ -34,7 +36,6 @@ async function streamChat(req: Request, res: Response) {
res.setHeader("Cache-Control", "no-cache, no-transform");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no"); // Disable nginx buffering
res.setHeader("Content-Encoding", "none"); // Disable compression
res.flushHeaders();
// Mark response as handled to prevent double-handling by apiResultHandler
@@ -46,7 +47,6 @@ async function streamChat(req: Request, res: Response) {
try {
if (!hasConfiguredProviders()) {
res.write(`data: ${JSON.stringify({ type: "error", error: "No LLM providers configured. Please add a provider in Options → AI / LLM." })}\n\n`);
res.end();
return;
}
@@ -54,7 +54,12 @@ async function streamChat(req: Request, res: Response) {
const result = provider.chat(messages, config);
// Get pricing and display name for the model
const modelId = config.model || "claude-sonnet-4-6";
const modelId = config.model || provider.getAvailableModels().find(m => m.isDefault)?.id;
if (!modelId) {
res.write(`data: ${JSON.stringify({ type: "error", error: "No model specified and no default model available for the provider." })}\n\n`);
return;
}
const pricing = provider.getModelPricing(modelId);
const modelDisplayName = provider.getAvailableModels().find(m => m.id === modelId)?.name || modelId;
for await (const chunk of streamToChunks(result, { model: modelDisplayName, pricing })) {
@@ -71,7 +76,7 @@ async function streamChat(req: Request, res: Response) {
await generateChatTitle(config.chatNoteId, userMessages[0].content);
} catch (err) {
// Title generation is best-effort; don't fail the chat
console.error("Failed to generate chat title:", err);
log.error(`Failed to generate chat title: ${safeExtractMessageAndStackFromError(err)}`);
}
}
} catch (error) {

View File

@@ -56,7 +56,7 @@ export const searchNotes = tool({
if (!note) return null;
return {
noteId: note.noteId,
title: note.title,
title: note.getTitleOrProtected(),
type: note.type
};
}).filter(Boolean);
@@ -76,13 +76,13 @@ export const readNote = tool({
if (!note) {
return { error: "Note not found" };
}
if (note.isProtected) {
if (!note.isContentAvailable()) {
return { error: "Note is protected" };
}
return {
noteId: note.noteId,
title: note.title,
title: note.getTitleOrProtected(),
type: note.type,
content: getNoteContentForLlm(note)
};
@@ -103,7 +103,7 @@ export const updateNoteContent = tool({
if (!note) {
return { error: "Note not found" };
}
if (note.isProtected) {
if (!note.isContentAvailable()) {
return { error: "Note is protected and cannot be modified" };
}
if (!note.hasStringContent()) {
@@ -115,7 +115,7 @@ export const updateNoteContent = tool({
return {
success: true,
noteId: note.noteId,
title: note.title
title: note.getTitleOrProtected()
};
}
});
@@ -134,7 +134,7 @@ export const appendToNote = tool({
if (!note) {
return { error: "Note not found" };
}
if (note.isProtected) {
if (!note.isContentAvailable()) {
return { error: "Note is protected and cannot be modified" };
}
if (!note.hasStringContent()) {
@@ -148,7 +148,7 @@ export const appendToNote = tool({
let newContent: string;
if (note.type === "text") {
const htmlToAppend = markdownImport.renderToHtml(content, note.title);
const htmlToAppend = markdownImport.renderToHtml(content, note.getTitleOrProtected());
newContent = existingContent + htmlToAppend;
} else {
newContent = existingContent + (existingContent.endsWith("\n") ? "" : "\n") + content;
@@ -159,7 +159,7 @@ export const appendToNote = tool({
return {
success: true,
noteId: note.noteId,
title: note.title
title: note.getTitleOrProtected()
};
}
});
@@ -180,7 +180,7 @@ export const createNote = tool({
if (!parentNote) {
return { error: "Parent note not found" };
}
if (parentNote.isProtected) {
if (!parentNote.isContentAvailable()) {
return { error: "Cannot create note under a protected parent" };
}
@@ -199,7 +199,7 @@ export const createNote = tool({
return {
success: true,
noteId: note.noteId,
title: note.title,
title: note.getTitleOrProtected(),
type: note.type
};
} catch (err) {
@@ -222,13 +222,13 @@ export function currentNoteTools(contextNoteId: string) {
if (!note) {
return { error: "Note not found" };
}
if (note.isProtected) {
if (!note.isContentAvailable()) {
return { error: "Note is protected" };
}
return {
noteId: note.noteId,
title: note.title,
title: note.getTitleOrProtected(),
type: note.type,
content: getNoteContentForLlm(note)
};

23
pnpm-lock.yaml generated
View File

@@ -376,9 +376,6 @@ importers:
'@types/bootstrap':
specifier: 5.2.10
version: 5.2.10
'@types/dompurify':
specifier: 3.2.0
version: 3.2.0
'@types/jquery':
specifier: 4.0.0
version: 4.0.0
@@ -6067,10 +6064,6 @@ packages:
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/dompurify@3.2.0':
resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==}
deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.
'@types/ejs@3.1.5':
resolution: {integrity: sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==}
@@ -17004,8 +16997,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.6.1
'@ckeditor/ckeditor5-widget': 47.6.1
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-cloud-services@47.6.1':
dependencies:
@@ -17390,8 +17381,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.6.1
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-highlight@47.6.1':
dependencies:
@@ -17401,8 +17390,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.6.1
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-horizontal-line@47.6.1':
dependencies:
@@ -17412,8 +17399,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.6.1
'@ckeditor/ckeditor5-widget': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-html-embed@47.6.1':
dependencies:
@@ -17423,8 +17408,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.6.1
'@ckeditor/ckeditor5-widget': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-html-support@47.6.1':
dependencies:
@@ -17604,6 +17587,8 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.6.1
'@ckeditor/ckeditor5-utils': 47.6.1
ckeditor5: 47.6.1
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-operations-compressor@47.6.1':
dependencies:
@@ -22170,10 +22155,6 @@ snapshots:
'@types/deep-eql@4.0.2': {}
'@types/dompurify@3.2.0':
dependencies:
dompurify: 3.3.3
'@types/ejs@3.1.5': {}
'@types/electron-squirrel-startup@1.0.2': {}