diff --git a/components/chat/Messages.tsx b/components/chat/Messages.tsx index 0b51664..f94d016 100644 --- a/components/chat/Messages.tsx +++ b/components/chat/Messages.tsx @@ -1,19 +1,15 @@ import { Message } from "@/lib/OpenRouter" -import { CreateNoteToolCard, ToolCall } from "./tools" +import { ToolCall } from "./tools" // MessageField is a single message bubble. Its style depends on whether // the message is from user or system (LLM response). export const MessageField = (data: Message | ToolCall, index: number) => { - if ('tool' in data && data.tool === 'createNote') { - return ( - - ) + // Skip rendering any tool calls + if ('tool' in data) { + return null; } - const isUser = (data as Message).role === "user" + + const isUser = data.role === "user" return (
{ - + // Filter out all tool calls + const displayMessages = messages.filter(msg => !('tool' in msg)); + // Show a placeholder message whenever there are no messages. - if (messages.length === 0) { + if (displayMessages.length === 0) { return (

Chat with your Notes

@@ -44,14 +42,12 @@ export const MessageView = (
) } - + return (
- { /* Render each message as a MessageField object. */ } - { messages.map(MessageField) } - + { displayMessages.map(MessageField) } { /* Show a grayed-out 'typing' placeholder when loading */ } {isTyping && (
@@ -64,4 +60,4 @@ export const MessageView = (
) -} +} \ No newline at end of file diff --git a/components/chat/tools.tsx b/components/chat/tools.tsx index 737a358..dcc00e8 100644 --- a/components/chat/tools.tsx +++ b/components/chat/tools.tsx @@ -1,38 +1,18 @@ import { ToolImplementation } from "@/lib/OpenRouter" import { ChatCompletionTool } from "openai/resources/index.mjs" -import { FileTextIcon } from "lucide-react" -import { Button } from "@/components/ui/button" import { SetStateAction, Dispatch } from "react" import { Message } from "@/lib/OpenRouter" import { DatabaseContextType } from "../DatabaseProvider" import UUID from "@/lib/UUID" -// ToolCall is a message containing the results of an LLM tool call. +// ToolCall type definition export type ToolCall = { - tool : 'createNote' + tool : 'createNote' | 'listNotes' | 'readNote' | 'searchNotes' id? : number content : string } -// CreateNoteToolCard is a message that appears in the chat denoting -// the creation of a new note. -const CreateNoteToolCard = ({ id, preview } : -{ id: number, preview: string }) => ( -
- -
- Created New Note -

- {preview} -

-
- -
-) - -// LLM tool-calling function definitions. +// Enhanced tool definitions with additional searchNotes function const toolDefinitions: ChatCompletionTool[] = [ { type: 'function', @@ -48,10 +28,50 @@ const toolDefinitions: ChatCompletionTool[] = [ required: ['title', 'content'] } } + }, + { + type: 'function', + function: { + name: 'listNotes', + description: 'List all notes in the database with their titles and IDs', + parameters: { + type: 'object', + properties: {}, + required: [] + } + } + }, + { + type: 'function', + function: { + name: 'readNote', + description: 'Read the content of a specific note by ID', + parameters: { + type: 'object', + properties: { + id: { type: 'number', description: 'The ID of the note to read' } + }, + required: ['id'] + } + } + }, + { + type: 'function', + function: { + name: 'searchNotes', + description: 'Search for notes containing a specific keyword or phrase', + parameters: { + type: 'object', + properties: { + query: { type: 'string', description: 'Search term or phrase to look for in notes' } + }, + required: ['query'] + } + } } ] -// LLM tool-calling i +// Enhanced tool implementations const toolImplementations = ( db : DatabaseContextType, setMessages: Dispatch> @@ -59,7 +79,6 @@ const toolImplementations = ( return { createNote: async (args) => { const id = UUID() - const content = args.content.slice(0,200) await db.notes.create({ id : id, title : args.title, @@ -67,23 +86,129 @@ const toolImplementations = ( atime : Math.floor(Date.now() / 1000), mtime : Math.floor(Date.now() / 1000), }) + // We'll still log the creation but won't display it await db.history.create({ id : id, role : 'tool', tool_name : 'createNote', - content : content, + content : args.content.slice(0,200), time : Math.floor(Date.now() / 1000) }) - setMessages((prev) => { - return [...prev, { - tool : 'createNote', - id : id, - content : content, - } as ToolCall] - }) - return { success: true } + return { success: true, id: id } + }, + + listNotes: async () => { + // Fetch all notes from the database + const notes = await db.notes.readAll(); + + // Format the notes list + const notesList = notes.map(note => ({ + id: note.id, + title: note.title + })); + + // Log the tool call but don't display it + const toolCallId = UUID(); + await db.history.create({ + id: toolCallId, + role: 'tool', + tool_name: 'listNotes', + content: JSON.stringify(notesList), + time: Math.floor(Date.now() / 1000) + }); + + return { notes: notesList, success: true }; + }, + + readNote: async (args) => { + // Fetch the specific note from the database + const note = await db.notes.read(args.id); + + if (!note) { + return { success: false, message: "Note not found" }; + } + + // Format the note data + const noteData = { + id: note.id, + title: note.title, + content: note.content + }; + + // Log the tool call but don't display it + const toolCallId = UUID(); + await db.history.create({ + id: toolCallId, + role: 'tool', + tool_name: 'readNote', + content: JSON.stringify(noteData), + time: Math.floor(Date.now() / 1000) + }); + + return { note: noteData, success: true }; + }, + + // New search function that combines listing and reading in one step + searchNotes: async (args) => { + // Get all notes + const notes = await db.notes.readAll(); + + // Filter notes that contain the search query in title or content + const matchedNotes = []; + for (const note of notes) { + // Check if query appears in title or content (case insensitive) + if ( + note.title.toLowerCase().includes(args.query.toLowerCase()) || + note.content.toLowerCase().includes(args.query.toLowerCase()) + ) { + matchedNotes.push({ + id: note.id, + title: note.title, + content: note.content, + // Add snippet with context around the matched term + snippet: extractContextSnippet(note.content, args.query) + }); + } + } + + // Log the search but don't display it + const toolCallId = UUID(); + await db.history.create({ + id: toolCallId, + role: 'tool', + tool_name: 'searchNotes', + content: JSON.stringify({ query: args.query, results: matchedNotes.length }), + time: Math.floor(Date.now() / 1000) + }); + + return { + results: matchedNotes, + count: matchedNotes.length, + success: true + }; } } as ToolImplementation } -export { toolDefinitions, toolImplementations, CreateNoteToolCard } +// Helper function to extract context around matched term +function extractContextSnippet(content: string, query: string): string { + const lowerContent = content.toLowerCase(); + const lowerQuery = query.toLowerCase(); + + const index = lowerContent.indexOf(lowerQuery); + if (index === -1) return ""; + + // Get context around the match (100 chars before and after) + const start = Math.max(0, index - 100); + const end = Math.min(content.length, index + query.length + 100); + + let snippet = content.substring(start, end); + + // Add ellipsis if we're not at the beginning/end of content + if (start > 0) snippet = "..." + snippet; + if (end < content.length) snippet = snippet + "..."; + + return snippet; +} + +export { toolDefinitions, toolImplementations } \ No newline at end of file diff --git a/components/editor/Decorations.ts b/components/editor/Decorations.ts index e171811..8de75f3 100644 --- a/components/editor/Decorations.ts +++ b/components/editor/Decorations.ts @@ -27,6 +27,10 @@ class LinkWidget extends WidgetType { if (this.dest.startsWith('node:')) { const nodeID = this.dest.substring(5) + // allow clicking while title is in focus + link.addEventListener('mousedown', (event) => { + event.preventDefault() + }) link.addEventListener('click', () => { const navigateEvent = new CustomEvent('navigate', { detail: { path: `/note?id=${nodeID}` }, diff --git a/components/editor/Editor.tsx b/components/editor/Editor.tsx index 67b0636..2dc0cd0 100644 --- a/components/editor/Editor.tsx +++ b/components/editor/Editor.tsx @@ -32,7 +32,7 @@ export default function Editor() { const [isSaving, setIsSaving] = useState(false) const [text, setText] = useState('') const [title, setTitle] = useState('') - const [noteID,] = useState( + const [noteID, setNoteID] = useState( idParam ? Number(idParam) : UUID() ) @@ -73,6 +73,23 @@ export default function Editor() { sel?.addRange(range) } + useEffect(() => { + if (idParam) { + setNoteID(Number(idParam)) + } + }, [idParam]) + + useEffect(() => { + const handleNavigate = (event: CustomEvent) => { + const { path } = event.detail + router.push(path) + } + document.addEventListener('navigate', handleNavigate as EventListener) + return () => { + document.removeEventListener('navigate', handleNavigate as EventListener) + } + }, [router]) + // Initialize editor. useEffect(() => { let isMounted = true @@ -143,15 +160,8 @@ export default function Editor() { } } editorDom.addEventListener('keydown', keyDownHandler, true) // ensure custom keydown handler runs first - // custom event to use nextjs react component only router - const handleNavigate = (event: CustomEvent) => { - const { path } = event.detail - router.push(path) - } - document.addEventListener('navigate', handleNavigate as EventListener) return () => { editorDom.removeEventListener('keydown', keyDownHandler, true) - document.removeEventListener('navigate', handleNavigate as EventListener) } } @@ -171,7 +181,7 @@ export default function Editor() { view.destroy() } } - }, []) + }, [noteID]) // Autosave. useEffect(() => {