From 43599fca47e88967c8136db46e4212646ff961e4 Mon Sep 17 00:00:00 2001 From: duanfuxiang Date: Sun, 13 Apr 2025 22:52:36 +0800 Subject: [PATCH] add command view --- src/components/chat-view/Chat.tsx | 241 +-- src/components/chat-view/CommandsView.tsx | 243 +++ src/styles.css | 1635 --------------------- styles.css | 289 ++++ 4 files changed, 667 insertions(+), 1741 deletions(-) create mode 100644 src/components/chat-view/CommandsView.tsx delete mode 100644 src/styles.css diff --git a/src/components/chat-view/Chat.tsx b/src/components/chat-view/Chat.tsx index 95e712f..9fae66c 100644 --- a/src/components/chat-view/Chat.tsx +++ b/src/components/chat-view/Chat.tsx @@ -1,7 +1,7 @@ import * as path from 'path' import { useMutation } from '@tanstack/react-query' -import { CircleStop, History, Plus } from 'lucide-react' +import { CircleStop, History, Plus, SquareSlash } from 'lucide-react' import { App, Notice } from 'obsidian' import { forwardRef, @@ -52,6 +52,7 @@ import { ModeSelect } from './chat-input/ModeSelect' import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions' import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text' import { ChatHistory } from './ChatHistory' +import CommandsView from './CommandsView' import MarkdownReasoningBlock from './Markdown/MarkdownReasoningBlock' import QueryProgress, { QueryProgressState } from './QueryProgress' import ReactMarkdown from './ReactMarkdown' @@ -160,6 +161,8 @@ const Chat = forwardRef((props, ref) => { } } + const [tab, setTab] = useState<'chat' | 'commands'>('commands') + useEffect(() => { const scrollContainer = chatMessagesRef.current if (!scrollContainer) return @@ -870,11 +873,15 @@ const Chat = forwardRef((props, ref) => { return (
+ {/* header view */}
-
- { - // If the chat is empty, show a message to start a new chat - chatMessages.length === 0 && ( -
- -
- ) - } - {chatMessages.map((message, index) => - message.role === 'user' ? ( - message.content && -
- registerChatUserInputRef(message.id, ref)} - initialSerializedEditorState={message.content} - onSubmit={(content, useVaultSearch) => { - if (editorStateToPlainText(content).trim() === '') return - handleSubmit( - [ - ...chatMessages.slice(0, index), - { - role: 'user', - applyStatus: ApplyStatus.Idle, - content: content, - promptContent: null, - id: message.id, - mentionables: message.mentionables, - }, - ], - useVaultSearch, - ) - chatUserInputRefs.current.get(inputMessage.id)?.focus() - }} - onFocus={() => { - setFocusedMessageId(message.id) - }} - mentionables={message.mentionables} - setMentionables={(mentionables) => { - setChatMessages((prevChatHistory) => - prevChatHistory.map((msg) => - msg.id === message.id ? { ...msg, mentionables } : msg, - ), - ) - }} - /> - {message.similaritySearchResults && ( - - )} -
- ) : ( -
- - handleApply(message.id, toolArgs)} - applyStatus={message.applyStatus} - > - {message.content} - -
- ), - )} - - {submitMutation.isPending && ( - - )} -
- registerChatUserInputRef(inputMessage.id, ref)} - initialSerializedEditorState={inputMessage.content} - onSubmit={(content, useVaultSearch) => { - if (editorStateToPlainText(content).trim() === '') return - handleSubmit( - [...chatMessages, { ...inputMessage, content }], - useVaultSearch, - ) - setInputMessage(getNewInputMessage(app, settings.defaultMention)) - preventAutoScrollRef.current = false - handleScrollToBottom() - }} - onFocus={() => { - setFocusedMessageId(inputMessage.id) - }} - mentionables={inputMessage.mentionables} - setMentionables={(mentionables) => { - setInputMessage((prevInputMessage) => ({ - ...prevInputMessage, - mentionables, - })) - }} - autoFocus - addedBlockKey={addedBlockKey} - /> + {/* main view */} + {tab === 'chat' ? ( + <> +
+ { + // If the chat is empty, show a message to start a new chat + chatMessages.length === 0 && ( +
+ +
+ ) + } + {chatMessages.map((message, index) => + message.role === 'user' ? ( + message.content && +
+ registerChatUserInputRef(message.id, ref)} + initialSerializedEditorState={message.content} + onSubmit={(content, useVaultSearch) => { + if (editorStateToPlainText(content).trim() === '') return + handleSubmit( + [ + ...chatMessages.slice(0, index), + { + role: 'user', + applyStatus: ApplyStatus.Idle, + content: content, + promptContent: null, + id: message.id, + mentionables: message.mentionables, + }, + ], + useVaultSearch, + ) + chatUserInputRefs.current.get(inputMessage.id)?.focus() + }} + onFocus={() => { + setFocusedMessageId(message.id) + }} + mentionables={message.mentionables} + setMentionables={(mentionables) => { + setChatMessages((prevChatHistory) => + prevChatHistory.map((msg) => + msg.id === message.id ? { ...msg, mentionables } : msg, + ), + ) + }} + /> + {message.similaritySearchResults && ( + + )} +
+ ) : ( +
+ + handleApply(message.id, toolArgs)} + applyStatus={message.applyStatus} + > + {message.content} + +
+ ), + )} + + {submitMutation.isPending && ( + + )} +
+ registerChatUserInputRef(inputMessage.id, ref)} + initialSerializedEditorState={inputMessage.content} + onSubmit={(content, useVaultSearch) => { + if (editorStateToPlainText(content).trim() === '') return + handleSubmit( + [...chatMessages, { ...inputMessage, content }], + useVaultSearch, + ) + setInputMessage(getNewInputMessage(app, settings.defaultMention)) + preventAutoScrollRef.current = false + handleScrollToBottom() + }} + onFocus={() => { + setFocusedMessageId(inputMessage.id) + }} + mentionables={inputMessage.mentionables} + setMentionables={(mentionables) => { + setInputMessage((prevInputMessage) => ({ + ...prevInputMessage, + mentionables, + })) + }} + autoFocus + addedBlockKey={addedBlockKey} + /> + + ) : ( +
+ +
+ )}
) }) diff --git a/src/components/chat-view/CommandsView.tsx b/src/components/chat-view/CommandsView.tsx new file mode 100644 index 0000000..5f77480 --- /dev/null +++ b/src/components/chat-view/CommandsView.tsx @@ -0,0 +1,243 @@ +import { Pencil, Save, Search, Trash2 } from 'lucide-react' +import { useEffect, useRef, useState } from 'react' +import { v4 as uuidv4 } from 'uuid' + +export interface Command { + id: string + title: string + content: string +} + +const CommandsView = () => { + const [commands, setCommands] = useState([]) + const [newCommand, setNewCommand] = useState({ + id: uuidv4(), + title: '', + content: '' + }) + const [searchTerm, setSearchTerm] = useState('') + const [editingCommandId, setEditingCommandId] = useState(null) + + const titleInputRefs = useRef>(new Map()) + const contentInputRefs = useRef>(new Map()) + + // 从本地存储加载commands + useEffect(() => { + const savedCommands = localStorage.getItem('commands') + if (savedCommands) { + try { + const parsedData = JSON.parse(savedCommands) + + // 验证解析的数据是否为符合Prompt接口的数组 + if (Array.isArray(parsedData) && parsedData.every(isCommand)) { + setCommands(parsedData) + } + } catch (error) { + console.error('无法解析保存的命令', error) + } + } + }, []) + + // 类型守卫函数,用于验证对象是否符合Command接口 + function isCommand(item: unknown): item is Command { + if (!item || typeof item !== 'object') { + return false; + } + + // 使用in操作符检查属性存在 + if (!('id' in item) || !('title' in item) || !('content' in item)) { + return false; + } + + // 使用JavaScript的hasOwnProperty和typeof来检查属性类型 + return ( + Object.prototype.hasOwnProperty.call(item, 'id') && + Object.prototype.hasOwnProperty.call(item, 'title') && + Object.prototype.hasOwnProperty.call(item, 'content') && + typeof Reflect.get(item, 'id') === 'string' && + typeof Reflect.get(item, 'title') === 'string' && + typeof Reflect.get(item, 'content') === 'string' + ); + } + + // 保存commands到本地存储 + useEffect(() => { + localStorage.setItem('commands', JSON.stringify(commands)) + }, [commands]) + + // 处理新command的标题变化 + const handleNewCommandTitleChange = (e: React.ChangeEvent) => { + setNewCommand({ ...newCommand, title: e.target.value }) + } + + // 处理新command的内容变化 + const handleNewCommandContentChange = (e: React.ChangeEvent) => { + setNewCommand({ ...newCommand, content: e.target.value }) + } + + // 添加新command + const handleAddCommand = () => { + if (newCommand.title.trim() === '' || newCommand.content.trim() === '') { + return + } + + setCommands([...commands, newCommand]) + setNewCommand({ + id: uuidv4(), + title: '', + content: '' + }) + } + + // 删除command + const handleDeleteCommand = (id: string) => { + setCommands(commands.filter(command => command.id !== id)) + if (editingCommandId === id) { + setEditingCommandId(null) + } + } + + // 编辑command + const handleEditCommand = (command: Command) => { + setEditingCommandId(command.id) + } + + // 保存编辑后的command + const handleSaveEdit = (id: string) => { + const titleInput = titleInputRefs.current.get(id) + const contentInput = contentInputRefs.current.get(id) + + if (titleInput && contentInput) { + setCommands( + commands.map(command => + command.id === id + ? { ...command, title: titleInput.value, content: contentInput.value } + : command + ) + ) + setEditingCommandId(null) + } + } + + // 处理搜索 + const handleSearch = (e: React.ChangeEvent) => { + setSearchTerm(e.target.value) + } + + // 过滤commands列表 + const filteredCommands = commands.filter( + command => + command.title.toLowerCase().includes(searchTerm.toLowerCase()) || + command.content.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + return ( +
+ {/* header */} +
+
+

Create Quick Command

+
Name
+ +
Content
+