From 2d5d5126b01238f822a5b3a37e10e6bb0ce30012 Mon Sep 17 00:00:00 2001 From: Kwanghyun On Date: Thu, 31 Oct 2024 22:08:39 +0900 Subject: [PATCH] Implement template plugin --- src/ChatView.tsx | 26 +- src/components/chat-view/Chat.tsx | 6 - .../chat-view/CreateTemplateDialog.tsx | 114 ++++----- .../chat-input/LexicalContentEditable.tsx | 6 +- .../CreateTemplatePopoverPlugin.tsx} | 68 ++++-- .../plugins/template/TemplatePlugin.tsx | 179 ++++++++++++++ styles.css | 222 ++++++++++++++++++ 7 files changed, 511 insertions(+), 110 deletions(-) rename src/components/chat-view/chat-input/plugins/{template-popover/TemplatePopoverPlugin.tsx => template/CreateTemplatePopoverPlugin.tsx} (65%) create mode 100644 src/components/chat-view/chat-input/plugins/template/TemplatePlugin.tsx diff --git a/src/ChatView.tsx b/src/ChatView.tsx index 2ebf439..9c5a047 100644 --- a/src/ChatView.tsx +++ b/src/ChatView.tsx @@ -7,6 +7,7 @@ import Chat, { ChatProps, ChatRef } from './components/chat-view/Chat' import { CHAT_VIEW_TYPE } from './constants' import { AppProvider } from './contexts/app-context' import { DarkModeProvider } from './contexts/dark-mode-context' +import { DatabaseProvider } from './contexts/database-context' import { DialogContainerProvider } from './contexts/dialog-container-context' import { LLMProvider } from './contexts/llm-context' import { RAGProvider } from './contexts/rag-context' @@ -68,6 +69,7 @@ export class ChatView extends ItemView { }, }, }) + const dbManager = await this.plugin.getDbManager() const ragEngine = await this.plugin.getRAGEngine() this.root.render( @@ -81,17 +83,19 @@ export class ChatView extends ItemView { > - - - - - - - - - + + + + + + + + + + + diff --git a/src/components/chat-view/Chat.tsx b/src/components/chat-view/Chat.tsx index 3d30625..74381f3 100644 --- a/src/components/chat-view/Chat.tsx +++ b/src/components/chat-view/Chat.tsx @@ -479,12 +479,6 @@ const Chat = forwardRef((props, ref) => { > - - - - - - diff --git a/src/components/chat-view/CreateTemplateDialog.tsx b/src/components/chat-view/CreateTemplateDialog.tsx index c43ae55..ea2f512 100644 --- a/src/components/chat-view/CreateTemplateDialog.tsx +++ b/src/components/chat-view/CreateTemplateDialog.tsx @@ -2,11 +2,14 @@ import { $generateNodesFromSerializedNodes } from '@lexical/clipboard' import { BaseSerializedNode } from '@lexical/clipboard/clipboard' import { InitialEditorStateType } from '@lexical/react/LexicalComposer' import * as Dialog from '@radix-ui/react-dialog' -import { $insertNodes, $parseSerializedNode, LexicalEditor } from 'lexical' +import { $insertNodes, LexicalEditor } from 'lexical' import { X } from 'lucide-react' +import { Notice } from 'obsidian' import { useRef, useState } from 'react' +import { useDatabase } from '../../contexts/database-context' import { useDialogContainer } from '../../contexts/dialog-container-context' +import { DuplicateTemplateException } from '../../database/exception' import LexicalContentEditable from './chat-input/LexicalContentEditable' @@ -17,10 +20,13 @@ import LexicalContentEditable from './chat-input/LexicalContentEditable' */ export default function CreateTemplateDialogContent({ selectedSerializedNodes, + onClose, }: { selectedSerializedNodes?: BaseSerializedNode[] | null + onClose: () => void }) { const container = useDialogContainer() + const { templateManager } = useDatabase() const [templateName, setTemplateName] = useState('') const editorRef = useRef(null) @@ -38,72 +44,57 @@ export default function CreateTemplateDialogContent({ }) } - const onSubmit = () => { - if (!editorRef.current) return - const serializedEditorState = editorRef.current.toJSON() - const nodes = serializedEditorState.editorState.root.children - // Testing inserting nodes - editorRef.current.update(() => { - const parsedNodes = nodes.map((node) => $parseSerializedNode(node)) - $insertNodes(parsedNodes) - }) + const onSubmit = async () => { + try { + if (!editorRef.current) return + const serializedEditorState = editorRef.current.toJSON() + const nodes = serializedEditorState.editorState.root.children + if (nodes.length === 0) return + if (templateName.trim().length === 0) { + new Notice('Please enter a name for your template') + return + } + + await templateManager.createTemplate({ + name: templateName, + data: { nodes }, + }) + new Notice(`Template created: ${templateName}`) + onClose() + } catch (error) { + if (error instanceof DuplicateTemplateException) { + new Notice('A template with this name already exists') + } else { + console.error(error) + new Notice('Failed to create template') + } + } } return ( - - + + Create Template - + Create a new template from the selected nodes -
+
Name
setTemplateName(e.target.value)} - style={{ - flex: 1, + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.stopPropagation() + e.preventDefault() + onSubmit() + } }} + className="smtcmp-tailwind flex-1" />
@@ -116,26 +107,11 @@ export default function CreateTemplateDialogContent({ />
-
+
- { - e.currentTarget.style.opacity = '1' - }} - onMouseLeave={(e) => { - e.currentTarget.style.opacity = '0.7' - }} - asChild - > + diff --git a/src/components/chat-view/chat-input/LexicalContentEditable.tsx b/src/components/chat-view/chat-input/LexicalContentEditable.tsx index 5fb42b0..b19400f 100644 --- a/src/components/chat-view/chat-input/LexicalContentEditable.tsx +++ b/src/components/chat-view/chat-input/LexicalContentEditable.tsx @@ -25,7 +25,8 @@ import OnEnterPlugin from './plugins/on-enter/OnEnterPlugin' import OnMutationPlugin, { NodeMutations, } from './plugins/on-mutation/OnMutationPlugin' -import TemplatePopoverPlugin from './plugins/template-popover/TemplatePopoverPlugin' +import CreateTemplatePopoverPlugin from './plugins/template/CreateTemplatePopoverPlugin' +import TemplatePlugin from './plugins/template/TemplatePlugin' export type LexicalContentEditableProps = { editorRef: RefObject @@ -136,8 +137,9 @@ export default function LexicalContentEditable({ + {plugins?.templatePopover && ( - diff --git a/src/components/chat-view/chat-input/plugins/template-popover/TemplatePopoverPlugin.tsx b/src/components/chat-view/chat-input/plugins/template/CreateTemplatePopoverPlugin.tsx similarity index 65% rename from src/components/chat-view/chat-input/plugins/template-popover/TemplatePopoverPlugin.tsx rename to src/components/chat-view/chat-input/plugins/template/CreateTemplatePopoverPlugin.tsx index 29bf1b2..389d0dd 100644 --- a/src/components/chat-view/chat-input/plugins/template-popover/TemplatePopoverPlugin.tsx +++ b/src/components/chat-view/chat-input/plugins/template/CreateTemplatePopoverPlugin.tsx @@ -3,11 +3,11 @@ import { BaseSerializedNode } from '@lexical/clipboard/clipboard' import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' import * as Dialog from '@radix-ui/react-dialog' import { $getSelection } from 'lexical' -import { useCallback, useEffect, useState } from 'react' +import { CSSProperties, useCallback, useEffect, useRef, useState } from 'react' import CreateTemplateDialogContent from '../../../CreateTemplateDialog' -export default function TemplatePopoverPlugin({ +export default function CreateTemplatePopoverPlugin({ anchorElement, contentEditableElement, }: { @@ -15,11 +15,13 @@ export default function TemplatePopoverPlugin({ contentEditableElement: HTMLElement | null }): JSX.Element | null { const [editor] = useLexicalComposerContext() - const [position, setPosition] = useState<{ - top: number - left: number - } | null>(null) - const [isOpen, setIsOpen] = useState(false) + + const [popoverStyle, setPopoverStyle] = useState(null) + const [popoverWidth, setPopoverWidth] = useState(0) + const [isPopoverOpen, setIsPopoverOpen] = useState(false) + const [isDialogOpen, setIsDialogOpen] = useState(false) + + const popoverRef = useRef(null) const getSelectedSerializedNodes = useCallback((): | BaseSerializedNode[] @@ -36,33 +38,50 @@ export default function TemplatePopoverPlugin({ }, [editor]) const updatePopoverPosition = useCallback(() => { + if (popoverRef.current) { + setPopoverWidth(popoverRef.current.offsetWidth) + } + if (!anchorElement || !contentEditableElement) return const nativeSelection = document.getSelection() const range = nativeSelection?.getRangeAt(0) if (!range || range.collapsed) { - setIsOpen(false) + setIsPopoverOpen(false) return } if (!contentEditableElement.contains(range.commonAncestorContainer)) { - setIsOpen(false) + setIsPopoverOpen(false) return } const rects = Array.from(range.getClientRects()) - // FIXME: Implement better positioning logic - // set position relative to anchorElement + if (rects.length === 0) { + setIsPopoverOpen(false) + return + } const anchorRect = anchorElement.getBoundingClientRect() - setPosition({ - top: rects[rects.length - 1].bottom - anchorRect.top, - left: rects[rects.length - 1].right - anchorRect.left, + const idealLeft = rects[rects.length - 1].right - anchorRect.left + const paddingX = 8 + const paddingY = 4 + const minLeft = popoverWidth + paddingX + const finalLeft = Math.max(minLeft, idealLeft) + setPopoverStyle({ + top: rects[rects.length - 1].bottom - anchorRect.top + paddingY, + left: finalLeft, + transform: 'translate(-100%, 0)', }) const selectedNodes = getSelectedSerializedNodes() if (!selectedNodes) { - setIsOpen(false) + setIsPopoverOpen(false) return } - setIsOpen(true) - }, [anchorElement, contentEditableElement, getSelectedSerializedNodes]) + setIsPopoverOpen(true) + }, [ + anchorElement, + contentEditableElement, + getSelectedSerializedNodes, + popoverWidth, + ]) useEffect(() => { const handleSelectionChange = () => { @@ -95,14 +114,18 @@ export default function TemplatePopoverPlugin({ }, [editor, updatePopoverPosition]) return ( - + - {isOpen ? ( + {isPopoverOpen ? (