Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 13 additions & 17 deletions components/chat/Messages.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<CreateNoteToolCard
key={index}
id={data.id!}
preview={data.content}
/>
)
// Skip rendering any tool calls
if ('tool' in data) {
return null;
}
const isUser = (data as Message).role === "user"

const isUser = data.role === "user"
return (
<div
key={index}
Expand All @@ -32,9 +28,11 @@ export const MessageView = (
isTyping: boolean,
ref: BottomRef
) => {

// 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 (
<div className="flex flex-col justify-center items-center text-center text-gray-700">
<h2 className="text-xl font-bold">Chat with your Notes</h2>
Expand All @@ -44,14 +42,12 @@ export const MessageView = (
</div>
)
}

return (
<div className="flex-1 overflow-auto flex flex-col gap-3"
style={{maxHeight: 'calc(100vh - 120px)'}}>

{ /* Render each message as a MessageField object. */ }
{ messages.map(MessageField) }

{ displayMessages.map(MessageField) }
{ /* Show a grayed-out 'typing' placeholder when loading */ }
{isTyping && (
<div className="self-start border border-[#979797] bg-[#F6F6F6] text-black rounded-md p-3 text-sm">
Expand All @@ -64,4 +60,4 @@ export const MessageView = (
<div ref={ref} />
</div>
)
}
}
195 changes: 160 additions & 35 deletions components/chat/tools.tsx
Original file line number Diff line number Diff line change
@@ -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 }) => (
<div className="break-words rounded-sm p-3 text-sm bg-[#F6F6F6] border border-[#979797] text-black grid grid-cols-[50px_auto_70px] gap-3 h-[70px] max-w-full">
<FileTextIcon className='h-full w-full'/>
<div className="flex flex-col min-w-0">
<b>Created New Note</b>
<p className='overflow-hidden text-ellipsis whitespace-nowrap'>
{preview}
</p>
</div>
<Button onClick={() => window.location.href=`/note?id=${id}`}>
View
</Button>
</div>
)

// LLM tool-calling function definitions.
// Enhanced tool definitions with additional searchNotes function
const toolDefinitions: ChatCompletionTool[] = [
{
type: 'function',
Expand All @@ -48,42 +28,187 @@ 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<SetStateAction<(ToolCall | Message)[]>>
) => {
return {
createNote: async (args) => {
const id = UUID()
const content = args.content.slice(0,200)
await db.notes.create({
id : id,
title : args.title,
content : args.content,
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 }
4 changes: 4 additions & 0 deletions components/editor/Decorations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}` },
Expand Down
28 changes: 19 additions & 9 deletions components/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default function Editor() {
const [isSaving, setIsSaving] = useState<boolean>(false)
const [text, setText] = useState<string>('')
const [title, setTitle] = useState<string>('')
const [noteID,] = useState<number>(
const [noteID, setNoteID] = useState<number>(
idParam ? Number(idParam) : UUID()
)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -171,7 +181,7 @@ export default function Editor() {
view.destroy()
}
}
}, [])
}, [noteID])

// Autosave.
useEffect(() => {
Expand Down
Loading