update history
This commit is contained in:
parent
b3ef7afa0c
commit
f6f14a2d64
@ -1,204 +1,491 @@
|
||||
import * as Popover from '@radix-ui/react-popover'
|
||||
import { Pencil, Trash2 } from 'lucide-react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Clock, MessageSquare, Pencil, Search, Trash2 } from 'lucide-react'
|
||||
import { Notice } from 'obsidian'
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { useChatHistory } from '../../hooks/use-chat-history'
|
||||
import { t } from '../../lang/helpers'
|
||||
import { ChatConversationMeta } from '../../types/chat'
|
||||
|
||||
|
||||
function TitleInput({
|
||||
title,
|
||||
onSubmit,
|
||||
}: {
|
||||
title: string
|
||||
onSubmit: (title: string) => Promise<void>
|
||||
}) {
|
||||
const [value, setValue] = useState(title)
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.select()
|
||||
inputRef.current.scrollLeft = 0
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value}
|
||||
className="infio-chat-list-dropdown-item-title-input"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.key === 'Enter') {
|
||||
onSubmit(value)
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
maxLength={100}
|
||||
/>
|
||||
)
|
||||
export interface ChatHistoryViewProps {
|
||||
currentConversationId?: string
|
||||
onSelect?: (conversationId: string) => void
|
||||
onDelete?: (conversationId: string) => void
|
||||
onUpdateTitle?: (conversationId: string, newTitle: string) => void
|
||||
}
|
||||
|
||||
function ChatListItem({
|
||||
title,
|
||||
isFocused,
|
||||
isEditing,
|
||||
onMouseEnter,
|
||||
onSelect,
|
||||
onDelete,
|
||||
onStartEdit,
|
||||
onFinishEdit,
|
||||
}: {
|
||||
title: string
|
||||
isFocused: boolean
|
||||
isEditing: boolean
|
||||
onMouseEnter: () => void
|
||||
onSelect: () => Promise<void>
|
||||
onDelete: () => Promise<void>
|
||||
onStartEdit: () => void
|
||||
onFinishEdit: (title: string) => Promise<void>
|
||||
}) {
|
||||
const itemRef = useRef<HTMLLIElement>(null)
|
||||
const ChatHistoryView = ({
|
||||
currentConversationId,
|
||||
onSelect,
|
||||
onDelete,
|
||||
onUpdateTitle,
|
||||
}: ChatHistoryViewProps) => {
|
||||
const {
|
||||
deleteConversation,
|
||||
updateConversationTitle,
|
||||
chatList,
|
||||
} = useChatHistory()
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused && itemRef.current) {
|
||||
itemRef.current.scrollIntoView({
|
||||
block: 'nearest',
|
||||
})
|
||||
}
|
||||
}, [isFocused])
|
||||
// search term
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
return (
|
||||
<li
|
||||
ref={itemRef}
|
||||
onClick={onSelect}
|
||||
onMouseEnter={onMouseEnter}
|
||||
className={isFocused ? 'selected' : ''}
|
||||
>
|
||||
{isEditing ? (
|
||||
<TitleInput title={title} onSubmit={onFinishEdit} />
|
||||
) : (
|
||||
<div className="infio-chat-list-dropdown-item-title">{title}</div>
|
||||
)}
|
||||
<div className="infio-chat-list-dropdown-item-actions">
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onStartEdit()
|
||||
}}
|
||||
className="infio-chat-list-dropdown-item-icon"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</div>
|
||||
<div
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation()
|
||||
await onDelete()
|
||||
}}
|
||||
className="infio-chat-list-dropdown-item-icon"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
// editing conversation id
|
||||
const [editingConversationId, setEditingConversationId] = useState<string | null>(null)
|
||||
|
||||
const titleInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
|
||||
|
||||
// handle search
|
||||
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSearchTerm(e.target.value)
|
||||
}
|
||||
|
||||
// filter conversations list
|
||||
const filteredConversations = useMemo(() => {
|
||||
if (!searchTerm.trim()) {
|
||||
return chatList
|
||||
}
|
||||
return chatList.filter(
|
||||
conversation =>
|
||||
conversation.title.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
}, [chatList, searchTerm])
|
||||
|
||||
// delete conversation
|
||||
const handleDeleteConversation = async (id: string) => {
|
||||
try {
|
||||
await deleteConversation(id)
|
||||
onDelete?.(id)
|
||||
} catch (error) {
|
||||
new Notice(String(t('chat.errors.failedToDeleteConversation')))
|
||||
console.error('Failed to delete conversation', error)
|
||||
}
|
||||
}
|
||||
|
||||
// edit conversation title
|
||||
const handleEditConversation = (conversation: ChatConversationMeta) => {
|
||||
setEditingConversationId(conversation.id)
|
||||
}
|
||||
|
||||
// save edited title
|
||||
const handleSaveEdit = async (id: string) => {
|
||||
const titleInput = titleInputRefs.current.get(id)
|
||||
if (!titleInput || !titleInput.value.trim()) {
|
||||
new Notice(String(t('chat.errors.titleRequired')))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await updateConversationTitle(id, titleInput.value.trim())
|
||||
onUpdateTitle?.(id, titleInput.value.trim())
|
||||
setEditingConversationId(null)
|
||||
} catch (error) {
|
||||
new Notice(String(t('chat.errors.failedToUpdateTitle')))
|
||||
console.error('Failed to update conversation title', error)
|
||||
}
|
||||
}
|
||||
|
||||
// select conversation
|
||||
const handleSelectConversation = (conversationId: string) => {
|
||||
onSelect?.(conversationId)
|
||||
}
|
||||
|
||||
// format date
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
if (date >= today) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
} else if (date >= yesterday) {
|
||||
return t('chat.history.yesterday')
|
||||
} else {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="infio-chat-history-container">
|
||||
{/* header */}
|
||||
<div className="infio-chat-history-header">
|
||||
<div className="infio-chat-history-title">
|
||||
<h2>{t('chat.history.title')}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* description */}
|
||||
<div className="infio-chat-history-tip">
|
||||
{t('chat.history.description')}
|
||||
</div>
|
||||
|
||||
{/* search bar */}
|
||||
<div className="infio-chat-history-search">
|
||||
<Search size={18} className="infio-chat-history-search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={t('chat.history.searchPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={handleSearch}
|
||||
className="infio-chat-history-search-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* conversations list */}
|
||||
<div className="infio-chat-history-list">
|
||||
{filteredConversations.length === 0 ? (
|
||||
<div className="infio-chat-history-empty">
|
||||
<MessageSquare size={48} className="infio-chat-history-empty-icon" />
|
||||
<p>{searchTerm ? t('chat.history.noMatchingChats') : t('chat.history.noChats')}</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredConversations.map(conversation => (
|
||||
<div
|
||||
key={conversation.id}
|
||||
className={`infio-chat-history-item ${currentConversationId === conversation.id ? 'active' : ''}`}
|
||||
>
|
||||
{editingConversationId === conversation.id ? (
|
||||
// edit mode
|
||||
<div className="infio-chat-history-edit-mode">
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={conversation.title}
|
||||
className="infio-chat-history-edit-title"
|
||||
ref={(el) => {
|
||||
if (el) titleInputRefs.current.set(conversation.id, el)
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSaveEdit(conversation.id)
|
||||
} else if (e.key === 'Escape') {
|
||||
setEditingConversationId(null)
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<div className="infio-chat-history-actions">
|
||||
<button
|
||||
onClick={() => handleSaveEdit(conversation.id)}
|
||||
className="infio-chat-history-save-btn"
|
||||
>
|
||||
<span>{t('chat.history.save')}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingConversationId(null)}
|
||||
className="infio-chat-history-cancel-btn"
|
||||
>
|
||||
<span>{t('chat.history.cancel')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
// view mode
|
||||
<div
|
||||
className="infio-chat-history-view-mode"
|
||||
onClick={() => handleSelectConversation(conversation.id)}
|
||||
>
|
||||
<div className="infio-chat-history-content">
|
||||
<div className="infio-chat-history-date">
|
||||
<Clock size={12} />
|
||||
{formatDate(conversation.updatedAt)}
|
||||
</div>
|
||||
<div className="infio-chat-history-conversation-title">{conversation.title}</div>
|
||||
</div>
|
||||
<div className="infio-chat-history-actions">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleEditConversation(conversation)
|
||||
}}
|
||||
className="infio-chat-history-btn"
|
||||
title={t('chat.history.editTitle')}
|
||||
>
|
||||
<Pencil size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteConversation(conversation.id)
|
||||
}}
|
||||
className="infio-chat-history-btn infio-chat-history-delete-btn"
|
||||
title={t('chat.history.deleteConversation')}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Styles */}
|
||||
<style>
|
||||
{`
|
||||
.infio-chat-history-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
gap: 16px;
|
||||
color: var(--text-normal);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
/* 隐藏滚动条 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.infio-chat-history-container::-webkit-scrollbar {
|
||||
display: none; /* Webkit browsers */
|
||||
}
|
||||
|
||||
.infio-chat-history-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.infio-chat-history-title h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.infio-chat-history-tip {
|
||||
color: var(--text-muted);
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.infio-chat-history-add-btn:disabled {
|
||||
background-color: var(--background-modifier-form-field);
|
||||
color: var(--text-faint);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.infio-chat-history-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--background-primary) !important;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
padding: 6px 12px;
|
||||
margin-bottom: var(--size-4-3);
|
||||
transition: all 0.2s ease;
|
||||
height: 36px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.infio-chat-history-search:focus-within {
|
||||
border-color: var(--background-modifier-border-focus);
|
||||
}
|
||||
|
||||
.infio-chat-history-search-icon {
|
||||
color: var(--text-muted);
|
||||
margin-right: 8px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.infio-chat-history-search-input {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
color: var(--text-normal);
|
||||
padding: 4px 0;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
height: 24px;
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.infio-chat-history-search-input::placeholder {
|
||||
color: var(--text-faint);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.infio-chat-history-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
/* 隐藏滚动条 */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
}
|
||||
|
||||
.infio-chat-history-list::-webkit-scrollbar {
|
||||
display: none; /* Webkit browsers */
|
||||
}
|
||||
|
||||
.infio-chat-history-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 16px;
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.infio-chat-history-empty-icon {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.infio-chat-history-item {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-primary);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-chat-history-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
border-color: var(--background-modifier-border-hover);
|
||||
}
|
||||
|
||||
.infio-chat-history-item.active {
|
||||
background-color: var(--background-modifier-active);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.infio-chat-history-view-mode {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.infio-chat-history-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.infio-chat-history-conversation-title {
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.infio-chat-history-date {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.infio-chat-history-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-chat-history-item:hover .infio-chat-history-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.infio-chat-history-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-chat-history-btn:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.infio-chat-history-delete-btn:hover {
|
||||
background-color: var(--background-modifier-error);
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
.infio-chat-history-edit-mode {
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.infio-chat-history-edit-title {
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-normal);
|
||||
padding: var(--size-4-2);
|
||||
font-size: var(--font-ui-small);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.infio-chat-history-edit-title:focus {
|
||||
outline: none;
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.infio-chat-history-save-btn,
|
||||
.infio-chat-history-cancel-btn {
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
color: var(--text-normal);
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-s);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--font-ui-small);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.infio-chat-history-save-btn {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.infio-chat-history-save-btn:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.infio-chat-history-cancel-btn {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.infio-chat-history-cancel-btn:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ChatHistory({
|
||||
chatList,
|
||||
currentConversationId,
|
||||
onSelect,
|
||||
onDelete,
|
||||
onUpdateTitle,
|
||||
className,
|
||||
children,
|
||||
}: {
|
||||
chatList: ChatConversationMeta[]
|
||||
currentConversationId: string
|
||||
onSelect: (conversationId: string) => Promise<void>
|
||||
onDelete: (conversationId: string) => Promise<void>
|
||||
onUpdateTitle: (conversationId: string, newTitle: string) => Promise<void>
|
||||
className?: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [focusedIndex, setFocusedIndex] = useState<number>(0)
|
||||
const [editingId, setEditingId] = useState<string | null>(null)
|
||||
export default ChatHistoryView
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
const currentIndex = chatList.findIndex(
|
||||
(chat) => chat.id === currentConversationId,
|
||||
)
|
||||
setFocusedIndex(currentIndex === -1 ? 0 : currentIndex)
|
||||
setEditingId(null)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'ArrowUp') {
|
||||
setFocusedIndex(Math.max(0, focusedIndex - 1))
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
setFocusedIndex(Math.min(chatList.length - 1, focusedIndex + 1))
|
||||
} else if (e.key === 'Enter') {
|
||||
onSelect(chatList[focusedIndex].id)
|
||||
setOpen(false)
|
||||
}
|
||||
},
|
||||
[chatList, focusedIndex, setFocusedIndex, onSelect],
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
||||
<Popover.Trigger asChild>
|
||||
<button className={className}>{children}</button>
|
||||
</Popover.Trigger>
|
||||
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
className="infio-popover infio-chat-list-dropdown-content"
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<ul>
|
||||
{chatList.length === 0 ? (
|
||||
<li className="infio-chat-list-dropdown-empty">
|
||||
{t('chat.history.noConversations')}
|
||||
</li>
|
||||
) : (
|
||||
chatList.map((chat, index) => (
|
||||
<ChatListItem
|
||||
key={chat.id}
|
||||
title={chat.title}
|
||||
isFocused={focusedIndex === index}
|
||||
isEditing={editingId === chat.id}
|
||||
onMouseEnter={() => {
|
||||
setFocusedIndex(index)
|
||||
}}
|
||||
onSelect={async () => {
|
||||
await onSelect(chat.id)
|
||||
setOpen(false)
|
||||
}}
|
||||
onDelete={async () => {
|
||||
await onDelete(chat.id)
|
||||
}}
|
||||
onStartEdit={() => {
|
||||
setEditingId(chat.id)
|
||||
}}
|
||||
onFinishEdit={async (title) => {
|
||||
await onUpdateTitle(chat.id, title)
|
||||
setEditingId(null)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
)
|
||||
}
|
||||
// Export the original ChatHistory component for backward compatibility
|
||||
export { ChatHistoryView as ChatHistory }
|
||||
|
||||
@ -59,7 +59,7 @@ import { fetchUrlsContent, onEnt, webSearch } from '../../utils/web-search'
|
||||
import { ModeSelect } from './chat-input/ModeSelect'; // Start of new group
|
||||
import PromptInputWithActions, { ChatUserInputRef } from './chat-input/PromptInputWithActions'
|
||||
import { editorStateToPlainText } from './chat-input/utils/editor-state-to-plain-text'
|
||||
import { ChatHistory } from './ChatHistoryView'
|
||||
import ChatHistoryView from './ChatHistoryView'
|
||||
import CommandsView from './CommandsView'
|
||||
import CustomModeView from './CustomModeView'
|
||||
import FileReadResults from './FileReadResults'
|
||||
@ -177,7 +177,7 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
}
|
||||
}
|
||||
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search'>('chat')
|
||||
const [tab, setTab] = useState<'chat' | 'commands' | 'custom-mode' | 'mcp' | 'search' | 'history'>('chat')
|
||||
|
||||
const [selectedSerializedNodes, setSelectedSerializedNodes] = useState<BaseSerializedNode[]>([])
|
||||
|
||||
@ -1005,36 +1005,18 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
>
|
||||
<Search size={18} color={tab === 'search' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<ChatHistory
|
||||
chatList={chatList}
|
||||
currentConversationId={currentConversationId}
|
||||
onSelect={async (conversationId) => {
|
||||
if (tab !== 'chat') {
|
||||
<button
|
||||
onClick={() => {
|
||||
if (tab === 'history') {
|
||||
setTab('chat')
|
||||
} else {
|
||||
setTab('history')
|
||||
}
|
||||
if (conversationId === currentConversationId) return
|
||||
await handleLoadConversation(conversationId)
|
||||
}}
|
||||
onDelete={async (conversationId) => {
|
||||
await deleteConversation(conversationId)
|
||||
if (conversationId === currentConversationId) {
|
||||
const nextConversation = chatList.find(
|
||||
(chat) => chat.id !== conversationId,
|
||||
)
|
||||
if (nextConversation) {
|
||||
void handleLoadConversation(nextConversation.id)
|
||||
} else {
|
||||
handleNewChat()
|
||||
}
|
||||
}
|
||||
}}
|
||||
onUpdateTitle={async (conversationId, newTitle) => {
|
||||
await updateConversationTitle(conversationId, newTitle)
|
||||
}}
|
||||
className="infio-chat-list-dropdown"
|
||||
>
|
||||
<History size={18} />
|
||||
</ChatHistory>
|
||||
<History size={18} color={tab === 'history' ? 'var(--text-accent)' : 'var(--text-color)'} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
// switch between chat and prompts
|
||||
@ -1211,6 +1193,33 @@ const Chat = forwardRef<ChatRef, ChatProps>((props, ref) => {
|
||||
<div className="infio-chat-commands">
|
||||
<CustomModeView />
|
||||
</div>
|
||||
) : tab === 'history' ? (
|
||||
<div className="infio-chat-commands">
|
||||
<ChatHistoryView
|
||||
currentConversationId={currentConversationId}
|
||||
onSelect={async (conversationId) => {
|
||||
setTab('chat')
|
||||
if (conversationId === currentConversationId) return
|
||||
await handleLoadConversation(conversationId)
|
||||
}}
|
||||
onDelete={async (conversationId) => {
|
||||
await deleteConversation(conversationId)
|
||||
if (conversationId === currentConversationId) {
|
||||
const nextConversation = chatList.find(
|
||||
(chat) => chat.id !== conversationId,
|
||||
)
|
||||
if (nextConversation) {
|
||||
void handleLoadConversation(nextConversation.id)
|
||||
} else {
|
||||
handleNewChat()
|
||||
}
|
||||
}
|
||||
}}
|
||||
onUpdateTitle={async (conversationId, newTitle) => {
|
||||
await updateConversationTitle(conversationId, newTitle)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="infio-chat-commands">
|
||||
<McpHubView />
|
||||
|
||||
@ -55,8 +55,10 @@ 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)
|
||||
return files.files
|
||||
console.log('AbstractJsonRepository - files in directory:', files)
|
||||
const result = files.files
|
||||
.map((filePath) => path.basename(filePath))
|
||||
.filter((fileName) => fileName.endsWith('.json'))
|
||||
.map((fileName) => {
|
||||
@ -66,6 +68,8 @@ export abstract class AbstractJsonRepository<T, M> {
|
||||
.filter(
|
||||
(metadata): metadata is M & { fileName: string } => metadata !== null,
|
||||
)
|
||||
console.log('AbstractJsonRepository - parsed metadata:', result)
|
||||
return result
|
||||
}
|
||||
|
||||
public async read(fileName: string): Promise<T | null> {
|
||||
|
||||
@ -111,7 +111,12 @@ export class ChatManager extends AbstractJsonRepository<
|
||||
}
|
||||
|
||||
public async listChats(): Promise<ChatConversationMeta[]> {
|
||||
console.log('ChatManager - listChats called')
|
||||
console.log('ChatManager - data directory:', this.dataDir)
|
||||
const metadata = await this.listMetadata()
|
||||
return metadata.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
console.log('ChatManager - raw metadata:', metadata)
|
||||
const sorted = metadata.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
console.log('ChatManager - sorted chats:', sorted)
|
||||
return sorted
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +26,10 @@ 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])
|
||||
|
||||
@ -38,20 +41,24 @@ 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
|
||||
|
||||
await chatManager.createChat({
|
||||
const newChat = await chatManager.createChat({
|
||||
id,
|
||||
title: firstUserMessage?.content
|
||||
? editorStateToPlainText(firstUserMessage.content).substring(
|
||||
@ -61,8 +68,10 @@ 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,
|
||||
|
||||
@ -20,7 +20,10 @@ export default {
|
||||
conversationNotFound: "Conversation not found",
|
||||
fileNotFound: "File not found: {{path}}",
|
||||
failedToApplyEditChanges: "Failed to apply edit changes",
|
||||
failedToSearchAndReplace: "Failed to search and replace"
|
||||
failedToSearchAndReplace: "Failed to search and replace",
|
||||
failedToDeleteConversation: "Failed to delete conversation",
|
||||
titleRequired: "Title is required",
|
||||
failedToUpdateTitle: "Failed to update title"
|
||||
},
|
||||
apply: {
|
||||
changesApplied: "Changes successfully applied",
|
||||
@ -30,7 +33,21 @@ export default {
|
||||
noResultsFound: "No results found for '{{query}}'"
|
||||
},
|
||||
history: {
|
||||
noConversations: "No conversations"
|
||||
title: "Chat History",
|
||||
description: "Manage your conversation history and switch between different chats",
|
||||
noConversations: "No conversations",
|
||||
noSearchResults: "No search results found",
|
||||
noMatchingChats: "No matching chats found",
|
||||
noChats: "No chats available",
|
||||
newChat: "New Chat",
|
||||
searchPlaceholder: "Search conversations...",
|
||||
editTitle: "Edit title",
|
||||
deleteChat: "Delete chat",
|
||||
deleteConversation: "Delete conversation",
|
||||
save: "Save",
|
||||
cancel: "Cancel",
|
||||
yesterday: "Yesterday",
|
||||
daysAgo: "days ago"
|
||||
},
|
||||
shortcutInfo: {
|
||||
editInline: "Edit inline",
|
||||
|
||||
@ -21,7 +21,10 @@ export default {
|
||||
conversationNotFound: "未找到对话",
|
||||
fileNotFound: "未找到文件:{{path}}",
|
||||
failedToApplyEditChanges: "应用编辑更改失败",
|
||||
failedToSearchAndReplace: "搜索和替换失败"
|
||||
failedToSearchAndReplace: "搜索和替换失败",
|
||||
failedToDeleteConversation: "删除对话失败",
|
||||
titleRequired: "标题不能为空",
|
||||
failedToUpdateTitle: "更新标题失败"
|
||||
},
|
||||
apply: {
|
||||
changesApplied: "更改已成功应用",
|
||||
@ -31,7 +34,21 @@ export default {
|
||||
noResultsFound: "未找到 '{{query}}' 的结果"
|
||||
},
|
||||
history: {
|
||||
noConversations: "没有对话"
|
||||
title: "聊天记录",
|
||||
description: "管理您的对话历史,并在不同聊天之间切换",
|
||||
noConversations: "没有对话",
|
||||
noSearchResults: "未找到搜索结果",
|
||||
noMatchingChats: "未找到匹配的聊天",
|
||||
noChats: "没有可用的聊天",
|
||||
newChat: "新建聊天",
|
||||
searchPlaceholder: "搜索对话...",
|
||||
editTitle: "编辑标题",
|
||||
deleteChat: "删除聊天",
|
||||
deleteConversation: "删除对话",
|
||||
save: "保存",
|
||||
cancel: "取消",
|
||||
yesterday: "昨天",
|
||||
daysAgo: "天前"
|
||||
},
|
||||
shortcutInfo: {
|
||||
editInline: "行内编辑",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user