diff --git a/apps/client/src/widgets/sidebar/SidebarChat.tsx b/apps/client/src/widgets/sidebar/SidebarChat.tsx index 0f52f97fbe..de96367ca0 100644 --- a/apps/client/src/widgets/sidebar/SidebarChat.tsx +++ b/apps/client/src/widgets/sidebar/SidebarChat.tsx @@ -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>(); const historyDropdownRef = useRef(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; diff --git a/apps/client/src/widgets/type_widgets/llm_chat/useLlmChat.ts b/apps/client/src/widgets/type_widgets/llm_chat/useLlmChat.ts index ed12dd81ab..381323004a 100644 --- a/apps/client/src/widgets/type_widgets/llm_chat/useLlmChat.ts +++ b/apps/client/src/widgets/type_widgets/llm_chat/useLlmChat.ts @@ -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; textareaRef: React.RefObject; @@ -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; @@ -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([]); 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(initialContextNoteId); const [lastPromptTokens, setLastPromptTokens] = useState(0); const messagesEndRef = useRef(null); const textareaRef = useRef(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[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, diff --git a/apps/server/src/services/llm/providers/anthropic.ts b/apps/server/src/services/llm/providers/anthropic.ts index 00a8caf2f4..a3de4affba 100644 --- a/apps/server/src/services/llm/providers/anthropic.ts +++ b/apps/server/src/services/llm/providers/anthropic.ts @@ -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 = 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", diff --git a/packages/commons/src/lib/llm_api.ts b/packages/commons/src/lib/llm_api.ts index 9c05f9f0f3..56e2d3b884 100644 --- a/packages/commons/src/lib/llm_api.ts +++ b/packages/commons/src/lib/llm_api.ts @@ -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; } /**