search WIP

This commit is contained in:
nicwands
2026-03-10 12:10:52 -04:00
parent 77b8ad2dcd
commit efc9c73751
8 changed files with 185 additions and 24 deletions

View File

@@ -98,8 +98,7 @@ class NotesAPI {
this.encryptionKey = encryptionKey || process.env.NOTES_ENCRYPTION_KEY; this.encryptionKey = encryptionKey || process.env.NOTES_ENCRYPTION_KEY;
this._sodiumReady = false; this._sodiumReady = false;
this.index = new Index({ this.index = new Index({
tokenize: "tolerant", tokenize: "forward"
resolution: 9
}); });
} }
async _initSodium() { async _initSodium() {
@@ -179,12 +178,23 @@ class NotesAPI {
try { try {
const note = this._decrypt(encryptedNote.data || encryptedNote); const note = this._decrypt(encryptedNote.data || encryptedNote);
this.notesCache.set(note.id, note); this.notesCache.set(note.id, note);
this.index.add(note.id, note.title + "\n" + note.content); const searchText = note.plainText || this._extractPlainText(note.content);
this.index.add(note.id, note.title + "\n" + searchText);
} catch (error) { } catch (error) {
console.error("Failed to decrypt note:", error); console.error("Failed to decrypt note:", error);
} }
} }
} }
_extractPlainText(content) {
if (!content) return "";
if (typeof content === "string") return content;
const extractText = (node) => {
if (typeof node === "string") return node;
if (!node || !node.content) return "";
return node.content.map(extractText).join(" ");
};
return extractText(content);
}
/* ----------------------- /* -----------------------
Public API Public API
------------------------*/ ------------------------*/
@@ -203,7 +213,7 @@ class NotesAPI {
getNote(id) { getNote(id) {
return this.notesCache.get(id) ?? null; return this.notesCache.get(id) ?? null;
} }
async createNote(metadata = {}, content = "") { async createNote(metadata = {}, content = "", plainText = "") {
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const now = (/* @__PURE__ */ new Date()).toISOString(); const now = (/* @__PURE__ */ new Date()).toISOString();
const note = { const note = {
@@ -212,14 +222,15 @@ class NotesAPI {
category: metadata.category || null, category: metadata.category || null,
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
content content,
plainText
}; };
const encryptedNote = { const encryptedNote = {
id: note.id, id: note.id,
data: this._encrypt(note) data: this._encrypt(note)
}; };
this.notesCache.set(id, note); this.notesCache.set(id, note);
this.index.add(id, note.title + "\n" + content); this.index.add(id, note.title + "\n" + plainText);
await this.adapter.create(encryptedNote); await this.adapter.create(encryptedNote);
return note; return note;
} }
@@ -228,16 +239,17 @@ class NotesAPI {
this.notesCache.delete(id); this.notesCache.delete(id);
this.index.remove(id); this.index.remove(id);
} }
async updateNote(id, content) { async updateNote(id, content, plainText = "") {
const note = this.notesCache.get(id); const note = this.notesCache.get(id);
if (!note) throw new Error("Note not found"); if (!note) throw new Error("Note not found");
note.content = content; note.content = content;
note.plainText = plainText;
note.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); note.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
const encryptedNote = { const encryptedNote = {
id: note.id, id: note.id,
data: this._encrypt(note) data: this._encrypt(note)
}; };
this.index.update(id, note.title + "\n" + content); this.index.update(id, note.title + "\n" + plainText);
await this.adapter.update(encryptedNote); await this.adapter.update(encryptedNote);
return note; return note;
} }
@@ -261,12 +273,18 @@ class NotesAPI {
id: note.id, id: note.id,
data: this._encrypt(note) data: this._encrypt(note)
}; };
this.index.update(id, note.title + "\n" + note.content); this.index.update(
id,
note.title + "\n" + (note.plainText || this._extractPlainText(note.content))
);
await this.adapter.update(encryptedNote); await this.adapter.update(encryptedNote);
return note; return note;
} }
search(query) { search(query) {
const ids = this.index.search(query); const ids = this.index.search(query, {
limit: 50,
suggest: true
});
return ids.map((id) => this.notesCache.get(id)); return ids.map((id) => this.notesCache.get(id));
} }
} }

View File

