feat: update ChatHistoryPanel with improved state management and UI enhancements
This commit is contained in:
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -8,8 +8,8 @@
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn"
|
||||
/>
|
||||
<script type="module" crossorigin src="./assets/index-or3WMx_v.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-BMu0CQ47.css">
|
||||
<script type="module" crossorigin src="./assets/index-DMzG5HGc.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-CkGjuuc2.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { ChatHistoryBucket } from './types';
|
||||
import {
|
||||
ChevronDown,
|
||||
LoaderCircle,
|
||||
Plus,
|
||||
MoreHorizontal,
|
||||
PanelLeftClose,
|
||||
PanelLeftOpen,
|
||||
PencilLine,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
|
||||
import blueLogo from '../../assets/images/login/blue_logo.png';
|
||||
|
||||
type ChatHistoryPanelProps = {
|
||||
buckets: ChatHistoryBucket[];
|
||||
@@ -10,6 +23,14 @@ type ChatHistoryPanelProps = {
|
||||
onDeleteConversation?: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
type MenuState = {
|
||||
conversationId: string;
|
||||
} | null;
|
||||
|
||||
function cx(...classes: Array<string | false | null | undefined>): string {
|
||||
return classes.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
export default function ChatHistoryPanel({
|
||||
buckets,
|
||||
selectedConversationId,
|
||||
@@ -19,88 +40,265 @@ export default function ChatHistoryPanel({
|
||||
onRenameConversation,
|
||||
onDeleteConversation,
|
||||
}: ChatHistoryPanelProps) {
|
||||
const panelRef = useRef<HTMLElement | null>(null);
|
||||
const [collapsedBuckets, setCollapsedBuckets] = useState<Record<string, boolean>>({});
|
||||
const [menuState, setMenuState] = useState<MenuState>(null);
|
||||
const [isCompact, setIsCompact] = useState(false);
|
||||
|
||||
const hasSessions = buckets.some((bucket) => bucket.sessions.length > 0);
|
||||
|
||||
useEffect(() => {
|
||||
setCollapsedBuckets((current) => {
|
||||
const next: Record<string, boolean> = {};
|
||||
|
||||
for (const bucket of buckets) {
|
||||
next[bucket.key] = current[bucket.key] ?? false;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
}, [buckets]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!menuState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (!target) {
|
||||
setMenuState(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.closest('[data-chat-history-menu="true"]')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!panelRef.current?.contains(target)) {
|
||||
setMenuState(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.closest('[data-chat-history-menu-toggle="true"]')) {
|
||||
setMenuState(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
setMenuState(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', handlePointerDown);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('pointerdown', handlePointerDown);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [menuState]);
|
||||
|
||||
const panelWidthClass = isCompact ? 'md:w-[96px] lg:w-[96px]' : 'md:w-[240px] lg:w-[252px]';
|
||||
|
||||
return (
|
||||
<aside className="flex h-full min-h-0 w-full flex-none flex-col transition-all duration-300 md:w-[220px] lg:w-[230px]">
|
||||
<div className="flex h-full min-h-0 flex-col rounded-[20px] bg-white p-2 shadow-[0_10px_30px_rgba(15,23,42,0.06)] dark:bg-[#1b1b1d]">
|
||||
<div className="flex items-center gap-3 px-2 py-1.5">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#eff6ff] text-sm font-bold text-[#2B7FFF] dark:bg-[#222225]">
|
||||
YN
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-semibold text-[#171717] dark:text-gray-100">YINIAN</div>
|
||||
<div className="truncate text-xs text-[#99A0AE] dark:text-gray-500">对话历史</div>
|
||||
</div>
|
||||
<aside
|
||||
ref={panelRef}
|
||||
className={cx(
|
||||
'flex h-full min-h-0 w-full flex-none flex-col transition-[width] duration-300',
|
||||
panelWidthClass,
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full min-h-0 flex-col bg-[#fbfcfe] px-3 py-3 dark:border-[#2a2a2d] dark:bg-[#1b1b1d]">
|
||||
<div className={cx('flex items-center justify-between gap-3', isCompact && 'flex-col items-center')}>
|
||||
|
||||
{!isCompact ? (
|
||||
<div className={cx('flex min-w-0 items-center gap-3', isCompact && 'flex-col gap-2')}>
|
||||
<div className="flex h-12 w-12 flex-none items-center justify-center overflow-hidden rounded-[16px] border border-white bg-white shadow-[0_6px_16px_rgba(15,23,42,0.08)]">
|
||||
<img className="h-full w-full object-cover" src={blueLogo} alt="YINIAN" />
|
||||
</div>
|
||||
<div className="truncate text-[20px] font-semibold tracking-[0.06em] text-[#111827] dark:text-gray-50">
|
||||
YINIAN
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex h-8 w-8 items-center justify-center text-[#64748b] transition-colors hover:border-[#cfd8e3] hover:text-[#111827] dark:border-[#2a2a2d] dark:text-gray-300 dark:hover:border-[#3a3a3f] dark:hover:text-gray-100"
|
||||
title={isCompact ? '展开侧栏' : '收起侧栏'}
|
||||
onClick={() => {
|
||||
setMenuState(null);
|
||||
setIsCompact((current) => !current);
|
||||
}}
|
||||
>
|
||||
{isCompact ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="mt-2 flex items-center justify-center gap-2 rounded-lg border border-[#E5E8EE] bg-white px-3 py-2.5 text-sm text-[#171717] shadow-sm transition-colors hover:border-[#2B7FFF] hover:text-[#2B7FFF] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-100 dark:hover:border-[#2B7FFF]"
|
||||
className={cx(
|
||||
'mt-4 inline-flex h-12 items-center justify-center gap-2 rounded-[14px] border border-[#e3eaf3] bg-white px-4 text-[15px] font-medium text-[#111827] shadow-[0_4px_14px_rgba(15,23,42,0.05)] transition-all hover:-translate-y-px hover:border-[#d5e3f4] hover:shadow-[0_10px_24px_rgba(15,23,42,0.08)] dark:border-[#2a2a2d] dark:bg-[#202024] dark:text-gray-100 dark:hover:border-[#3a3a3f]',
|
||||
isCompact && 'px-0',
|
||||
)}
|
||||
title="新对话"
|
||||
onClick={onNewChat}
|
||||
>
|
||||
<span className="text-lg leading-none">+</span>
|
||||
<span>新对话</span>
|
||||
<Plus className="h-5 w-5 flex-none" />
|
||||
{!isCompact ? <span>新对话</span> : null}
|
||||
</button>
|
||||
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-1.5 py-3">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto pt-4">
|
||||
{loading ? (
|
||||
<div className="rounded-lg border border-dashed border-[#dfeaf6] bg-[#f8fbff] px-4 py-6 text-sm text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
|
||||
<div className="flex flex-col items-center justify-center gap-3 rounded-[18px] border border-dashed border-[#dbe7f4] bg-white px-4 py-8 text-sm text-[#94a3b8] shadow-[0_4px_14px_rgba(15,23,42,0.04)] dark:border-[#2a2a2d] dark:bg-[#202024] dark:text-gray-400">
|
||||
<LoaderCircle className="h-5 w-5 animate-spin text-[#8bb7ff]" />
|
||||
正在加载会话历史...
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!loading && !hasSessions ? (
|
||||
<div className="rounded-lg border border-dashed border-[#dfeaf6] bg-[#f8fbff] px-4 py-6 text-sm text-[#99A0AE] dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-400">
|
||||
还没有对话,点击上方“新对话”开始。
|
||||
<div className="rounded-[18px] border border-dashed border-[#dbe7f4] bg-white px-4 py-8 text-sm text-[#94a3b8] shadow-[0_4px_14px_rgba(15,23,42,0.04)] dark:border-[#2a2a2d] dark:bg-[#202024] dark:text-gray-400">
|
||||
还没有对话,点击“新对话”开始。
|
||||
</div>
|
||||
) : null}
|
||||
{buckets.map((bucket) => (
|
||||
<div key={bucket.key} className="mb-3 last:mb-0">
|
||||
<div className="px-2 pb-1 text-[11px] font-medium tracking-tight text-gray-400">{bucket.label}</div>
|
||||
<ul className="list-none space-y-2">
|
||||
{bucket.sessions.map((session) => {
|
||||
const isActive = session.conversationId === selectedConversationId;
|
||||
|
||||
return (
|
||||
<li key={session.conversationId}>
|
||||
<button
|
||||
type="button"
|
||||
className={[
|
||||
'flex w-full items-center gap-2 rounded-lg px-2 py-2 text-left text-sm transition-colors',
|
||||
isActive
|
||||
? 'border border-[#E5E8EE] bg-white text-[#171717] shadow-sm dark:border-[#2a2a2d] dark:bg-[#1f1f22] dark:text-gray-100'
|
||||
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700/50',
|
||||
].join(' ')}
|
||||
onClick={() => onSelectConversation?.(session.conversationId)}
|
||||
>
|
||||
<span className="h-2 w-2 flex-none rounded-full bg-[#BEDBFF]" />
|
||||
<span className="min-w-0 flex-1 truncate">{session.title}</span>
|
||||
<span className="shrink-0 text-[11px] text-[#99A0AE] dark:text-gray-500">{session.updatedAt}</span>
|
||||
</button>
|
||||
{isActive ? (
|
||||
<div className="mt-1 flex items-center justify-end gap-2 pr-2 text-[11px]">
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#99A0AE] transition-colors hover:text-[#2B7FFF] dark:text-gray-500"
|
||||
onClick={() => onRenameConversation?.(session.conversationId)}
|
||||
>
|
||||
重命名
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="text-[#99A0AE] transition-colors hover:text-[#ef4444] dark:text-gray-500"
|
||||
onClick={() => onDeleteConversation?.(session.conversationId)}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
{!isCompact
|
||||
? buckets.map((bucket) => {
|
||||
const isCollapsed = collapsedBuckets[bucket.key] ?? false;
|
||||
|
||||
return (
|
||||
<section key={bucket.key} className="mb-4 last:mb-0">
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between rounded-[12px] px-1 py-1 text-left transition-colors hover:bg-[#eef4fb] dark:hover:bg-[#222225]"
|
||||
aria-expanded={!isCollapsed}
|
||||
onClick={() => {
|
||||
setMenuState(null);
|
||||
setCollapsedBuckets((current) => ({
|
||||
...current,
|
||||
[bucket.key]: !current[bucket.key],
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[14px] font-medium text-[#94a3b8] dark:text-gray-400">{bucket.label}</span>
|
||||
<span className="text-[11px] text-[#c2cad6] dark:text-gray-600">{bucket.sessions.length}</span>
|
||||
</div>
|
||||
<ChevronDown
|
||||
className={cx(
|
||||
'h-4 w-4 text-[#b2bccb] transition-transform duration-200 dark:text-gray-500',
|
||||
isCollapsed && '-rotate-90',
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{!isCollapsed ? (
|
||||
<ul className="mt-2 space-y-2">
|
||||
{bucket.sessions.map((session) => {
|
||||
const isActive = session.conversationId === selectedConversationId;
|
||||
const isMenuOpen = menuState?.conversationId === session.conversationId;
|
||||
|
||||
return (
|
||||
<li key={session.conversationId}>
|
||||
<div className="group relative">
|
||||
<button
|
||||
type="button"
|
||||
className={cx(
|
||||
'flex w-full items-center gap-2 rounded-[14px] px-3 py-3 pr-11 text-left text-[12px] transition-all',
|
||||
isActive
|
||||
? 'border border-[#e5edf7] bg-white text-[#111827] shadow-[0_4px_14px_rgba(15,23,42,0.06)] dark:border-[#2f3136] dark:bg-[#202024] dark:text-gray-50'
|
||||
: 'border border-transparent text-[#5b6472] hover:border-[#e5edf7] hover:bg-white hover:text-[#111827] hover:shadow-[0_4px_14px_rgba(15,23,42,0.05)] dark:text-gray-400 dark:hover:border-[#2a2a2d] dark:hover:bg-[#202024] dark:hover:text-gray-100',
|
||||
)}
|
||||
onClick={() => {
|
||||
setMenuState(null);
|
||||
onSelectConversation?.(session.conversationId);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cx(
|
||||
'h-2 w-2 flex-none rounded-full transition-colors',
|
||||
isActive ? 'bg-[#9fc4ff]' : 'bg-[#c7dcff]',
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate">{session.title}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
data-chat-history-menu-toggle="true"
|
||||
className={cx(
|
||||
'absolute right-2 top-1/2 inline-flex h-7 w-7 -translate-y-1/2 items-center justify-center rounded-full border border-transparent text-[#9aa5b1] transition-all hover:bg-[#f1f5f9] hover:text-[#111827] dark:hover:bg-[#2a2a2d] dark:hover:text-gray-100',
|
||||
isActive || isMenuOpen
|
||||
? 'opacity-100'
|
||||
: 'pointer-events-none opacity-0 group-hover:pointer-events-auto group-hover:opacity-100',
|
||||
)}
|
||||
title="更多操作"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setMenuState((current) =>
|
||||
current?.conversationId === session.conversationId
|
||||
? null
|
||||
: {
|
||||
conversationId: session.conversationId,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
{isMenuOpen ? (
|
||||
<div
|
||||
data-chat-history-menu="true"
|
||||
className="absolute right-1 top-full z-20 mt-2 w-32 overflow-hidden rounded-[14px] border border-[#e5edf7] bg-white shadow-[0_16px_28px_rgba(15,23,42,0.12)] dark:border-[#2f3136] dark:bg-[#202024]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 px-3 py-2.5 text-left text-sm text-[#111827] transition-colors hover:bg-[#f4f8fc] dark:text-gray-100 dark:hover:bg-[#2a2a2d]"
|
||||
disabled={!onRenameConversation}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setMenuState(null);
|
||||
onRenameConversation?.(session.conversationId);
|
||||
}}
|
||||
>
|
||||
<PencilLine className="h-4 w-4 text-[#94a3b8]" />
|
||||
重命名
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-2 border-t border-[#eef3f9] px-3 py-2.5 text-left text-sm text-[#ef4444] transition-colors hover:bg-[#fff5f5] disabled:cursor-not-allowed disabled:text-[#f5a2a2] dark:border-[#2f3136] dark:hover:bg-[#2a2a2d]"
|
||||
disabled={!onDeleteConversation}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setMenuState(null);
|
||||
onDeleteConversation?.(session.conversationId);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
|
||||
{isCompact && hasSessions ? (
|
||||
<div className="mt-4 rounded-[18px] border border-dashed border-[#dbe7f4] bg-white px-3 py-4 text-center text-xs text-[#94a3b8] shadow-[0_4px_14px_rgba(15,23,42,0.04)] dark:border-[#2a2a2d] dark:bg-[#202024] dark:text-gray-500">
|
||||
已收起历史
|
||||
</div>
|
||||
))}
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user