diff --git a/apps/client/src/widgets/react/NoteLink.tsx b/apps/client/src/widgets/react/NoteLink.tsx index 31dd4fbeb..21c0af3d3 100644 --- a/apps/client/src/widgets/react/NoteLink.tsx +++ b/apps/client/src/widgets/react/NoteLink.tsx @@ -5,16 +5,21 @@ import RawHtml from "./RawHtml"; interface NoteLinkOpts { notePath: string | string[]; showNotePath?: boolean; + style?: Record; } -export default function NoteLink({ notePath, showNotePath }: NoteLinkOpts) { +export default function NoteLink({ notePath, showNotePath, style }: NoteLinkOpts) { const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath; const [ jqueryEl, setJqueryEl ] = useState>(); useEffect(() => { - link.createLink(stringifiedNotePath, { showNotePath: true }) + link.createLink(stringifiedNotePath, { showNotePath }) .then(setJqueryEl); - }, [ stringifiedNotePath, showNotePath ]) + }, [ stringifiedNotePath, showNotePath ]); + + if (style) { + jqueryEl?.css(style); + } return diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx index abe0e1fdd..14d448d45 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -14,6 +14,7 @@ import ScriptTab from "./ScriptTab"; import EditedNotesTab from "./EditedNotesTab"; import NotePropertiesTab from "./NotePropertiesTab"; import NoteInfoTab from "./NoteInfoTab"; +import SimilarNotesTab from "./SimilarNotesTab"; interface TitleContext { note: FNote | null | undefined; @@ -114,9 +115,11 @@ const TAB_CONFIGURATION = numberObjectsInPlace([ icon: "bx bxs-network-chart" }, { - // SimilarNotesWidget title: t("similar_notes.title"), - icon: "bx bx-bar-chart" + icon: "bx bx-bar-chart", + show: ({ note }) => note?.type !== "search" && !note?.isLabelTruthy("similarNotesWidgetDisabled"), + content: SimilarNotesTab, + toggleCommand: "toggleRibbonTabSimilarNotes" }, { title: t("note_info_widget.title"), diff --git a/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx b/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx new file mode 100644 index 000000000..b771f13bc --- /dev/null +++ b/apps/client/src/widgets/ribbon/SimilarNotesTab.tsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from "preact/hooks"; +import { TabContext } from "./ribbon-interface"; +import { SimilarNoteResponse } from "@triliumnext/commons"; +import server from "../../services/server"; +import { t } from "../../services/i18n"; +import froca from "../../services/froca"; +import NoteLink from "../react/NoteLink"; + +export default function SimilarNotesTab({ note }: TabContext) { + const [ similarNotes, setSimilarNotes ] = useState(); + + useEffect(() => { + if (note) { + server.get(`similar-notes/${note.noteId}`).then(async similarNotes => { + if (similarNotes) { + const noteIds = similarNotes.flatMap((note) => note.notePath); + await froca.getNotes(noteIds, true); // preload all at once + } + setSimilarNotes(similarNotes); + }); + } + + }, [ note?.noteId ]); + + return ( +
+ {similarNotes?.length ? ( +
+ {similarNotes.map(({notePath, score}) => ( + + ))} +
+ ) : ( + <>{t("similar_notes.no_similar_notes_found")} + )} +
+ ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index 678c24af7..7200e6a00 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -182,4 +182,25 @@ text-overflow: ellipsis; white-space: nowrap; } +/* #endregion */ + +/* #region Similar Notes */ +.similar-notes-wrapper { + max-height: 200px; + overflow: auto; + padding: 12px; +} + +.similar-notes-wrapper a { + display: inline-block; + border: 1px dotted var(--main-border-color); + border-radius: 20px; + background-color: var(--accented-background-color); + padding: 0 10px 0 10px; + margin: 0 3px 0 3px; + max-width: 10em; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} /* #endregion */ \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon_widgets/similar_notes.ts b/apps/client/src/widgets/ribbon_widgets/similar_notes.ts deleted file mode 100644 index f86fc714a..000000000 --- a/apps/client/src/widgets/ribbon_widgets/similar_notes.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { t } from "../../services/i18n.js"; -import linkService from "../../services/link.js"; -import server from "../../services/server.js"; -import froca from "../../services/froca.js"; -import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import type FNote from "../../entities/fnote.js"; -import type { EventData } from "../../components/app_context.js"; - -const TPL = /*html*/` -
- - -
-
-`; - -// TODO: Deduplicate with server -interface SimilarNote { - score: number; - notePath: string[]; - noteId: string; -} - -export default class SimilarNotesWidget extends NoteContextAwareWidget { - - private $similarNotesWrapper!: JQuery; - private title?: string; - private rendered?: boolean; - - get name() { - return "similarNotes"; - } - - get toggleCommand() { - return "toggleRibbonTabSimilarNotes"; - } - - isEnabled() { - return super.isEnabled() && this.note?.type !== "search" && !this.note?.isLabelTruthy("similarNotesWidgetDisabled"); - } - - getTitle() { - return { - show: this.isEnabled(), - - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - - this.$similarNotesWrapper = this.$widget.find(".similar-notes-wrapper"); - } - - async refreshWithNote(note: FNote) { - if (!this.note) { - return; - } - - // remember which title was when we found the similar notes - this.title = this.note.title; - - const similarNotes = await server.get(`similar-notes/${this.noteId}`); - - if (similarNotes.length === 0) { - this.$similarNotesWrapper.empty().append(t("similar_notes.no_similar_notes_found")); - - return; - } - - const noteIds = similarNotes.flatMap((note) => note.notePath); - - await froca.getNotes(noteIds, true); // preload all at once - - const $list = $("
"); - - for (const similarNote of similarNotes) { - const note = await froca.getNote(similarNote.noteId, true); - - if (!note) { - continue; - } - - const $item = (await linkService.createLink(similarNote.notePath.join("/"))).css("font-size", 24 * (1 - 1 / (1 + similarNote.score))); - - $list.append($item); - } - - this.$similarNotesWrapper.empty().append($list); - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - if (this.note && this.title !== this.note.title) { - this.rendered = false; - - this.refresh(); - } - } -} diff --git a/apps/server/src/becca/similarity.ts b/apps/server/src/becca/similarity.ts index fe7ecda21..dc0a4c3f8 100644 --- a/apps/server/src/becca/similarity.ts +++ b/apps/server/src/becca/similarity.ts @@ -4,6 +4,7 @@ import beccaService from "./becca_service.js"; import dateUtils from "../services/date_utils.js"; import { JSDOM } from "jsdom"; import type BNote from "./entities/bnote.js"; +import { SimilarNote } from "@triliumnext/commons"; const DEBUG = false; @@ -36,12 +37,6 @@ interface DateLimits { maxDate: string; } -export interface SimilarNote { - score: number; - notePath: string[]; - noteId: string; -} - function filterUrlValue(value: string) { return value .replace(/https?:\/\//gi, "") diff --git a/apps/server/src/routes/api/similar_notes.ts b/apps/server/src/routes/api/similar_notes.ts index 8cd82dc72..6b9cbb926 100644 --- a/apps/server/src/routes/api/similar_notes.ts +++ b/apps/server/src/routes/api/similar_notes.ts @@ -4,13 +4,14 @@ import type { Request } from "express"; import similarityService from "../../becca/similarity.js"; import becca from "../../becca/becca.js"; +import { SimilarNoteResponse } from "@triliumnext/commons"; async function getSimilarNotes(req: Request) { const noteId = req.params.noteId; const _note = becca.getNoteOrThrow(noteId); - return await similarityService.findSimilarNotes(noteId); + return (await similarityService.findSimilarNotes(noteId) satisfies SimilarNoteResponse); } export default { diff --git a/packages/commons/src/lib/server_api.ts b/packages/commons/src/lib/server_api.ts index 46b403d0f..84a471670 100644 --- a/packages/commons/src/lib/server_api.ts +++ b/packages/commons/src/lib/server_api.ts @@ -185,3 +185,11 @@ export interface SubtreeSizeResponse { subTreeNoteCount: number; subTreeSize: number; } + +export interface SimilarNote { + score: number; + notePath: string[]; + noteId: string; +} + +export type SimilarNoteResponse = (SimilarNote[] | undefined);