@@ -14,8 +14,7 @@ export default class NotesAPI {
this._sodiumReady = false this._sodiumReady = false
this.index = new Index({ this.index = new Index({
tokenize: 'tolerant', tokenize: 'forward',
resolution: 9,
}) })
} }
@@ -118,13 +117,28 @@ export default class NotesAPI {
const note = this._decrypt(encryptedNote.data || encryptedNote) const note = this._decrypt(encryptedNote.data || encryptedNote)
this.notesCache.set(note.id, note) this.notesCache.set(note.id, note)
this.index.add(note.id, note.title + '\n' + note.content) const searchText =
note.plainText || this._extractPlainText(note.content)
this.index.add(note.id, note.title + '\n' + searchText)
} catch (error) { } catch (error) {
console.error('Failed to decrypt note:', error) console.error('Failed to decrypt note:', error)
} }
} }
} }
_extractPlainText(content) {
if (!content) return ''
if (typeof content === 'string') return content
const extractText = (node) => {
if (typeof node === 'string') return node
if (!node || !node.content) return ''
return node.content.map(extractText).join(' ')
}
return extractText(content)
}
/* ----------------------- /* -----------------------
Public API Public API
------------------------*/ ------------------------*/
@@ -150,7 +164,7 @@ export default class NotesAPI {
return this.notesCache.get(id) ?? null return this.notesCache.get(id) ?? null
} }
async createNote(metadata = {}, content = '') { async createNote(metadata = {}, content = '', plainText = '') {
const id = crypto.randomUUID() const id = crypto.randomUUID()
const now = new Date().toISOString() const now = new Date().toISOString()
@@ -161,6 +175,7 @@ export default class NotesAPI {
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
content, content,
plainText,
} }
const encryptedNote = { const encryptedNote = {
@@ -169,7 +184,7 @@ export default class NotesAPI {
} }
this.notesCache.set(id, note) this.notesCache.set(id, note)
this.index.add(id, note.title + '\n' + content) this.index.add(id, note.title + '\n' + plainText)
await this.adapter.create(encryptedNote) await this.adapter.create(encryptedNote)
@@ -183,11 +198,12 @@ export default class NotesAPI {
this.index.remove(id) this.index.remove(id)
} }
async updateNote(id, content) { async updateNote(id, content, plainText = '') {
const note = this.notesCache.get(id) const note = this.notesCache.get(id)
if (!note) throw new Error('Note not found') if (!note) throw new Error('Note not found')
note.content = content note.content = content
note.plainText = plainText
note.updatedAt = new Date().toISOString() note.updatedAt = new Date().toISOString()
const encryptedNote = { const encryptedNote = {
@@ -195,7 +211,7 @@ export default class NotesAPI {
data: this._encrypt(note), data: this._encrypt(note),
} }
this.index.update(id, note.title + '\n' + content) this.index.update(id, note.title + '\n' + plainText)
await this.adapter.update(encryptedNote) await this.adapter.update(encryptedNote)
@@ -228,7 +244,12 @@ export default class NotesAPI {
data: this._encrypt(note), data: this._encrypt(note),
} }
this.index.update(id, note.title + '\n' + note.content) this.index.update(
id,
note.title +
'\n' +
(note.plainText || this._extractPlainText(note.content)),
)
await this.adapter.update(encryptedNote) await this.adapter.update(encryptedNote)
@@ -236,7 +257,10 @@ export default class NotesAPI {
} }
search(query) { search(query) {
const ids = this.index.search(query) const ids = this.index.search(query, {
limit: 50,
suggest: true,
})
return ids.map((id) => this.notesCache.get(id)) return ids.map((id) => this.notesCache.get(id))
} }
} }

View File

