mirror of
https://github.com/zadam/trilium.git
synced 2026-07-06 00:08:10 +02:00
feat(llm): allow the sidebar chat access to the note content
This commit is contained in:
@@ -7,6 +7,7 @@ import server from "../../services/server.js";
|
||||
import ActionButton from "../react/ActionButton.js";
|
||||
import Dropdown from "../react/Dropdown.js";
|
||||
import { FormListItem } from "../react/FormList.js";
|
||||
import { useActiveNoteContext } from "../react/hooks.js";
|
||||
import NoItems from "../react/NoItems.js";
|
||||
import ChatInputBar from "../type_widgets/llm_chat/ChatInputBar.js";
|
||||
import ChatMessage from "../type_widgets/llm_chat/ChatMessage.js";
|
||||
@@ -27,6 +28,9 @@ export default function SidebarChat() {
|
||||
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
const historyDropdownRef = useRef<BootstrapDropdown | null>(null);
|
||||
|
||||
// Get the current active note context
|
||||
const { noteId: activeNoteId } = useActiveNoteContext();
|
||||
|
||||
// Use shared chat hook with sidebar-specific options
|
||||
const chat = useLlmChat(
|
||||
// onMessagesChange - trigger save
|
||||
@@ -34,6 +38,11 @@ export default function SidebarChat() {
|
||||
{ defaultEnableNoteTools: true, supportsExtendedThinking: true }
|
||||
);
|
||||
|
||||
// Update chat context when active note changes
|
||||
useEffect(() => {
|
||||
chat.setContextNoteId(activeNoteId ?? undefined);
|
||||
}, [activeNoteId, chat.setContextNoteId]);
|
||||
|
||||
// Ref to access chat methods in effects without triggering re-runs
|
||||
const chatRef = useRef(chat);
|
||||
chatRef.current = chat;
|
||||
|
||||
@@ -15,6 +15,8 @@ export interface LlmChatOptions {
|
||||
defaultEnableNoteTools?: boolean;
|
||||
/** Whether extended thinking is supported */
|
||||
supportsExtendedThinking?: boolean;
|
||||
/** Initial context note ID (the note the user is viewing) */
|
||||
contextNoteId?: string;
|
||||
}
|
||||
|
||||
export interface UseLlmChatReturn {
|
||||
@@ -31,6 +33,7 @@ export interface UseLlmChatReturn {
|
||||
enableWebSearch: boolean;
|
||||
enableNoteTools: boolean;
|
||||
enableExtendedThinking: boolean;
|
||||
contextNoteId: string | undefined;
|
||||
lastPromptTokens: number;
|
||||
messagesEndRef: React.RefObject<HTMLDivElement>;
|
||||
textareaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
@@ -42,6 +45,7 @@ export interface UseLlmChatReturn {
|
||||
setEnableWebSearch: (value: boolean) => void;
|
||||
setEnableNoteTools: (value: boolean) => void;
|
||||
setEnableExtendedThinking: (value: boolean) => void;
|
||||
setContextNoteId: (noteId: string | undefined) => void;
|
||||
|
||||
// Actions
|
||||
handleSubmit: (e: Event) => Promise<void>;
|
||||
@@ -55,7 +59,7 @@ export function useLlmChat(
|
||||
onMessagesChange?: (messages: StoredMessage[]) => void,
|
||||
options: LlmChatOptions = {}
|
||||
): UseLlmChatReturn {
|
||||
const { defaultEnableNoteTools = false, supportsExtendedThinking = false } = options;
|
||||
const { defaultEnableNoteTools = false, supportsExtendedThinking = false, contextNoteId: initialContextNoteId } = options;
|
||||
|
||||
const [messages, setMessagesInternal] = useState<StoredMessage[]>([]);
|
||||
const [input, setInput] = useState("");
|
||||
@@ -69,6 +73,7 @@ export function useLlmChat(
|
||||
const [enableWebSearch, setEnableWebSearch] = useState(true);
|
||||
const [enableNoteTools, setEnableNoteTools] = useState(defaultEnableNoteTools);
|
||||
const [enableExtendedThinking, setEnableExtendedThinking] = useState(false);
|
||||
const [contextNoteId, setContextNoteId] = useState<string | undefined>(initialContextNoteId);
|
||||
const [lastPromptTokens, setLastPromptTokens] = useState<number>(0);
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -84,6 +89,8 @@ export function useLlmChat(
|
||||
enableNoteToolsRef.current = enableNoteTools;
|
||||
const enableExtendedThinkingRef = useRef(enableExtendedThinking);
|
||||
enableExtendedThinkingRef.current = enableExtendedThinking;
|
||||
const contextNoteIdRef = useRef(contextNoteId);
|
||||
contextNoteIdRef.current = contextNoteId;
|
||||
|
||||
// Wrapper to call onMessagesChange when messages update
|
||||
const setMessages = useCallback((newMessages: StoredMessage[]) => {
|
||||
@@ -195,7 +202,8 @@ export function useLlmChat(
|
||||
const streamOptions: Parameters<typeof streamChatCompletion>[1] = {
|
||||
model: selectedModel || undefined,
|
||||
enableWebSearch,
|
||||
enableNoteTools
|
||||
enableNoteTools,
|
||||
contextNoteId
|
||||
};
|
||||
if (supportsExtendedThinking) {
|
||||
streamOptions.enableExtendedThinking = enableExtendedThinking;
|
||||
@@ -294,7 +302,7 @@ export function useLlmChat(
|
||||
}
|
||||
}
|
||||
);
|
||||
}, [input, isStreaming, messages, selectedModel, enableWebSearch, enableNoteTools, enableExtendedThinking, supportsExtendedThinking, setMessages]);
|
||||
}, [input, isStreaming, messages, selectedModel, enableWebSearch, enableNoteTools, enableExtendedThinking, contextNoteId, supportsExtendedThinking, setMessages]);
|
||||
|
||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
@@ -317,6 +325,7 @@ export function useLlmChat(
|
||||
enableWebSearch,
|
||||
enableNoteTools,
|
||||
enableExtendedThinking,
|
||||
contextNoteId,
|
||||
lastPromptTokens,
|
||||
messagesEndRef,
|
||||
textareaRef,
|
||||
@@ -328,6 +337,7 @@ export function useLlmChat(
|
||||
setEnableWebSearch,
|
||||
setEnableNoteTools,
|
||||
setEnableExtendedThinking,
|
||||
setContextNoteId,
|
||||
|
||||
// Actions
|
||||
handleSubmit,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { anthropic } from "@ai-sdk/anthropic";
|
||||
import { streamText, stepCountIs, type CoreMessage } from "ai";
|
||||
import type { LlmMessage } from "@triliumnext/commons";
|
||||
|
||||
import becca from "../../../becca/becca.js";
|
||||
import { noteTools } from "../tools.js";
|
||||
import type { LlmProvider, LlmProviderConfig, ModelInfo, ModelPricing, StreamResult } from "../types.js";
|
||||
|
||||
@@ -94,6 +95,42 @@ const MODEL_PRICING: Record<string, ModelPricing> = Object.fromEntries(
|
||||
AVAILABLE_MODELS.map(m => [m.id, m.pricing])
|
||||
);
|
||||
|
||||
/**
|
||||
* Build context string from the current note being viewed.
|
||||
*/
|
||||
function buildNoteContext(noteId: string): string | null {
|
||||
const note = becca.getNote(noteId);
|
||||
if (!note) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
parts.push(`The user is currently viewing a note titled "${note.title}" (ID: ${noteId}).`);
|
||||
|
||||
// Add note type context
|
||||
if (note.type !== "text") {
|
||||
parts.push(`Note type: ${note.type}`);
|
||||
}
|
||||
|
||||
// Add content for text notes (truncate if too long)
|
||||
if (note.type === "text" || note.type === "code") {
|
||||
try {
|
||||
const content = note.getContent();
|
||||
if (typeof content === "string" && content.trim()) {
|
||||
const maxLength = 4000;
|
||||
const truncated = content.length > maxLength
|
||||
? content.substring(0, maxLength) + "\n... (content truncated)"
|
||||
: content;
|
||||
parts.push(`\nNote content:\n\`\`\`\n${truncated}\n\`\`\``);
|
||||
}
|
||||
} catch {
|
||||
// Content not available
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join("\n");
|
||||
}
|
||||
|
||||
export class AnthropicProvider implements LlmProvider {
|
||||
name = "anthropic";
|
||||
|
||||
@@ -106,9 +143,19 @@ export class AnthropicProvider implements LlmProvider {
|
||||
}
|
||||
|
||||
chat(messages: LlmMessage[], config: LlmProviderConfig): StreamResult {
|
||||
const systemPrompt = config.systemPrompt || messages.find(m => m.role === "system")?.content;
|
||||
let systemPrompt = config.systemPrompt || messages.find(m => m.role === "system")?.content;
|
||||
const chatMessages = messages.filter(m => m.role !== "system");
|
||||
|
||||
// Add note context if viewing a note
|
||||
if (config.contextNoteId) {
|
||||
const noteContext = buildNoteContext(config.contextNoteId);
|
||||
if (noteContext) {
|
||||
systemPrompt = systemPrompt
|
||||
? `${systemPrompt}\n\n${noteContext}`
|
||||
: noteContext;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to AI SDK message format
|
||||
const coreMessages: CoreMessage[] = chatMessages.map(m => ({
|
||||
role: m.role as "user" | "assistant",
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface LlmChatConfig {
|
||||
enableExtendedThinking?: boolean;
|
||||
/** Token budget for extended thinking (default: 10000) */
|
||||
thinkingBudget?: number;
|
||||
/** Current note context (note ID the user is viewing) */
|
||||
contextNoteId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user