mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 02:45:54 +01:00
feat(react/ribbon): port similar notes
This commit is contained in:
@@ -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} />
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
40
apps/client/src/widgets/ribbon/SimilarNotesTab.tsx
Normal file
40
apps/client/src/widgets/ribbon/SimilarNotesTab.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -183,3 +183,24 @@
|
||||
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 */
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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, "")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user