mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +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> | ||||
|     ) | ||||
| } | ||||
| @@ -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 */ | ||||
| @@ -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