import { uniqBy } from "lodash-es"; import { LinkIcon, X } from "lucide-react"; import React, { useContext, useState } from "react"; import { toast } from "react-hot-toast"; import useDebounce from "react-use/lib/useDebounce"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { memoServiceClient } from "@/grpcweb"; import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; import useCurrentUser from "@/hooks/useCurrentUser"; import { extractMemoIdFromName } from "@/store/common"; import { Memo, MemoRelation_Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; import { EditorRefActions } from "../Editor"; import { MemoEditorContext } from "../types"; interface Props { editorRef: React.RefObject; } const AddMemoRelationPopover = (props: Props) => { const { editorRef } = props; const t = useTranslate(); const context = useContext(MemoEditorContext); const user = useCurrentUser(); const [searchText, setSearchText] = useState(""); const [isFetching, setIsFetching] = useState(true); const [fetchedMemos, setFetchedMemos] = useState([]); const [selectedMemos, setSelectedMemos] = useState([]); const [embedded, setEmbedded] = useState(false); const [popoverOpen, setPopoverOpen] = useState(false); const filteredMemos = fetchedMemos.filter( (memo) => !selectedMemos.includes(memo) && memo.name !== context.memoName && !context.relationList.some((relation) => relation.relatedMemo?.name === memo.name), ); useDebounce( async () => { if (!popoverOpen) return; setIsFetching(true); try { const conditions = []; if (searchText) { conditions.push(`content_search == [${JSON.stringify(searchText)}]`); } const { memos } = await memoServiceClient.listMemos({ parent: user.name, pageSize: DEFAULT_LIST_MEMOS_PAGE_SIZE, oldFilter: conditions.length > 0 ? conditions.join(" && ") : undefined, }); setFetchedMemos(memos); } catch (error: any) { toast.error(error.details); console.error(error); } setIsFetching(false); }, 300, [popoverOpen, searchText], ); const getHighlightedContent = (content: string) => { const index = content.toLowerCase().indexOf(searchText.toLowerCase()); if (index === -1) { return content; } let before = content.slice(0, index); if (before.length > 20) { before = "..." + before.slice(before.length - 20); } const highlighted = content.slice(index, index + searchText.length); let after = content.slice(index + searchText.length); if (after.length > 20) { after = after.slice(0, 20) + "..."; } return ( <> {before} {highlighted} {after} ); }; const addMemoRelations = async () => { // If embedded mode is enabled, embed the memo instead of creating a relation. if (embedded) { if (!editorRef.current) { toast.error(t("message.failed-to-embed-memo")); return; } const cursorPosition = editorRef.current.getCursorPosition(); const prevValue = editorRef.current.getContent().slice(0, cursorPosition); if (prevValue !== "" && !prevValue.endsWith("\n")) { editorRef.current.insertText("\n"); } for (const memo of selectedMemos) { editorRef.current.insertText(`![[memos/${extractMemoIdFromName(memo.name)}]]\n`); } setTimeout(() => { editorRef.current?.scrollToCursor(); editorRef.current?.focus(); }); } else { context.setRelationList( uniqBy( [ ...selectedMemos.map((memo) => ({ memo: MemoRelation_Memo.fromPartial({ name: memo.name }), relatedMemo: MemoRelation_Memo.fromPartial({ name: memo.name }), type: MemoRelation_Type.REFERENCE, })), ...context.relationList, ].filter((relation) => relation.relatedMemo !== context.memoName), "relatedMemo", ), ); } setSelectedMemos([]); setPopoverOpen(false); }; return (
{/* Selected memos display */} {selectedMemos.length > 0 && (
{selectedMemos.map((memo) => (

{memo.displayTime?.toLocaleString()}

{memo.content}
setSelectedMemos((memos) => memos.filter((m) => m.name !== memo.name))} />
))}
)} {/* Search and selection interface */}
setSearchText(e.target.value)} className="h-9 mb-2" />
{filteredMemos.length === 0 ? (
{isFetching ? "Loading..." : t("reference.no-memos-found")}
) : ( filteredMemos.map((memo) => (
{ setSelectedMemos((prev) => [...prev, memo]); }} >

{memo.displayTime?.toLocaleString()}

{searchText ? getHighlightedContent(memo.content) : memo.snippet}

)) )}
setEmbedded(checked === true)} />
); }; export default AddMemoRelationPopover;