feat(react/ribbon): port similar notes

This commit is contained in:
Elian Doran
2025-08-22 19:27:58 +03:00
parent c5bb310613
commit cc05572a35
8 changed files with 85 additions and 131 deletions

View File

@@ -5,16 +5,21 @@ import RawHtml from "./RawHtml";
interface NoteLinkOpts {
notePath: string | string[];
showNotePath?: boolean;
style?: Record<string, string | number>;
}
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<JQuery<HTMLElement>>();
useEffect(() => {
link.createLink(stringifiedNotePath, { showNotePath: true })
link.createLink(stringifiedNotePath, { showNotePath })
.then(setJqueryEl);
}, [ stringifiedNotePath, showNotePath ])
}, [ stringifiedNotePath, showNotePath ]);
if (style) {
jqueryEl?.css(style);
}
return <RawHtml html={jqueryEl} />

View File

@@ -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<TabConfiguration>([
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"),

View File

@@ -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<SimilarNoteResponse>();
useEffect(() => {
if (note) {
server.get<SimilarNoteResponse>(`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 (
<div className="similar-notes-wrapper">
{similarNotes?.length ? (
<div>
{similarNotes.map(({notePath, score}) => (
<NoteLink notePath={notePath} style={{
"font-size": 24 * (1 - 1 / (1 + score))
}}/>
))}
</div>
) : (
<>{t("similar_notes.no_similar_notes_found")}</>
)}
</div>
)
}

View File

@@ -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 */

View File

@@ -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*/`
<div class="similar-notes-widget">
<style>
.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;
}
</style>
<div class="similar-notes-wrapper"></div>
</div>
`;
// TODO: Deduplicate with server
interface SimilarNote {
score: number;
notePath: string[];
noteId: string;
}
export default class SimilarNotesWidget extends NoteContextAwareWidget {
private $similarNotesWrapper!: JQuery<HTMLElement>;
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<SimilarNote[]>(`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 = $("<div>");
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();
}
}
}

View File

@@ -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, "")

View File

@@ -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 {

View File

@@ -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);