@@ -1,6 +1,8 @@
<template> <template>
<nav class="nav layout-block"> <nav class="nav layout-block">
<button @click="toggleMenu">Menu</button> <button @click="toggleMenu">Menu</button>
<router-link to="/search">Search</router-link>
</nav> </nav>
</template> </template>
@@ -29,6 +31,9 @@ onMounted(() => {
<style lang="scss"> <style lang="scss">
.nav { .nav {
display: flex;
align-items: center;
gap: 10px;
padding-top: 9px; padding-top: 9px;
color: var(--grey-100); color: var(--grey-100);
} }

View File

@@ -23,6 +23,7 @@ const formatDate = (date) => {
.note-row { .note-row {
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;
display: grid; display: grid;
width: 100%;
gap: 20px; gap: 20px;
cursor: pointer; cursor: pointer;

View File

@@ -33,8 +33,9 @@ const { loadNote, updateNoteContent, updateNoteMetadata } = useNotes()
const updateNote = _debounce(async ({ editor }) => { const updateNote = _debounce(async ({ editor }) => {
const json = editor.getJSON() const json = editor.getJSON()
const text = editor.getText()
await updateNoteContent(props.id, json) await updateNoteContent(props.id, json, text)
updateTitle(editor) updateTitle(editor)
}, 300) }, 300)

View File

@@ -44,8 +44,13 @@ export default () => {
/* ------------------------- /* -------------------------
Create Create
--------------------------*/ --------------------------*/
async function createNote(metadata, content) { async function createNote(metadata, content, plainText = '') {
const note = await window.notesAPI.call('createNote', metadata, content) const note = await window.notesAPI.call(
'createNote',
metadata,
content,
plainText,
)
await loadCategories() await loadCategories()
return note return note
} }
@@ -53,8 +58,13 @@ export default () => {
/* ------------------------- /* -------------------------
Update Update
--------------------------*/ --------------------------*/
async function updateNoteContent(id, content) { async function updateNoteContent(id, content, plainText = '') {
const note = await window.notesAPI.call('updateNote', id, content) const note = await window.notesAPI.call(
'updateNote',
id,
content,
plainText,
)
return note return note
} }

View File

@@ -5,6 +5,7 @@ import Note from '@/views/Note.vue'
import CreateCategory from '@/views/CreateCategory.vue' import CreateCategory from '@/views/CreateCategory.vue'
import Category from '@/views/Category.vue' import Category from '@/views/Category.vue'
import Instructions from '@/views/Instructions.vue' import Instructions from '@/views/Instructions.vue'
import Search from '@/views/Search.vue'
const routes = [ const routes = [
{ path: '/', name: 'directory', component: Directory }, { path: '/', name: 'directory', component: Directory },
@@ -12,6 +13,7 @@ const routes = [
{ path: '/category', name: 'create-category', component: CreateCategory }, { path: '/category', name: 'create-category', component: CreateCategory },
{ path: '/category/:id', name: 'category', component: Category }, { path: '/category/:id', name: 'category', component: Category },
{ path: '/instructions', name: 'instructions', component: Instructions }, { path: '/instructions', name: 'instructions', component: Instructions },
{ path: '/search', name: 'search', component: Search },
] ]
export const router = createRouter({ export const router = createRouter({

View File

@@ -0,0 +1,100 @@
<template>
<main class="search layout-block">
<router-link class="back" to="/"><- Back</router-link>
<form @submit.prevent="onSearch">
<div class="input-wrap">
<input
v-model="query"
type="text"
placeholder="Search"
ref="searchInput"
/>
</div>
</form>
<div class="results">
<note-row
v-for="note in searchResults"
:key="note.id"
:note="note"
/>
</div>
</main>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import useNotes from '@/composables/useNotes'
import NoteRow from '@/components/NoteRow.vue'
const query = ref('')
const searchInput = ref()
const { search, searchResults } = useNotes()
onMounted(async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
searchInput.value?.focus()
})
const onSearch = async () => {
await search(query.value)
}
</script>
<style lang="scss">
main.search {
.back {
display: block;
opacity: 0.25;
margin-top: 9px;
}
.input-wrap {
margin-top: 19px;
position: relative;
input {
display: block;
width: 100%;
position: relative;
padding: 5px 15px 6px;
background: var(--theme-bg);
--clip-start: 16px;
clip-path: polygon(
var(--clip-start) 1px,
calc(100% - var(--clip-start)) 1px,
calc(100% - 1.5px) 50%,
calc(100% - var(--clip-start)) calc(100% - 1px),
var(--clip-start) calc(100% - 1px),
1.5px 50%
);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--theme-fg);
--clip-start: 15px;
clip-path: polygon(
var(--clip-start) 0,
calc(100% - var(--clip-start)) 0,
100% 50%,
calc(100% - var(--clip-start)) 100%,
var(--clip-start) 100%,
0% 50%
);
}
}
.results {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 14px;
}
}
</style>