update history
This commit is contained in:
parent
f6f14a2d64
commit
d4776405ba
@ -1,4 +1,4 @@
|
|||||||
import { Clock, MessageSquare, Pencil, Search, Trash2 } from 'lucide-react'
|
import { CheckSquare, Clock, Edit3, MessageSquare, Pencil, Search, Square, Trash2, CopyPlus } from 'lucide-react'
|
||||||
import { Notice } from 'obsidian'
|
import { Notice } from 'obsidian'
|
||||||
import React, { useMemo, useRef, useState } from 'react'
|
import React, { useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
@ -31,6 +31,10 @@ const ChatHistoryView = ({
|
|||||||
// editing conversation id
|
// editing conversation id
|
||||||
const [editingConversationId, setEditingConversationId] = useState<string | null>(null)
|
const [editingConversationId, setEditingConversationId] = useState<string | null>(null)
|
||||||
|
|
||||||
|
// selection mode and selected conversations
|
||||||
|
const [selectionMode, setSelectionMode] = useState(false)
|
||||||
|
const [selectedConversations, setSelectedConversations] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
const titleInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
|
const titleInputRefs = useRef<Map<string, HTMLInputElement>>(new Map())
|
||||||
|
|
||||||
// handle search
|
// handle search
|
||||||
@ -49,6 +53,38 @@ const ChatHistoryView = ({
|
|||||||
)
|
)
|
||||||
}, [chatList, searchTerm])
|
}, [chatList, searchTerm])
|
||||||
|
|
||||||
|
// toggle selection mode
|
||||||
|
const toggleSelectionMode = () => {
|
||||||
|
setSelectionMode(!selectionMode)
|
||||||
|
setSelectedConversations(new Set()) // clear selections when toggling
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggle conversation selection
|
||||||
|
const toggleConversationSelection = (conversationId: string) => {
|
||||||
|
const newSelected = new Set(selectedConversations)
|
||||||
|
if (newSelected.has(conversationId)) {
|
||||||
|
newSelected.delete(conversationId)
|
||||||
|
} else {
|
||||||
|
newSelected.add(conversationId)
|
||||||
|
}
|
||||||
|
setSelectedConversations(newSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// select all conversations
|
||||||
|
const selectAllConversations = () => {
|
||||||
|
const allIds = new Set(filteredConversations.map(conv => conv.id))
|
||||||
|
setSelectedConversations(allIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear all selections
|
||||||
|
const clearAllSelections = () => {
|
||||||
|
setSelectedConversations(new Set())
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if all conversations are selected
|
||||||
|
const isAllSelected = filteredConversations.length > 0 &&
|
||||||
|
filteredConversations.every(conv => selectedConversations.has(conv.id))
|
||||||
|
|
||||||
// delete conversation
|
// delete conversation
|
||||||
const handleDeleteConversation = async (id: string) => {
|
const handleDeleteConversation = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
@ -60,6 +96,46 @@ const ChatHistoryView = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// batch delete selected conversations
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
if (selectedConversations.size === 0) {
|
||||||
|
new Notice('请先选择要删除的对话')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// show confirmation
|
||||||
|
const confirmed = confirm(`确定要删除选中的 ${selectedConversations.size} 个对话吗?此操作不可撤销。`)
|
||||||
|
if (!confirmed) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletedIds: string[] = []
|
||||||
|
const errors: string[] = []
|
||||||
|
|
||||||
|
// delete conversations one by one
|
||||||
|
for (const id of selectedConversations) {
|
||||||
|
try {
|
||||||
|
await deleteConversation(id)
|
||||||
|
deletedIds.push(id)
|
||||||
|
onDelete?.(id)
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(id)
|
||||||
|
console.error('Failed to delete conversation', id, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// show results
|
||||||
|
if (deletedIds.length > 0) {
|
||||||
|
new Notice(`成功删除 ${deletedIds.length} 个对话`)
|
||||||
|
}
|
||||||
|
if (errors.length > 0) {
|
||||||
|
new Notice(`${errors.length} 个对话删除失败`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear selections
|
||||||
|
setSelectedConversations(new Set())
|
||||||
|
}
|
||||||
|
|
||||||
// edit conversation title
|
// edit conversation title
|
||||||
const handleEditConversation = (conversation: ChatConversationMeta) => {
|
const handleEditConversation = (conversation: ChatConversationMeta) => {
|
||||||
setEditingConversationId(conversation.id)
|
setEditingConversationId(conversation.id)
|
||||||
@ -85,7 +161,11 @@ const ChatHistoryView = ({
|
|||||||
|
|
||||||
// select conversation
|
// select conversation
|
||||||
const handleSelectConversation = (conversationId: string) => {
|
const handleSelectConversation = (conversationId: string) => {
|
||||||
onSelect?.(conversationId)
|
if (selectionMode) {
|
||||||
|
toggleConversationSelection(conversationId)
|
||||||
|
} else {
|
||||||
|
onSelect?.(conversationId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// format date
|
// format date
|
||||||
@ -98,7 +178,7 @@ const ChatHistoryView = ({
|
|||||||
if (date >= today) {
|
if (date >= today) {
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||||
} else if (date >= yesterday) {
|
} else if (date >= yesterday) {
|
||||||
return t('chat.history.yesterday')
|
return String(t('chat.history.yesterday'))
|
||||||
} else {
|
} else {
|
||||||
return date.toLocaleDateString()
|
return date.toLocaleDateString()
|
||||||
}
|
}
|
||||||
@ -111,19 +191,66 @@ const ChatHistoryView = ({
|
|||||||
<div className="infio-chat-history-title">
|
<div className="infio-chat-history-title">
|
||||||
<h2>{t('chat.history.title')}</h2>
|
<h2>{t('chat.history.title')}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="infio-chat-history-header-actions">
|
||||||
|
<button
|
||||||
|
onClick={toggleSelectionMode}
|
||||||
|
className={`infio-chat-history-selection-btn ${selectionMode ? 'active' : ''}`}
|
||||||
|
title={selectionMode ? '退出选择模式' : '进入选择模式'}
|
||||||
|
>
|
||||||
|
<CopyPlus size={16} />
|
||||||
|
{selectionMode ? '取消' : '多选'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* description */}
|
{/* description */}
|
||||||
<div className="infio-chat-history-tip">
|
<div className="infio-chat-history-tip">
|
||||||
{t('chat.history.description')}
|
{selectionMode
|
||||||
|
? `选择模式 - 已选择 ${selectedConversations.size} 个对话`
|
||||||
|
: String(t('chat.history.description'))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* batch operations bar */}
|
||||||
|
{selectionMode && (
|
||||||
|
<div className="infio-chat-history-batch-actions">
|
||||||
|
<div className="infio-chat-history-select-actions">
|
||||||
|
<button
|
||||||
|
onClick={isAllSelected ? clearAllSelections : selectAllConversations}
|
||||||
|
className="infio-chat-history-select-all-btn"
|
||||||
|
>
|
||||||
|
{isAllSelected ? (
|
||||||
|
<>
|
||||||
|
<CheckSquare size={16} />
|
||||||
|
取消全选
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Square size={16} />
|
||||||
|
全选
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="infio-chat-history-batch-delete">
|
||||||
|
<button
|
||||||
|
onClick={handleBatchDelete}
|
||||||
|
disabled={selectedConversations.size === 0}
|
||||||
|
className="infio-chat-history-batch-delete-btn"
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
批量删除 ({selectedConversations.size})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* search bar */}
|
{/* search bar */}
|
||||||
<div className="infio-chat-history-search">
|
<div className="infio-chat-history-search">
|
||||||
<Search size={18} className="infio-chat-history-search-icon" />
|
<Search size={18} className="infio-chat-history-search-icon" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('chat.history.searchPlaceholder')}
|
placeholder={String(t('chat.history.searchPlaceholder'))}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
className="infio-chat-history-search-input"
|
className="infio-chat-history-search-input"
|
||||||
@ -135,13 +262,13 @@ const ChatHistoryView = ({
|
|||||||
{filteredConversations.length === 0 ? (
|
{filteredConversations.length === 0 ? (
|
||||||
<div className="infio-chat-history-empty">
|
<div className="infio-chat-history-empty">
|
||||||
<MessageSquare size={48} className="infio-chat-history-empty-icon" />
|
<MessageSquare size={48} className="infio-chat-history-empty-icon" />
|
||||||
<p>{searchTerm ? t('chat.history.noMatchingChats') : t('chat.history.noChats')}</p>
|
<p>{searchTerm ? String(t('chat.history.noMatchingChats')) : String(t('chat.history.noChats'))}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
filteredConversations.map(conversation => (
|
filteredConversations.map(conversation => (
|
||||||
<div
|
<div
|
||||||
key={conversation.id}
|
key={conversation.id}
|
||||||
className={`infio-chat-history-item ${currentConversationId === conversation.id ? 'active' : ''}`}
|
className={`infio-chat-history-item ${currentConversationId === conversation.id ? 'active' : ''} ${selectedConversations.has(conversation.id) ? 'selected' : ''}`}
|
||||||
>
|
>
|
||||||
{editingConversationId === conversation.id ? (
|
{editingConversationId === conversation.id ? (
|
||||||
// edit mode
|
// edit mode
|
||||||
@ -167,13 +294,13 @@ const ChatHistoryView = ({
|
|||||||
onClick={() => handleSaveEdit(conversation.id)}
|
onClick={() => handleSaveEdit(conversation.id)}
|
||||||
className="infio-chat-history-save-btn"
|
className="infio-chat-history-save-btn"
|
||||||
>
|
>
|
||||||
<span>{t('chat.history.save')}</span>
|
<span>{String(t('chat.history.save'))}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditingConversationId(null)}
|
onClick={() => setEditingConversationId(null)}
|
||||||
className="infio-chat-history-cancel-btn"
|
className="infio-chat-history-cancel-btn"
|
||||||
>
|
>
|
||||||
<span>{t('chat.history.cancel')}</span>
|
<span>{String(t('chat.history.cancel'))}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -183,6 +310,15 @@ const ChatHistoryView = ({
|
|||||||
className="infio-chat-history-view-mode"
|
className="infio-chat-history-view-mode"
|
||||||
onClick={() => handleSelectConversation(conversation.id)}
|
onClick={() => handleSelectConversation(conversation.id)}
|
||||||
>
|
>
|
||||||
|
{selectionMode && (
|
||||||
|
<div className="infio-chat-history-checkbox">
|
||||||
|
{selectedConversations.has(conversation.id) ? (
|
||||||
|
<CheckSquare size={20} className="infio-chat-history-checkbox-checked" />
|
||||||
|
) : (
|
||||||
|
<Square size={20} className="infio-chat-history-checkbox-unchecked" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="infio-chat-history-content">
|
<div className="infio-chat-history-content">
|
||||||
<div className="infio-chat-history-date">
|
<div className="infio-chat-history-date">
|
||||||
<Clock size={12} />
|
<Clock size={12} />
|
||||||
@ -190,28 +326,30 @@ const ChatHistoryView = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="infio-chat-history-conversation-title">{conversation.title}</div>
|
<div className="infio-chat-history-conversation-title">{conversation.title}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="infio-chat-history-actions">
|
{!selectionMode && (
|
||||||
<button
|
<div className="infio-chat-history-actions">
|
||||||
onClick={(e) => {
|
<button
|
||||||
e.stopPropagation()
|
onClick={(e) => {
|
||||||
handleEditConversation(conversation)
|
e.stopPropagation()
|
||||||
}}
|
handleEditConversation(conversation)
|
||||||
className="infio-chat-history-btn"
|
}}
|
||||||
title={t('chat.history.editTitle')}
|
className="infio-chat-history-btn"
|
||||||
>
|
title={String(t('chat.history.editTitle'))}
|
||||||
<Pencil size={16} />
|
>
|
||||||
</button>
|
<Pencil size={16} />
|
||||||
<button
|
</button>
|
||||||
onClick={(e) => {
|
<button
|
||||||
e.stopPropagation()
|
onClick={(e) => {
|
||||||
handleDeleteConversation(conversation.id)
|
e.stopPropagation()
|
||||||
}}
|
handleDeleteConversation(conversation.id)
|
||||||
className="infio-chat-history-btn infio-chat-history-delete-btn"
|
}}
|
||||||
title={t('chat.history.deleteConversation')}
|
className="infio-chat-history-btn infio-chat-history-delete-btn"
|
||||||
>
|
title={String(t('chat.history.deleteConversation'))}
|
||||||
<Trash2 size={16} />
|
>
|
||||||
</button>
|
<Trash2 size={16} />
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -243,11 +381,113 @@ const ChatHistoryView = ({
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-history-title h2 {
|
.infio-chat-history-title h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-selection-btn {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background-color: var(--background-primary, #ffffff);
|
||||||
|
border: 1px solid var(--background-modifier-border, #e0e0e0);
|
||||||
|
color: var(--text-normal, #333333);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-s, 4px);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-ui-small, 14px);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
min-width: 60px;
|
||||||
|
height: 32px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-selection-btn:hover {
|
||||||
|
background-color: var(--background-modifier-hover, #f5f5f5);
|
||||||
|
border-color: var(--background-modifier-border-hover, #d0d0d0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-selection-btn.active {
|
||||||
|
background-color: var(--interactive-accent, #007acc);
|
||||||
|
color: var(--text-on-accent, #ffffff);
|
||||||
|
border-color: var(--interactive-accent, #007acc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-batch-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px;
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-select-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-select-all-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 1px solid var(--background-modifier-border);
|
||||||
|
color: var(--text-normal);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-select-all-btn:hover {
|
||||||
|
background-color: var(--background-modifier-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-batch-delete {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-batch-delete-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background-color: var(--background-modifier-error);
|
||||||
|
border: 1px solid var(--background-modifier-error);
|
||||||
|
color: var(--text-error);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--radius-s);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-ui-small);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-batch-delete-btn:hover:not(:disabled) {
|
||||||
|
background-color: var(--background-modifier-error-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-batch-delete-btn:disabled {
|
||||||
|
background-color: var(--background-modifier-form-field);
|
||||||
|
color: var(--text-faint);
|
||||||
|
border-color: var(--background-modifier-border);
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infio-chat-history-tip {
|
.infio-chat-history-tip {
|
||||||
@ -353,6 +593,11 @@ const ChatHistoryView = ({
|
|||||||
border-color: var(--text-accent);
|
border-color: var(--text-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-item.selected {
|
||||||
|
background-color: var(--background-modifier-active);
|
||||||
|
border-color: var(--interactive-accent);
|
||||||
|
}
|
||||||
|
|
||||||
.infio-chat-history-view-mode {
|
.infio-chat-history-view-mode {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -361,6 +606,20 @@ const ChatHistoryView = ({
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-checkbox {
|
||||||
|
margin-right: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-checkbox-checked {
|
||||||
|
color: var(--interactive-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.infio-chat-history-checkbox-unchecked {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.infio-chat-history-content {
|
.infio-chat-history-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user