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 {
|
interface NoteLinkOpts {
|
||||||
notePath: string | string[];
|
notePath: string | string[];
|
||||||
showNotePath?: boolean;
|
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 stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
|
||||||
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
|
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
link.createLink(stringifiedNotePath, { showNotePath: true })
|
link.createLink(stringifiedNotePath, { showNotePath })
|
||||||
.then(setJqueryEl);
|
.then(setJqueryEl);
|
||||||
}, [ stringifiedNotePath, showNotePath ])
|
}, [ stringifiedNotePath, showNotePath ]);
|
||||||
|
|
||||||
|
if (style) {
|
||||||
|
jqueryEl?.css(style);
|
||||||
|
}
|
||||||
|
|
||||||
return <RawHtml html={jqueryEl} />
|
return <RawHtml html={jqueryEl} />
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ScriptTab from "./ScriptTab";
|
|||||||
import EditedNotesTab from "./EditedNotesTab";
|
import EditedNotesTab from "./EditedNotesTab";
|
||||||
import NotePropertiesTab from "./NotePropertiesTab";
|
import NotePropertiesTab from "./NotePropertiesTab";
|
||||||
import NoteInfoTab from "./NoteInfoTab";
|
import NoteInfoTab from "./NoteInfoTab";
|
||||||
|
import SimilarNotesTab from "./SimilarNotesTab";
|
||||||
|
|
||||||
interface TitleContext {
|
interface TitleContext {
|
||||||
note: FNote | null | undefined;
|
note: FNote | null | undefined;
|
||||||
@@ -114,9 +115,11 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
|
|||||||
icon: "bx bxs-network-chart"
|
icon: "bx bxs-network-chart"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
// SimilarNotesWidget
|
|
||||||
title: t("similar_notes.title"),
|
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"),
|
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;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #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 dateUtils from "../services/date_utils.js";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
import type BNote from "./entities/bnote.js";
|
import type BNote from "./entities/bnote.js";
|
||||||
|
import { SimilarNote } from "@triliumnext/commons";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
|
|
||||||
@@ -36,12 +37,6 @@ interface DateLimits {
|
|||||||
maxDate: string;
|
maxDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SimilarNote {
|
|
||||||
score: number;
|
|
||||||
notePath: string[];
|
|
||||||
noteId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterUrlValue(value: string) {
|
function filterUrlValue(value: string) {
|
||||||
return value
|
return value
|
||||||
.replace(/https?:\/\//gi, "")
|
.replace(/https?:\/\//gi, "")
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import type { Request } from "express";
|
|||||||
|
|
||||||
import similarityService from "../../becca/similarity.js";
|
import similarityService from "../../becca/similarity.js";
|
||||||
import becca from "../../becca/becca.js";
|
import becca from "../../becca/becca.js";
|
||||||
|
import { SimilarNoteResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
async function getSimilarNotes(req: Request) {
|
async function getSimilarNotes(req: Request) {
|
||||||
const noteId = req.params.noteId;
|
const noteId = req.params.noteId;
|
||||||
|
|
||||||
const _note = becca.getNoteOrThrow(noteId);
|
const _note = becca.getNoteOrThrow(noteId);
|
||||||
|
|
||||||
return await similarityService.findSimilarNotes(noteId);
|
return (await similarityService.findSimilarNotes(noteId) satisfies SimilarNoteResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -185,3 +185,11 @@ export interface SubtreeSizeResponse {
|
|||||||
subTreeNoteCount: number;
|
subTreeNoteCount: number;
|
||||||
subTreeSize: number;
|
subTreeSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SimilarNote {
|
||||||
|
score: number;
|
||||||
|
notePath: string[];
|
||||||
|
noteId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SimilarNoteResponse = (SimilarNote[] | undefined);
|
||||||
|
|||||||
Reference in New Issue
Block a user