feat: update ChatHistoryPanel with improved state management and UI enhancements

This commit is contained in:
duanshuwen
2026-04-17 23:36:43 +08:00
parent c93f7ae2c8
commit 33e428cc94
3 changed files with 409 additions and 209 deletions

View File

@@ -282,159 +282,161 @@ export default function HomePage() {
return (
<section className="h-full w-full min-h-0">
<div className="flex h-full w-full min-h-0 flex-col gap-2 md:flex-row">
<ChatHistoryPanel
buckets={historyBuckets}
loading={!chat.initialized}
selectedConversationId={chat.currentSessionKey}
onNewChat={() => {
void chatStore.newSession(selectedAgentId || undefined);
}}
onSelectConversation={(conversationId) => {
chatStore.switchSession(conversationId);
}}
onRenameConversation={(conversationId) => {
const currentLabel = chat.sessionLabels[conversationId]
|| chat.sessions.find((session) => session.key === conversationId)?.displayName
|| '';
const nextLabel = window.prompt('重命名对话', currentLabel);
if (nextLabel) {
chatStore.renameSession(conversationId, nextLabel);
}
}}
onDeleteConversation={(conversationId) => {
const confirmed = window.confirm('确定删除该会话吗?删除后将无法恢复。');
if (confirmed) {
void chatStore.deleteSession(conversationId);
}
}}
/>
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden rounded-[20px] bg-white shadow-[0_10px_30px_rgba(15,23,42,0.08)] dark:bg-[#1b1b1d] md:flex-row">
<ChatHistoryPanel
buckets={historyBuckets}
loading={!chat.initialized}
selectedConversationId={chat.currentSessionKey}
onNewChat={() => {
void chatStore.newSession(selectedAgentId || undefined);
}}
onSelectConversation={(conversationId) => {
chatStore.switchSession(conversationId);
}}
onRenameConversation={(conversationId) => {
const currentLabel = chat.sessionLabels[conversationId]
|| chat.sessions.find((session) => session.key === conversationId)?.displayName
|| '';
const nextLabel = window.prompt('重命名对话', currentLabel);
if (nextLabel) {
chatStore.renameSession(conversationId, nextLabel);
}
}}
onDeleteConversation={(conversationId) => {
const confirmed = window.confirm('确定删除该会话吗?删除后将无法恢复。');
if (confirmed) {
void chatStore.deleteSession(conversationId);
}
}}
/>
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-[20px] bg-white shadow-[0_10px_30px_rgba(15,23,42,0.08)] dark:bg-[#1b1b1d]">
<div className="flex items-center justify-between border-b border-[#edf2f7] px-6 py-4 dark:border-[#2a2a2d]">
<div>
<h2 className="text-base font-semibold text-[#171717] dark:text-gray-100"></h2>
<div className="mt-1 text-xs text-[#99A0AE] dark:text-gray-500">
{chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'}
{currentAgent ? ` · 当前代理:${currentAgent.name}` : ''}
</div>
</div>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 text-xs text-[#525866] dark:text-gray-300">
<span></span>
<select
className="rounded-full border border-[#E5E8EE] bg-white px-3 py-1.5 text-xs text-[#525866] outline-none transition-colors hover:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300"
disabled={agentsState.loading || agentsState.agents.length === 0}
value={selectedAgentId}
onChange={(event) => {
chatStore.selectAgent(event.target.value);
}}
>
{agentsState.agents.map((agent) => (
<option key={agent.id} value={agent.id}>
{agent.name}
</option>
))}
</select>
</label>
<button
type="button"
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
onClick={() => {
void agentsStore.load();
void chatStore.loadSessions();
void chatStore.loadHistory();
}}
>
</button>
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col">
<ChatMessageList loading={chat.loading} messages={visibleMessages} />
<ChatComposer
attachments={attachments}
error={chat.error}
isSending={chat.sending}
value={inputMessage}
onAttach={handleAttach}
onChange={setInputMessage}
onDismissError={() => chatStore.clearError()}
onRemoveAttachment={(index) => {
setAttachments((currentAttachments) => currentAttachments.filter((_, currentIndex) => currentIndex !== index));
}}
onSend={() => {
void handleSendMessage();
}}
onStop={() => {
void chatStore.abortRun();
}}
/>
</div>
<div className="border-t border-[#edf2f7] dark:border-[#2a2a2d]">
<div className="px-4 py-3">
<div className="flex items-center justify-between pb-3">
<h3 className="text-base font-semibold text-[#171717] dark:text-gray-100"></h3>
<div className="text-xs text-[#99A0AE] dark:text-gray-500">沿</div>
</div>
{taskCenterNotice ? (
<div className="mb-3 rounded-[12px] border border-[#dfeaf6] bg-[#f8fbff] px-4 py-3 text-sm text-[#525866] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300">
{taskCenterNotice}
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
<div className="flex items-center justify-between border-b border-[#edf2f7] px-6 py-4 dark:border-[#2a2a2d]">
<div>
<h2 className="text-base font-semibold text-[#171717] dark:text-gray-100"></h2>
<div className="mt-1 text-xs text-[#99A0AE] dark:text-gray-500">
{chat.gatewayStatus === 'connected' ? '已连接' : chat.gatewayStatus === 'reconnecting' ? '重连中' : '未连接'}
{currentAgent ? ` · 当前代理:${currentAgent.name}` : ''}
</div>
) : null}
<div className="grid grid-cols-2 gap-4 max-[800px]:grid-cols-1">
{taskCenterList.map((item) => (
<button
key={item.id}
type="button"
className="relative flex items-start gap-3 rounded-[10px] border border-[#dfeaf6] bg-white p-3.5 text-left transition-colors hover:bg-[#F5F7FA] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:hover:bg-[#2a2a2d]"
onClick={() => {
void handleTaskCenterItem(item);
</div>
<div className="flex items-center gap-3">
<label className="flex items-center gap-2 text-xs text-[#525866] dark:text-gray-300">
<span></span>
<select
className="rounded-full border border-[#E5E8EE] bg-white px-3 py-1.5 text-xs text-[#525866] outline-none transition-colors hover:border-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300"
disabled={agentsState.loading || agentsState.agents.length === 0}
value={selectedAgentId}
onChange={(event) => {
chatStore.selectAgent(event.target.value);
}}
>
{item.type === 'channel' ? (
<span
role="button"
tabIndex={0}
className="absolute right-2 top-2 rounded-full p-1 text-[#99A0AE] transition-colors hover:text-[#525866] dark:hover:text-gray-200"
onClick={(event) => {
event.stopPropagation();
setTaskCenterNotice(null);
void channelStore.refreshAvailableChannels();
setAddChannelDialogOpen(true);
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
{agentsState.agents.map((agent) => (
<option key={agent.id} value={agent.id}>
{agent.name}
</option>
))}
</select>
</label>
<button
type="button"
className="rounded-full border border-[#E5E8EE] px-3 py-1.5 text-xs text-[#525866] transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:text-gray-300"
onClick={() => {
void agentsStore.load();
void chatStore.loadSessions();
void chatStore.loadHistory();
}}
>
</button>
</div>
</div>
<div className="flex min-h-0 flex-1 flex-col">
<ChatMessageList loading={chat.loading} messages={visibleMessages} />
<ChatComposer
attachments={attachments}
error={chat.error}
isSending={chat.sending}
value={inputMessage}
onAttach={handleAttach}
onChange={setInputMessage}
onDismissError={() => chatStore.clearError()}
onRemoveAttachment={(index) => {
setAttachments((currentAttachments) => currentAttachments.filter((_, currentIndex) => currentIndex !== index));
}}
onSend={() => {
void handleSendMessage();
}}
onStop={() => {
void chatStore.abortRun();
}}
/>
</div>
<div className="border-t border-[#edf2f7] dark:border-[#2a2a2d]">
<div className="px-4 py-3">
<div className="flex items-center justify-between pb-3">
<h3 className="text-base font-semibold text-[#171717] dark:text-gray-100"></h3>
<div className="text-xs text-[#99A0AE] dark:text-gray-500">沿</div>
</div>
{taskCenterNotice ? (
<div className="mb-3 rounded-xl border border-[#dfeaf6] bg-[#f8fbff] px-4 py-3 text-sm text-[#525866] dark:border-[#2a2a2d] dark:bg-[#232327] dark:text-gray-300">
{taskCenterNotice}
</div>
) : null}
<div className="grid grid-cols-2 gap-4 max-[800px]:grid-cols-1">
{taskCenterList.map((item) => (
<button
key={item.id}
type="button"
className="relative flex items-start gap-3 rounded-[10px] border border-[#dfeaf6] bg-white p-3.5 text-left transition-colors hover:bg-[#F5F7FA] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:hover:bg-[#2a2a2d]"
onClick={() => {
void handleTaskCenterItem(item);
}}
>
{item.type === 'channel' ? (
<span
role="button"
tabIndex={0}
className="absolute right-2 top-2 rounded-full p-1 text-[#99A0AE] transition-colors hover:text-[#525866] dark:hover:text-gray-200"
onClick={(event) => {
event.stopPropagation();
setTaskCenterNotice(null);
void channelStore.refreshAvailableChannels();
setAddChannelDialogOpen(true);
}
}}
aria-label="配置渠道"
>
<svg viewBox="0 0 24 24" className="h-4 w-4 fill-none stroke-current" strokeWidth="1.8">
<path
d="M12 3V6M12 18V21M4.93 4.93L7.05 7.05M16.95 16.95L19.07 19.07M3 12H6M18 12H21M4.93 19.07L7.05 16.95M16.95 7.05L19.07 4.93M15 12A3 3 0 1 1 9 12A3 3 0 0 1 15 12Z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
) : null}
<div className="flex h-11 w-11 items-center justify-center rounded-lg border border-dashed border-[#9fc0e8] bg-[#EFF6FF] text-[23px] text-[#3b82f6] dark:border-gray-700 dark:bg-[#222225]">
{item.icon}
</div>
<div>
<div className="font-semibold text-[#171717] dark:text-gray-100">{item.title}</div>
<div className="mt-1.5 text-[13px] text-[#9aa5b1] dark:text-gray-400">{item.desc}</div>
</div>
</button>
))}
}}
onKeyDown={(event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.stopPropagation();
setTaskCenterNotice(null);
void channelStore.refreshAvailableChannels();
setAddChannelDialogOpen(true);
}
}}
aria-label="配置渠道"
>
<svg viewBox="0 0 24 24" className="h-4 w-4 fill-none stroke-current" strokeWidth="1.8">
<path
d="M12 3V6M12 18V21M4.93 4.93L7.05 7.05M16.95 16.95L19.07 19.07M3 12H6M18 12H21M4.93 19.07L7.05 16.95M16.95 7.05L19.07 4.93M15 12A3 3 0 1 1 9 12A3 3 0 0 1 15 12Z"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</span>
) : null}
<div className="flex h-11 w-11 items-center justify-center rounded-lg border border-dashed border-[#9fc0e8] bg-[#EFF6FF] text-[23px] text-[#3b82f6] dark:border-gray-700 dark:bg-[#222225]">
{item.icon}
</div>
<div>
<div className="font-semibold text-[#171717] dark:text-gray-100">{item.title}</div>
<div className="mt-1.5 text-[13px] text-[#9aa5b1] dark:text-gray-400">{item.desc}</div>
</div>
</button>
))}
</div>
</div>
</div>
</div>