Skip to content

Commit

Permalink
Implement template plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
kevin-on committed Oct 31, 2024
1 parent e27a8a4 commit 2d5d512
Show file tree
Hide file tree
Showing 7 changed files with 511 additions and 110 deletions.
26 changes: 15 additions & 11 deletions src/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -68,6 +69,7 @@ export class ChatView extends ItemView {
},
},
})
const dbManager = await this.plugin.getDbManager()
const ragEngine = await this.plugin.getRAGEngine()

this.root.render(
Expand All @@ -81,17 +83,19 @@ export class ChatView extends ItemView {
>
<DarkModeProvider>
<LLMProvider>
<RAGProvider ragEngine={ragEngine}>
<QueryClientProvider client={queryClient}>
<React.StrictMode>
<DialogContainerProvider
container={this.containerEl.children[1] as HTMLElement}
>
<Chat ref={this.chatRef} {...this.initialChatProps} />
</DialogContainerProvider>
</React.StrictMode>
</QueryClientProvider>
</RAGProvider>
<DatabaseProvider databaseManager={dbManager}>
<RAGProvider ragEngine={ragEngine}>
<QueryClientProvider client={queryClient}>
<React.StrictMode>
<DialogContainerProvider
container={this.containerEl.children[1] as HTMLElement}
>
<Chat ref={this.chatRef} {...this.initialChatProps} />
</DialogContainerProvider>
</React.StrictMode>
</QueryClientProvider>
</RAGProvider>
</DatabaseProvider>
</LLMProvider>
</DarkModeProvider>
</SettingsProvider>
Expand Down
6 changes: 0 additions & 6 deletions src/components/chat-view/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -479,12 +479,6 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
>
<Plus size={18} />
</button>
<Dialog.Root modal={false}>
<Dialog.Trigger asChild>
<button>Create Template</button>
</Dialog.Trigger>
<CreateTemplateDialogContent />
</Dialog.Root>
<ChatListDropdown
chatList={chatList}
onSelectConversation={(conversationId) =>
Expand Down
114 changes: 45 additions & 69 deletions src/components/chat-view/CreateTemplateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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<LexicalEditor | null>(null)
Expand All @@ -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 (
<Dialog.Portal container={container}>
<Dialog.Content
style={{
position: 'fixed',
left: '50%',
top: '50%',
zIndex: 50,
display: 'grid',
width: '100%',
maxWidth: '32rem',
transform: 'translate(-50%, -50%)',
gap: 'var(--size-4-4)',
border: 'var(--border-width) solid var(--background-modifier-border)',
backgroundColor: 'var(--background-secondary)',
padding: 'var(--size-4-6)',
transitionDuration: '200ms',
borderRadius: 'var(--radius-m)',
}}
>
<Dialog.Title
style={{
fontSize: 'var(--font-ui-larger)',
fontWeight: 'var(--font-semibold)',
lineHeight: 'var(--line-height-tight)',
margin: 0,
}}
>
<Dialog.Content className="smtcmp-dialog-content">
<Dialog.Title className="smtcmp-dialog-title">
Create Template
</Dialog.Title>
<Dialog.Description
style={{
fontSize: 'var(--font-ui-small)',
color: 'var(--text-muted)',
margin: 0,
}}
>
<Dialog.Description className="smtcmp-dialog-description">
Create a new template from the selected nodes
</Dialog.Description>

<div
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--size-4-4)',
}}
>
<div className="smtcmp-tailwind flex items-center gap-4">
<div>Name</div>
<input
type="text"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
style={{
flex: 1,
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.stopPropagation()
e.preventDefault()
onSubmit()
}
}}
className="smtcmp-tailwind flex-1"
/>
</div>

Expand All @@ -116,26 +107,11 @@ export default function CreateTemplateDialogContent({
/>
</div>

<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<div className="smtcmp-tailwind flex justify-end">
<button onClick={onSubmit}>Create Template</button>
</div>

<Dialog.Close
style={{
position: 'absolute',
right: 'var(--size-4-4)',
top: 'var(--size-4-4)',
cursor: 'var(--cursor)',
opacity: 0.7,
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = '1'
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = '0.7'
}}
asChild
>
<Dialog.Close className="smtcmp-dialog-close" asChild>
<X size={16} />
</Dialog.Close>
</Dialog.Content>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<LexicalEditor>
Expand Down Expand Up @@ -136,8 +137,9 @@ export default function LexicalContentEditable({
<EditorRefPlugin editorRef={editorRef} />
<NoFormatPlugin />
<AutoLinkMentionPlugin />
<TemplatePlugin />
{plugins?.templatePopover && (
<TemplatePopoverPlugin
<CreateTemplatePopoverPlugin
anchorElement={plugins.templatePopover.anchorElement}
contentEditableElement={contentEditableRef.current}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,25 @@ 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,
}: {
anchorElement: HTMLElement | null
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<CSSProperties | null>(null)
const [popoverWidth, setPopoverWidth] = useState(0)
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const [isDialogOpen, setIsDialogOpen] = useState(false)

const popoverRef = useRef<HTMLButtonElement>(null)

const getSelectedSerializedNodes = useCallback(():
| BaseSerializedNode[]
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -95,14 +114,18 @@ export default function TemplatePopoverPlugin({
}, [editor, updatePopoverPosition])

return (
<Dialog.Root modal={false}>
<Dialog.Root
modal={false}
open={isDialogOpen}
onOpenChange={setIsDialogOpen}
>
<Dialog.Trigger asChild>
{isOpen ? (
{isPopoverOpen ? (
<button
ref={popoverRef}
style={{
position: 'absolute', // relative to anchorElement
top: position?.top,
left: position?.left,
position: 'absolute',
...popoverStyle,
}}
>
Create template
Expand All @@ -111,6 +134,7 @@ export default function TemplatePopoverPlugin({
</Dialog.Trigger>
<CreateTemplateDialogContent
selectedSerializedNodes={getSelectedSerializedNodes()}
onClose={() => setIsDialogOpen(false)}
/>
</Dialog.Root>
)
Expand Down
Loading

0 comments on commit 2d5d512

Please sign in to comment.