This commit is contained in:
duanfuxiang 2025-06-15 19:02:22 +08:00
parent d4776405ba
commit 47e0962f4b
5 changed files with 347 additions and 63 deletions

View File

@ -2,7 +2,7 @@ import * as path from 'path'
import { BaseSerializedNode } from '@lexical/clipboard/clipboard' import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash } from 'lucide-react' import { CircleStop, History, NotebookPen, Plus, Search, Server, SquareSlash, Undo } from 'lucide-react'
import { App, Notice } from 'obsidian' import { App, Notice } from 'obsidian'
import { import {
forwardRef, forwardRef,
@ -70,6 +70,7 @@ import QueryProgress, { QueryProgressState } from './QueryProgress'
import ReactMarkdown from './ReactMarkdown' import ReactMarkdown from './ReactMarkdown'
import SearchView from './SearchView' import SearchView from './SearchView'
import SimilaritySearchResults from './SimilaritySearchResults' import SimilaritySearchResults from './SimilaritySearchResults'
import UserMessageView from './UserMessageView'
import WebsiteReadResults from './WebsiteReadResults' import WebsiteReadResults from './WebsiteReadResults'
// Add an empty line here // Add an empty line here
@ -180,6 +181,9 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history'>('chat') const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history'>('chat')
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([]) const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
// 跟踪正在编辑的消息ID
const [editingMessageId, setEditingMessageId] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
const scrollContainer = chatMessagesRef.current const scrollContainer = chatMessagesRef.current
@ -993,18 +997,6 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
> >
<Plus size={18} /> <Plus size={18} />
</button> </button>
<button
onClick={() => {
if (tab === 'search') {
setTab('chat')
} else {
setTab('search')
}
}}
className="infio-chat-list-dropdown"
>
<Search size={18} color={tab === 'search' ? 'var(--text-accent)' : 'var(--text-color)'} />
</button>
<button <button
onClick={() => { onClick={() => {
if (tab === 'history') { if (tab === 'history') {
@ -1017,6 +1009,18 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
> >
<History size={18} color={tab === 'history' ? 'var(--text-accent)' : 'var(--text-color)'} /> <History size={18} color={tab === 'history' ? 'var(--text-accent)' : 'var(--text-color)'} />
</button> </button>
<button
onClick={() => {
if (tab === 'search') {
setTab('chat')
} else {
setTab('search')
}
}}
className="infio-chat-list-dropdown"
>
<Search size={18} color={tab === 'search' ? 'var(--text-accent)' : 'var(--text-color)'} />
</button>
<button <button
onClick={() => { onClick={() => {
// switch between chat and prompts // switch between chat and prompts
@ -1073,41 +1077,70 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
message.role === 'user' ? ( message.role === 'user' ? (
message.content && message.content &&
<div key={"user-" + message.id} className="infio-chat-messages-user"> <div key={"user-" + message.id} className="infio-chat-messages-user">
<PromptInputWithActions {editingMessageId === message.id ? (
key={"input-" + message.id} <div className="infio-chat-edit-container">
ref={(ref) => registerChatUserInputRef(message.id, ref)} <button
initialSerializedEditorState={message.content} onClick={() => {
onSubmit={(content, useVaultSearch) => { setEditingMessageId(null)
if (editorStateToPlainText(content).trim() === '') return chatUserInputRefs.current.get(inputMessage.id)?.focus()
handleSubmit( }}
[ className="infio-chat-edit-cancel-button"
...chatMessages.slice(0, index), title="取消编辑"
{ >
role: 'user', <Undo size={16} />
applyStatus: ApplyStatus.Idle, </button>
content: content, <PromptInputWithActions
promptContent: null, key={"input-" + message.id}
id: message.id, ref={(ref) => registerChatUserInputRef(message.id, ref)}
mentionables: message.mentionables, initialSerializedEditorState={message.content}
}, onSubmit={(content, useVaultSearch) => {
], if (editorStateToPlainText(content).trim() === '') return
useVaultSearch, setEditingMessageId(null) // 退出编辑模式
) handleSubmit(
chatUserInputRefs.current.get(inputMessage.id)?.focus() [
}} ...chatMessages.slice(0, index),
onFocus={() => { {
setFocusedMessageId(message.id) role: 'user',
}} applyStatus: ApplyStatus.Idle,
onCreateCommand={handleCreateCommand} content: content,
mentionables={message.mentionables} promptContent: null,
setMentionables={(mentionables) => { id: message.id,
setChatMessages((prevChatHistory) => mentionables: message.mentionables,
prevChatHistory.map((msg) => },
msg.id === message.id ? { ...msg, mentionables } : msg, ],
), useVaultSearch,
) )
}} chatUserInputRefs.current.get(inputMessage.id)?.focus()
/> }}
onFocus={() => {
setFocusedMessageId(message.id)
}}
onCreateCommand={handleCreateCommand}
mentionables={message.mentionables}
setMentionables={(mentionables) => {
setChatMessages((prevChatHistory) =>
prevChatHistory.map((msg) =>
msg.id === message.id ? { ...msg, mentionables } : msg,
),
)
}}
/>
</div>
) : (
<UserMessageView
content={message.content}
mentionables={message.mentionables}
onEdit={() => {
setEditingMessageId(message.id)
setFocusedMessageId(message.id)
// 延迟聚焦,确保组件已渲染
setTimeout(() => {
chatUserInputRefs.current.get(message.id)?.focus()
}, 0)
}}
/>
)}
{message.fileReadResults && ( {message.fileReadResults && (
<FileReadResults <FileReadResults
key={"file-read-" + message.id} key={"file-read-" + message.id}

View File

@ -0,0 +1,234 @@
import { SerializedEditorState } from 'lexical'
import { Pencil } from 'lucide-react'
import React, { useState } from 'react'
import { Mentionable } from '../../types/mentionable'
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
import { getMentionableIcon } from './chat-input/utils/get-metionable-icon'
interface UserMessageViewProps {
content: SerializedEditorState | null
mentionables: Mentionable[]
onEdit: () => void
}
const UserMessageView: React.FC<UserMessageViewProps> = ({
content,
mentionables,
onEdit,
}) => {
const [isExpanded, setIsExpanded] = useState(false)
// 将编辑器状态转换为纯文本
const plainText = content ? editorStateToPlainText(content) : ''
// 判断是否需要截断超过2行或超过80个字符
const lines = plainText.split('\n')
const needsTruncation = lines.length > 3 || plainText.length > 80
// 显示的文本内容
let displayText = plainText
if (needsTruncation && !isExpanded) {
// 取前2行或前80个字符取较小值
const truncatedByLines = lines.slice(0, 2).join('\n')
displayText = truncatedByLines.length > 80
? plainText.substring(0, 80) + '...'
: truncatedByLines + (lines.length > 2 ? '...' : '')
}
return (
<div className="infio-user-message-view">
<div className="infio-user-message-content">
{/* 显示 mentionables */}
{mentionables.length > 0 && (
<div className="infio-user-message-mentions">
{mentionables.map((mentionable, index) => {
const Icon = getMentionableIcon(mentionable)
return (
<span key={index} className="infio-mention-tag">
{Icon && <Icon size={12} />}
{mentionable.type === 'current-file' && (
<span>{mentionable.file.name}</span>
)}
{mentionable.type === 'vault' && (
<span>Vault</span>
)}
{mentionable.type === 'block' && (
<span>{mentionable.file.name}</span>
)}
{mentionable.type === 'file' && (
<span>{mentionable.file.name}</span>
)}
{mentionable.type === 'folder' && (
<span>{mentionable.folder.name}</span>
)}
{mentionable.type === 'url' && (
<span>{mentionable.url}</span>
)}
{mentionable.type === 'image' && (
<span>{mentionable.name}</span>
)}
</span>
)
})}
</div>
)}
{/* 显示文本内容 */}
<div className="infio-user-message-text">
<pre>{displayText}</pre>
{/* {needsTruncation && (
<button
className="infio-user-message-expand-btn"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<>
<ChevronUp size={14} />
</>
) : (
<>
<ChevronDown size={14} />
</>
)}
</button>
)} */}
</div>
</div>
{/* 编辑按钮 */}
<button
className="infio-user-message-edit-btn"
onClick={onEdit}
title="编辑消息"
>
<Pencil size={14} />
</button>
<style>
{`
/*
* User Message View
* - Readonly view for user messages with edit functionality
*/
.infio-user-message-view {
position: relative;
display: flex;
align-items: flex-start;
background: var(--background-secondary-alt);
border: 2px solid var(--background-modifier-border);
border-radius: var(--radius-s);
padding: calc(var(--size-2-2) + 1px);
min-height: 62px;
gap: var(--size-2-2);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.15s ease-in-out;
}
.infio-user-message-view:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.infio-user-message-avatar {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: var(--interactive-accent);
border-radius: 50%;
color: var(--text-on-accent);
margin-top: 2px;
}
.infio-user-message-content {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--size-2-1);
min-width: 0; /* 防止内容溢出 */
}
.infio-user-message-mentions {
display: flex;
flex-wrap: wrap;
gap: var(--size-2-1);
}
.infio-mention-tag {
display: inline-flex;
align-items: center;
background-color: var(--background-secondary-alt);
border: 1px solid var(--interactive-accent);
border-radius: var(--radius-s);
font-size: var(--font-smallest);
padding: var(--size-2-1) var(--size-4-1);
gap: var(--size-2-1);
color: var(--interactive-accent);
white-space: nowrap;
font-weight: 500;
}
.infio-user-message-text {
color: var(--text-normal);
font-size: var(--font-ui-medium);
line-height: var(--line-height-normal);
}
.infio-user-message-text pre {
margin: 0;
font-family: inherit;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.infio-user-message-view:hover {
opacity: 1;
}
.infio-user-message-edit-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
color: var(--text-muted);
padding: 0 !important;
margin: 0 !important;
width: 24px !important;
height: 24px !important;
&:hover {
background-color: var(--background-modifier-hover) !important;
}
}
.infio-user-message-expand-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
color: var(--text-muted);
padding: 0 !important;
margin: 0 !important;
width: 24px !important;
height: 24px !important;
&:hover {
background-color: var(--background-modifier-hover) !important;
}
}
`}
</style>
</div>
)
}
export default UserMessageView

View File

@ -55,9 +55,7 @@ export abstract class AbstractJsonRepository<T, M> {
// List metadata for all records by parsing file names. // List metadata for all records by parsing file names.
public async listMetadata(): Promise<(M & { fileName: string })[]> { public async listMetadata(): Promise<(M & { fileName: string })[]> {
console.log('AbstractJsonRepository - listMetadata called for dataDir:', this.dataDir)
const files = await this.app.vault.adapter.list(this.dataDir) const files = await this.app.vault.adapter.list(this.dataDir)
console.log('AbstractJsonRepository - files in directory:', files)
const result = files.files const result = files.files
.map((filePath) => path.basename(filePath)) .map((filePath) => path.basename(filePath))
.filter((fileName) => fileName.endsWith('.json')) .filter((fileName) => fileName.endsWith('.json'))
@ -68,7 +66,6 @@ export abstract class AbstractJsonRepository<T, M> {
.filter( .filter(
(metadata): metadata is M & { fileName: string } => metadata !== null, (metadata): metadata is M & { fileName: string } => metadata !== null,
) )
console.log('AbstractJsonRepository - parsed metadata:', result)
return result return result
} }

View File

@ -26,10 +26,7 @@ export function useChatHistory(): UseChatHistory {
const [chatList, setChatList] = useState<ChatConversationMeta[]>([]) const [chatList, setChatList] = useState<ChatConversationMeta[]>([])
const fetchChatList = useCallback(async () => { const fetchChatList = useCallback(async () => {
console.log('useChatHistory - fetching chat list...')
const conversations = await chatManager.listChats() const conversations = await chatManager.listChats()
console.log('useChatHistory - fetched conversations:', conversations)
console.log('useChatHistory - conversations length:', conversations.length)
setChatList(conversations) setChatList(conversations)
}, [chatManager]) }, [chatManager])
@ -41,21 +38,17 @@ export function useChatHistory(): UseChatHistory {
() => () =>
debounce( debounce(
async (id: string, messages: ChatMessage[]): Promise<void> => { async (id: string, messages: ChatMessage[]): Promise<void> => {
console.log('useChatHistory - createOrUpdateConversation called with id:', id, 'messages length:', messages.length)
const serializedMessages = messages.map(serializeChatMessage) const serializedMessages = messages.map(serializeChatMessage)
const existingConversation = await chatManager.findById(id) const existingConversation = await chatManager.findById(id)
if (existingConversation) { if (existingConversation) {
console.log('useChatHistory - updating existing conversation:', existingConversation.id)
if (isEqual(existingConversation.messages, serializedMessages)) { if (isEqual(existingConversation.messages, serializedMessages)) {
console.log('useChatHistory - messages are identical, skipping update')
return return
} }
await chatManager.updateChat(existingConversation.id, { await chatManager.updateChat(existingConversation.id, {
messages: serializedMessages, messages: serializedMessages,
}) })
} else { } else {
console.log('useChatHistory - creating new conversation')
const firstUserMessage = messages.find((v) => v.role === 'user') as ChatUserMessage const firstUserMessage = messages.find((v) => v.role === 'user') as ChatUserMessage
const newChat = await chatManager.createChat({ const newChat = await chatManager.createChat({
@ -68,10 +61,8 @@ export function useChatHistory(): UseChatHistory {
: 'New chat', : 'New chat',
messages: serializedMessages, messages: serializedMessages,
}) })
console.log('useChatHistory - created new chat:', newChat)
} }
console.log('useChatHistory - refreshing chat list after create/update')
await fetchChatList() await fetchChatList()
}, },
300, 300,

View File

@ -457,6 +457,35 @@ button:not(.clickable-icon).infio-chat-list-dropdown {
} }
} }
/* Chat editing cancel button */
.infio-chat-edit-container {
position: relative;
}
.infio-chat-edit-cancel-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
color: var(--text-muted);
padding: 0 !important;
margin: 0 !important;
width: 24px !important;
height: 24px !important;
&:hover {
background-color: var(--background-modifier-hover) !important;
}
}
/* /*
* Interactive States * Interactive States
* - Hover and active states * - Hover and active states
@ -750,8 +779,9 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
*/ */
.infio-chat-lexical-content-editable-root { .infio-chat-lexical-content-editable-root {
min-height: 46px; min-height: 62px;
max-height: 120px; max-height: 500px;
overflow-y: auto;
} }
.infio-chat-lexical-content-editable-root .mention { .infio-chat-lexical-content-editable-root .mention {
@ -2389,4 +2419,3 @@ button.infio-chat-input-model-select {
max-height: 80vh; max-height: 80vh;
} }