update
This commit is contained in:
parent
d4776405ba
commit
47e0962f4b
@ -2,7 +2,7 @@ import * as path from 'path'
|
||||
|
||||
import { BaseSerializedNode } from '@lexical/clipboard/clipboard'
|
||||
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 {
|
||||
forwardRef,
|
||||
@ -70,6 +70,7 @@ import QueryProgress, { QueryProgressState } from './QueryProgress'
|
||||
import ReactMarkdown from './ReactMarkdown'
|
||||
import SearchView from './SearchView'
|
||||
import SimilaritySearchResults from './SimilaritySearchResults'
|
||||
import UserMessageView from './UserMessageView'
|
||||
import WebsiteReadResults from './WebsiteReadResults'
|
||||
|
||||
// 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 [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
|
||||
|
||||
// 跟踪正在编辑的消息ID
|
||||
const [editingMessageId, setEditingMessageId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const scrollContainer = chatMessagesRef.current
|
||||
@ -993,18 +997,6 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
>
|
||||
<Plus size={18} />
|
||||
</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
|
||||
onClick={() => {
|
||||
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)'} />
|
||||
</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
|
||||
onClick={() => {
|
||||
// switch between chat and prompts
|
||||
@ -1073,41 +1077,70 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
message.role === 'user' ? (
|
||||
message.content &&
|
||||
<div key={"user-" + message.id} className="infio-chat-messages-user">
|
||||
<PromptInputWithActions
|
||||
key={"input-" + message.id}
|
||||
ref={(ref) => 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)
|
||||
}}
|
||||
onCreateCommand={handleCreateCommand}
|
||||
mentionables={message.mentionables}
|
||||
setMentionables={(mentionables) => {
|
||||
setChatMessages((prevChatHistory) =>
|
||||
prevChatHistory.map((msg) =>
|
||||
msg.id === message.id ? { ...msg, mentionables } : msg,
|
||||
),
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{editingMessageId === message.id ? (
|
||||
<div className="infio-chat-edit-container">
|
||||
<button
|
||||
onClick={() => {
|
||||
setEditingMessageId(null)
|
||||
chatUserInputRefs.current.get(inputMessage.id)?.focus()
|
||||
}}
|
||||
className="infio-chat-edit-cancel-button"
|
||||
title="取消编辑"
|
||||
>
|
||||
<Undo size={16} />
|
||||
</button>
|
||||
<PromptInputWithActions
|
||||
key={"input-" + message.id}
|
||||
ref={(ref) => registerChatUserInputRef(message.id, ref)}
|
||||
initialSerializedEditorState={message.content}
|
||||
onSubmit={(content, useVaultSearch) => {
|
||||
if (editorStateToPlainText(content).trim() === '') return
|
||||
setEditingMessageId(null) // 退出编辑模式
|
||||
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)
|
||||
}}
|
||||
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 && (
|
||||
<FileReadResults
|
||||
key={"file-read-" + message.id}
|
||||
|
||||
234
src/components/chat-view/UserMessageView.tsx
Normal file
234
src/components/chat-view/UserMessageView.tsx
Normal 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
|
||||
@ -55,9 +55,7 @@ export abstract class AbstractJsonRepository<T, M> {
|
||||
|
||||
// List metadata for all records by parsing file names.
|
||||
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)
|
||||
console.log('AbstractJsonRepository - files in directory:', files)
|
||||
const result = files.files
|
||||
.map((filePath) => path.basename(filePath))
|
||||
.filter((fileName) => fileName.endsWith('.json'))
|
||||
@ -68,7 +66,6 @@ export abstract class AbstractJsonRepository<T, M> {
|
||||
.filter(
|
||||
(metadata): metadata is M & { fileName: string } => metadata !== null,
|
||||
)
|
||||
console.log('AbstractJsonRepository - parsed metadata:', result)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@ -26,10 +26,7 @@ export function useChatHistory(): UseChatHistory {
|
||||
const [chatList, setChatList] = useState<ChatConversationMeta[]>([])
|
||||
|
||||
const fetchChatList = useCallback(async () => {
|
||||
console.log('useChatHistory - fetching chat list...')
|
||||
const conversations = await chatManager.listChats()
|
||||
console.log('useChatHistory - fetched conversations:', conversations)
|
||||
console.log('useChatHistory - conversations length:', conversations.length)
|
||||
setChatList(conversations)
|
||||
}, [chatManager])
|
||||
|
||||
@ -41,21 +38,17 @@ export function useChatHistory(): UseChatHistory {
|
||||
() =>
|
||||
debounce(
|
||||
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 existingConversation = await chatManager.findById(id)
|
||||
|
||||
if (existingConversation) {
|
||||
console.log('useChatHistory - updating existing conversation:', existingConversation.id)
|
||||
if (isEqual(existingConversation.messages, serializedMessages)) {
|
||||
console.log('useChatHistory - messages are identical, skipping update')
|
||||
return
|
||||
}
|
||||
await chatManager.updateChat(existingConversation.id, {
|
||||
messages: serializedMessages,
|
||||
})
|
||||
} else {
|
||||
console.log('useChatHistory - creating new conversation')
|
||||
const firstUserMessage = messages.find((v) => v.role === 'user') as ChatUserMessage
|
||||
|
||||
const newChat = await chatManager.createChat({
|
||||
@ -68,10 +61,8 @@ export function useChatHistory(): UseChatHistory {
|
||||
: 'New chat',
|
||||
messages: serializedMessages,
|
||||
})
|
||||
console.log('useChatHistory - created new chat:', newChat)
|
||||
}
|
||||
|
||||
console.log('useChatHistory - refreshing chat list after create/update')
|
||||
await fetchChatList()
|
||||
},
|
||||
300,
|
||||
|
||||
35
styles.css
35
styles.css
@ -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
|
||||
* - Hover and active states
|
||||
@ -750,8 +779,9 @@ input[type='text'].infio-chat-list-dropdown-item-title-input {
|
||||
*/
|
||||
|
||||
.infio-chat-lexical-content-editable-root {
|
||||
min-height: 46px;
|
||||
max-height: 120px;
|
||||
min-height: 62px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.infio-chat-lexical-content-editable-root .mention {
|
||||
@ -2389,4 +2419,3 @@ button.infio-chat-input-model-select {
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user