From b38d7bb61c99a6997527c697c0c45dd245d1984e Mon Sep 17 00:00:00 2001 From: Heesu Suh Date: Wed, 23 Oct 2024 20:46:28 +0900 Subject: [PATCH] fix UI & show mentionable content on added --- package-lock.json | 31 ----- package.json | 1 - src/components/chat-view/Chat.tsx | 17 +++ .../chat-view/chat-input/ChatUserInput.tsx | 97 ++++++++++++++ .../chat-view/chat-input/MentionableBadge.tsx | 121 ++++++++---------- src/utils/obsidian.ts | 4 +- styles.css | 20 +-- 7 files changed, 178 insertions(+), 113 deletions(-) diff --git a/package-lock.json b/package-lock.json index de89463..c9d2909 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@anthropic-ai/sdk": "^0.27.3", "@lexical/react": "^0.17.1", "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-hover-card": "^1.1.2", "@tanstack/react-query": "^5.56.2", "diff": "^7.0.0", "fuzzysort": "^3.1.0", @@ -1625,36 +1624,6 @@ } } }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.2.tgz", - "integrity": "sha512-Y5w0qGhysvmqsIy6nQxaPa6mXNKznfoGjOfBgzOjocLxr2XlSjqBMYQQL+FfyogsMuX+m8cZyQGYhJxvxUzO4w==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.1", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.2", - "@radix-ui/react-presence": "1.1.1", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "license": "MIT", diff --git a/package.json b/package.json index 2706bc5..ec9f3c7 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "@anthropic-ai/sdk": "^0.27.3", "@lexical/react": "^0.17.1", "@radix-ui/react-dropdown-menu": "^2.1.2", - "@radix-ui/react-hover-card": "^1.1.2", "@tanstack/react-query": "^5.56.2", "diff": "^7.0.0", "fuzzysort": "^3.1.0", diff --git a/src/components/chat-view/Chat.tsx b/src/components/chat-view/Chat.tsx index fb4564c..36bc35e 100644 --- a/src/components/chat-view/Chat.tsx +++ b/src/components/chat-view/Chat.tsx @@ -30,6 +30,10 @@ import { LLMAPIKeyInvalidException, LLMAPIKeyNotSetException, } from '../../utils/llm/exception' +import { + getMentionableKey, + serializeMentionable, +} from '../../utils/mentionable' import { readTFileContent } from '../../utils/obsidian' import { parseRequestMessages } from '../../utils/prompt' @@ -87,6 +91,16 @@ const Chat = forwardRef((props, ref) => { } return newMessage }) + const [addedBlockKey, setAddedBlockKey] = useState( + props.selectedBlock + ? getMentionableKey( + serializeMentionable({ + type: 'block', + ...props.selectedBlock, + }), + ) + : null, + ) const [chatMessages, setChatMessages] = useState([]) const [focusedMessageId, setFocusedMessageId] = useState(null) const [currentConversationId, setCurrentConversationId] = @@ -356,6 +370,8 @@ const Chat = forwardRef((props, ref) => { ...data, } + setAddedBlockKey(getMentionableKey(serializeMentionable(mentionable))) + if (focusedMessageId === inputMessage.id) { setInputMessage((prevInputMessage) => ({ ...prevInputMessage, @@ -498,6 +514,7 @@ const Chat = forwardRef((props, ref) => { })) }} autoFocus + addedBlockKey={addedBlockKey} /> ) diff --git a/src/components/chat-view/chat-input/ChatUserInput.tsx b/src/components/chat-view/chat-input/ChatUserInput.tsx index 99233cd..79e5590 100644 --- a/src/components/chat-view/chat-input/ChatUserInput.tsx +++ b/src/components/chat-view/chat-input/ChatUserInput.tsx @@ -9,13 +9,16 @@ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' +import { useQuery } from '@tanstack/react-query' import { $nodesOfType, LexicalEditor, SerializedEditorState } from 'lexical' +import { MarkdownView } from 'obsidian' import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, + useState, } from 'react' import { deserializeMentionable, @@ -26,6 +29,7 @@ import { useApp } from '../../../contexts/app-context' import { Mentionable, SerializedMentionable } from '../../../types/mentionable' import { fuzzySearch } from '../../../utils/fuzzy-search' import { getMentionableKey } from '../../../utils/mentionable' +import { readTFileContent } from '../../../utils/obsidian' import MentionableBadge from './MentionableBadge' import { ModelSelect } from './ModelSelect' @@ -53,6 +57,7 @@ export type ChatUserInputProps = { mentionables: Mentionable[] setMentionables: (mentionables: Mentionable[]) => void autoFocus?: boolean + addedBlockKey?: string | null } const ChatUserInput = forwardRef( @@ -65,6 +70,7 @@ const ChatUserInput = forwardRef( mentionables, setMentionables, autoFocus = false, + addedBlockKey, }, ref, ) => { @@ -74,6 +80,16 @@ const ChatUserInput = forwardRef( const contentEditableRef = useRef(null) const updaterRef = useRef(null) + const [displayedMentionableKey, setDisplayedMentionableKey] = useState< + string | null + >(addedBlockKey ?? null) + + useEffect(() => { + if (addedBlockKey) { + setDisplayedMentionableKey(addedBlockKey) + } + }, [addedBlockKey]) + useImperativeHandle(ref, () => ({ focus: () => { contentEditableRef.current?.focus() @@ -120,6 +136,7 @@ const ChatUserInput = forwardRef( return } setMentionables([...mentionables, mentionable]) + setDisplayedMentionableKey(mentionableKey) } const handleMentionNodeMutation = ( @@ -196,6 +213,48 @@ const ChatUserInput = forwardRef( }) } + const { data: fileContent } = useQuery({ + queryKey: [ + 'file', + displayedMentionableKey, + mentionables.map((m) => getMentionableKey(serializeMentionable(m))), // should be updated when mentionables change (especially on delete) + ], + queryFn: async () => { + if (!displayedMentionableKey) return null + + const displayedMentionable = mentionables.find( + (m) => + getMentionableKey(serializeMentionable(m)) === + displayedMentionableKey, + ) + + if (!displayedMentionable) return null + + if ( + displayedMentionable.type === 'file' || + displayedMentionable.type === 'current-file' + ) { + if (!displayedMentionable.file) return null + return await readTFileContent(displayedMentionable.file, app.vault) + } else if (displayedMentionable.type === 'block') { + const fileContent = await readTFileContent( + displayedMentionable.file, + app.vault, + ) + + return fileContent + .split('\n') + .slice( + displayedMentionable.startLine - 1, + displayedMentionable.endLine, + ) + .join('\n') + } + + return null + }, + }) + return (
{mentionables.length > 0 && ( @@ -205,11 +264,49 @@ const ChatUserInput = forwardRef( key={getMentionableKey(serializeMentionable(m))} mentionable={m} onDelete={() => handleMentionableDelete(m)} + onClick={() => { + const mentionableKey = getMentionableKey( + serializeMentionable(m), + ) + if ( + (m.type === 'current-file' || + m.type === 'file' || + m.type === 'block') && + m.file && + mentionableKey === displayedMentionableKey + ) { + setDisplayedMentionableKey(null) + + // Open the file if clicked again + const existingLeaf = app.workspace + .getLeavesOfType('markdown') + .find( + (leaf) => + leaf.view instanceof MarkdownView && + leaf.view.file?.path === m.file?.path, + ) + + if (existingLeaf) { + app.workspace.setActiveLeaf(existingLeaf, { focus: true }) + } else { + const leaf = app.workspace.getLeaf('tab') + leaf.openFile(m.file) + } + } else { + setDisplayedMentionableKey(mentionableKey) + } + }} /> ))}
)} + {fileContent && ( +
+ {fileContent} +
+ )} + {/* There was two approach to make mentionable node copy and pasteable. diff --git a/src/components/chat-view/chat-input/MentionableBadge.tsx b/src/components/chat-view/chat-input/MentionableBadge.tsx index 4896912..63c7551 100644 --- a/src/components/chat-view/chat-input/MentionableBadge.tsx +++ b/src/components/chat-view/chat-input/MentionableBadge.tsx @@ -1,10 +1,6 @@ -import * as HoverCard from '@radix-ui/react-hover-card' -import { useQuery } from '@tanstack/react-query' import { X } from 'lucide-react' -import { MarkdownView } from 'obsidian' import { PropsWithChildren } from 'react' -import { useApp } from '../../../contexts/app-context' import { Mentionable, MentionableBlock, @@ -15,15 +11,20 @@ import { function BadgeBase({ children, onDelete, + onClick, }: PropsWithChildren<{ onDelete: () => void + onClick: () => void }>) { return ( -
+
{children}
{ + evt.stopPropagation() + onDelete() + }} >
@@ -34,95 +35,53 @@ function BadgeBase({ function FileBadge({ mentionable, onDelete, + onClick, }: { mentionable: MentionableFile onDelete: () => void + onClick: () => void }) { - const app = useApp() - const { data: fileContent } = useQuery({ - queryKey: ['file', mentionable.file.path], - queryFn: () => app.vault.cachedRead(mentionable.file), - }) - - const handleClick = () => { - const existingLeaf = app.workspace - .getLeavesOfType('markdown') - .find( - (leaf) => - leaf.view instanceof MarkdownView && - leaf.view.file?.path === mentionable.file.path, - ) - - if (existingLeaf) { - app.workspace.setActiveLeaf(existingLeaf, { focus: true }) - } else { - const leaf = app.workspace.getLeaf('tab') - leaf.openFile(mentionable.file) - } - } - return ( - - - -
- {mentionable.file.name} -
-
-
- - - {fileContent} - - -
+ +
+ {mentionable.file.name} +
+
) } function CurrentFileBadge({ mentionable, onDelete, + onClick, }: { mentionable: MentionableCurrentFile onDelete: () => void + onClick: () => void }) { - const app = useApp() - const { data: fileContent } = useQuery({ - queryKey: ['file', mentionable.file?.path], - queryFn: () => - mentionable.file ? app.vault.cachedRead(mentionable?.file) : null, - }) - return mentionable.file ? ( - - - -
- {`${mentionable.file.name}`} -
-
- {' (Current File)'} -
-
-
- - - {fileContent} - - -
+ +
+ {`${mentionable.file.name}`} +
+
+ {' (Current File)'} +
+
) : null } function BlockBadge({ mentionable, onDelete, + onClick, }: { mentionable: MentionableBlock onDelete: () => void + onClick: () => void }) { return ( - +
{`${mentionable.file.name}`}
@@ -136,16 +95,36 @@ function BlockBadge({ export default function MentionableBadge({ mentionable, onDelete, + onClick, }: { mentionable: Mentionable onDelete: () => void + onClick: () => void }) { switch (mentionable.type) { case 'file': - return + return ( + + ) case 'current-file': - return + return ( + + ) case 'block': - return + return ( + + ) } } diff --git a/src/utils/obsidian.ts b/src/utils/obsidian.ts index d12d9d7..ad10604 100644 --- a/src/utils/obsidian.ts +++ b/src/utils/obsidian.ts @@ -39,8 +39,8 @@ export async function getMentionableBlockData( return { content: selectionContent, file, - startLine, - endLine, + startLine: startLine + 1, // +1 because startLine is 0-indexed + endLine: endLine + 1, // +1 because startLine is 0-indexed } } diff --git a/styles.css b/styles.css index 6c24f7f..997437f 100644 --- a/styles.css +++ b/styles.css @@ -223,6 +223,11 @@ button:not(.clickable-icon).smtcmp-chat-list-dropdown { white-space: nowrap; } +.smtcmp-chat-user-input-file-badge:hover { + background-color: var(--background-modifier-hover); + cursor: pointer; +} + .smtcmp-chat-user-input-file-badge-delete { cursor: pointer; display: flex; @@ -252,18 +257,17 @@ button:not(.clickable-icon).smtcmp-chat-list-dropdown { color: var(--color-base-50); } -.smtcmp-chat-mentionable-hover-content { +/* + * TODO: Fix style + */ +.smtcmp-chat-user-input-file-content-preview { background-color: var(--background-primary); - border-radius: var(--radius-m); + border-radius: var(--radius-s); border: 1px solid var(--background-modifier-border); padding: var(--size-4-2); - box-shadow: var(--shadow-s); - white-space: pre-line; - max-width: 300px; - z-index: 1000; - max-height: 300px; + max-height: 400px; overflow-y: auto; - font-size: var(--font-ui-small); + white-space: pre-line; } /**