feat(llm): allow the sidebar chat access to the note content

This commit is contained in:
Elian Doran
2026-03-29 22:09:29 +03:00
parent 490406e12a
commit 41a122f722
4 changed files with 72 additions and 4 deletions

View File

@@ -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;

View File

@@ -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,

View File

@@ -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",

View File

@@ -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;
}
/**