update 优化将自行实现的ai聊天室页面改为内嵌 snail-ai 自带的聊天室页面

This commit is contained in:
疯狂的狮子Li 2026-07-09 13:22:42 +08:00
parent 497a317e90
commit 830eacbf02
4 changed files with 69 additions and 900 deletions

View File

@ -1,62 +1,6 @@
import type { PageResult, R } from '@/api/types';
import request, { globalHeaders } from '@/api/request';
import { useAppStore } from '@/stores/appStore';
import { appEnv } from '@/utils/env';
import type {
AgentChatRequest,
AgentChatSyncResponse,
AgentItem,
ConversationMessage,
ConversationSummaryItem,
SnailOpenApiUser
} from './types';
export function fetchMyAgents() {
return request<R<AgentItem[] | PageResult<AgentItem>>>({
url: '/snail-ai/agents',
method: 'get'
});
}
export function fetchAgentDetail(id: number) {
return request<R<AgentItem>>({
url: `/snail-ai/agent/${id}`,
method: 'get'
});
}
export function fetchAgentConversations(
id: number,
params: { page?: number; size?: number; start?: string; end?: string }
) {
return request<R<PageResult<ConversationSummaryItem> | ConversationSummaryItem[]>>({
url: `/snail-ai/agent/${id}/conversations`,
method: 'get',
params
});
}
export function fetchConversationMessages(agentId: number, conversationId: string) {
return request<R<ConversationMessage[] | PageResult<ConversationMessage>>>({
url: `/snail-ai/agent/${agentId}/conversation/${conversationId}/messages`,
method: 'get'
});
}
export function createConversation(agentId: number, data: { title?: string }) {
return request<R<ConversationSummaryItem>>({
url: `/snail-ai/agent/${agentId}/conversation`,
method: 'post',
data
});
}
export function deleteConversation(agentId: number, conversationId: string) {
return request<R>({
url: `/snail-ai/agent/${agentId}/conversation/${conversationId}`,
method: 'delete'
});
}
import type { R } from '@/api/types';
import request from '@/api/request';
import type { SnailOpenApiUser } from './types';
export function registerCurrentSnailUser() {
return request<R<SnailOpenApiUser>>({
@ -64,87 +8,3 @@ export function registerCurrentSnailUser() {
method: 'post'
});
}
export function fetchChatMode() {
return request<R<{ mode?: 'stream' | 'sync' }>>({
url: '/snail-ai/chat/mode',
method: 'get'
});
}
export async function fetchAgentChat(
agentId: number,
data: AgentChatRequest,
options: {
onMessage: (chunk: string) => void;
onThinking?: (chunk: string) => void;
onDone: () => void;
onError: (error: Error) => void;
signal?: AbortSignal;
}
) {
const baseURL = appEnv.baseApi;
const language = useAppStore.getState().appLocale;
try {
const response = await fetch(`${baseURL}/snail-ai/agent/${agentId}/chat/stream`, {
method: 'POST',
headers: {
...globalHeaders(),
'Content-Language': language,
Accept: 'text/event-stream',
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(data),
signal: options.signal
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || `HTTP ${response.status}`);
}
const reader = response.body?.getReader();
if (!reader) throw new Error('ReadableStream not supported');
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const eventBlocks = buffer.split(/\r?\n\r?\n/);
buffer = eventBlocks.pop() || '';
for (const block of eventBlocks) {
if (!block.trim()) continue;
let eventName = 'message';
let payload = '';
for (const line of block.split(/\r?\n/)) {
if (line.startsWith('event:')) eventName = line.slice(6).trim();
if (line.startsWith('data:')) payload += line.slice(5).trim();
}
if (!payload && eventName !== 'done') continue;
if (eventName === 'thinking') {
options.onThinking?.(payload);
} else if (eventName === 'done') {
options.onDone();
return;
} else if (eventName === 'error') {
throw new Error(payload || 'SSE stream error');
} else {
options.onMessage(payload);
}
}
}
options.onDone();
} catch (error) {
if ((error as Error).name !== 'AbortError') options.onError(error as Error);
}
}
export function fetchAgentChatSync(agentId: number, data: AgentChatRequest) {
return request<R<AgentChatSyncResponse>>({
url: `/snail-ai/agent/${agentId}/chat/sync`,
method: 'post',
data
});
}

View File

@ -1,58 +1,3 @@
export interface AgentItem {
id: number;
name: string;
description?: string;
avatar?: string;
greeting?: string;
status?: number;
presetQuestions?: string[];
}
export interface ConversationSummaryItem {
conversationId: string;
agentId?: number;
title: string;
lastMessageDt?: string;
createDt?: string;
updateDt?: string;
}
export interface ConversationSummaryList {
total: number;
rows: ConversationSummaryItem[];
}
export interface ConversationMessage {
role?: string;
content?: string;
thinking?: string;
message?: string;
text?: string;
messageType?: string;
senderType?: string;
}
export interface AgentChatRequest {
conversationId?: string;
content: string;
disabledMcpServerIds?: number[];
disabledSkillIds?: number[];
}
export interface AgentChatSyncResponse {
conversationId?: string;
content?: string;
traceId?: string;
durationMs?: number;
text?: string;
message?: string;
answer?: string;
reply?: string;
outputText?: string;
result?: string;
messages?: ConversationMessage[];
}
export interface SnailOpenApiUser {
openId: string;
nickname?: string;

View File

@ -1,6 +1,5 @@
.ai-chat-page {
display: grid;
grid-template-rows: 38px minmax(0, 1fr);
.ai-chat-page {
position: relative;
height: calc(100dvh - 150px);
min-height: 0;
overflow: hidden;
@ -17,202 +16,28 @@
padding-block-end: 0 !important;
}
.ai-chat-header {
display: flex;
align-items: center;
padding: 0 12px;
background: var(--app-surface-react);
border-bottom: 1px solid var(--app-border-subtle-react);
}
.ai-chat-brand {
display: flex;
gap: 8px;
align-items: center;
font-size: 16px;
font-weight: 600;
}
.ai-chat-brand-dot,
.ai-chat-agent-dot {
width: 12px;
height: 12px;
background: var(--app-link-react);
border-radius: 3px;
}
.ai-chat-body {
display: grid;
grid-template-columns: 292px minmax(0, 1fr);
min-height: 0;
overflow: hidden;
}
.ai-chat-sidebar {
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
min-height: 0;
background: var(--app-surface-muted-react);
border-right: 1px solid var(--app-border-subtle-react);
}
.ai-chat-sidebar > .ant-spin-nested-loading {
min-height: 0;
}
.ai-chat-sidebar > .ant-spin-nested-loading > .ant-spin-container {
height: 100%;
min-height: 0;
}
.ai-chat-side-scroll {
height: 100%;
min-height: 0;
overflow: auto;
}
.ai-chat-side-block {
padding: 10px 12px;
}
.ai-chat-block-title {
margin-bottom: 8px;
color: var(--app-text-muted-react);
font-size: 12px;
}
.ai-chat-side-item {
display: flex;
gap: 8px;
align-items: center;
.ai-chat-frame {
display: block;
width: 100%;
height: 34px;
padding: 0 10px;
color: var(--app-text-react);
background: transparent;
height: 100%;
border: 0;
border-radius: 8px;
cursor: pointer;
}
.ai-chat-side-item:hover,
.ai-chat-side-item.active {
color: var(--app-link-react);
background: var(--app-link-soft-react);
}
.ai-chat-side-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.ai-chat-delete {
color: var(--app-danger-react);
opacity: 0;
}
.ai-chat-side-item:hover .ai-chat-delete,
.ai-chat-side-item.active .ai-chat-delete {
opacity: 1;
}
.ai-chat-user {
display: flex;
gap: 10px;
align-items: center;
padding: 10px 12px;
background: var(--app-surface-react);
border-top: 1px solid var(--app-border-subtle-react);
}
.ai-chat-user-name {
font-weight: 600;
}
.ai-chat-user-status {
color: var(--app-text-muted-react);
font-size: 12px;
}
.ai-chat-main {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
min-width: 0;
min-height: 0;
}
.ai-chat-empty {
display: flex;
min-height: 0;
height: 100%;
align-items: center;
justify-content: center;
color: var(--app-text-muted-react);
}
.ai-chat-scroll {
min-height: 0;
padding: 12px 14px 0;
overflow: auto;
}
.ai-chat-content {
width: 100%;
max-width: 920px;
margin: 0 auto;
}
.ai-chat-welcome {
padding: 14px;
margin-bottom: 12px;
background: var(--app-surface-react);
border: 1px solid var(--app-border-subtle-react);
border-radius: 8px;
}
.ai-chat-message-row {
display: flex !important;
justify-content: flex-start;
padding: 4px 0 !important;
border-block-end: 0 !important;
}
.ai-chat-message-row.user {
justify-content: flex-end;
}
.ai-chat-message {
max-width: 80%;
padding: 8px 10px;
line-height: 1.6;
white-space: pre-wrap;
background: var(--app-surface-react);
border: 1px solid var(--app-border-subtle-react);
border-radius: 8px;
}
.ai-chat-message-row.user .ai-chat-message {
color: var(--app-link-strong-react);
background: var(--app-link-soft-react);
border-color: var(--app-link-border-react);
}
.ai-chat-message.pending {
color: var(--app-text-muted-react);
}
.ai-chat-input-wrap {
flex: 0 0 auto;
padding: 8px 14px 10px;
background: var(--app-bg-react);
border-top: 1px solid var(--app-border-subtle-react);
}
.ai-chat-sender {
max-width: 920px;
margin: 0 auto;
background: var(--app-surface-react);
}
.ai-chat-loading {
position: absolute;
inset: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, var(--app-surface-react) 76%, transparent);
}

View File

@ -1,532 +1,71 @@
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { ReloadOutlined } from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components';
import { Bubble, Conversations, Prompts, Sender, type BubbleItemType } from '@ant-design/x';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { Avatar, Button, Empty, message, Space, Spin, Typography } from 'antd';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type {
AgentChatSyncResponse,
AgentItem,
ConversationMessage,
ConversationSummaryItem
} from '@/api/ai/agent/types';
import {
createConversation,
deleteConversation,
fetchAgentChat,
fetchAgentChatSync,
fetchAgentConversations,
fetchChatMode,
fetchConversationMessages,
fetchMyAgents,
registerCurrentSnailUser
} from '@/api/ai/agent';
import { Button, Empty, Spin } from 'antd';
import { useCallback, useEffect, useState } from 'react';
import { registerCurrentSnailUser } from '@/api/ai/agent';
import { getToken } from '@/utils/auth';
import { appEnv } from '@/utils/env';
interface ChatMessage {
role: 'user' | 'assistant';
content: string;
}
const aiChatKeys = {
bootstrap: ['ai-chat', 'bootstrap'] as const,
conversations: (agentId?: number) => ['ai-chat', 'conversations', agentId] as const,
messages: (agentId?: number, conversationId?: string) => ['ai-chat', 'messages', agentId, conversationId] as const
};
function getList<T>(payload: unknown): T[] {
const container = (payload as { data?: unknown })?.data ?? payload;
if (Array.isArray(container)) return container as T[];
const record = container as { rows?: T[]; list?: T[]; records?: T[] };
if (Array.isArray(record?.rows)) return record.rows;
if (Array.isArray(record?.list)) return record.list;
if (Array.isArray(record?.records)) return record.records;
return [];
}
function normalizeAgentList(payload: unknown): AgentItem[] {
return getList<Record<string, unknown>>(payload)
.map(item => ({
id: Number(item.id ?? item.agentId),
name: String(item.name ?? item.title ?? ''),
description: item.description as string | undefined,
avatar: item.avatar as string | undefined,
greeting: item.greeting as string | undefined,
status: item.status as number | undefined,
presetQuestions: Array.isArray(item.presetQuestions) ? item.presetQuestions.map(String) : []
}))
.filter(item => Number.isFinite(item.id) && !!item.name);
}
function normalizeConversationList(payload: unknown): ConversationSummaryItem[] {
return getList<Record<string, unknown>>(payload)
.map(item => ({
conversationId: String(item.conversationId ?? item.id ?? ''),
title: String(item.title ?? item.name ?? ''),
lastMessageDt: item.lastMessageDt as string | undefined,
createDt: (item.createDt ?? item.createTime) as string | undefined,
updateDt: (item.updateDt ?? item.updateTime) as string | undefined
}))
.filter(item => !!item.conversationId)
.sort(
(a, b) =>
new Date(b.lastMessageDt || b.createDt || 0).getTime() - new Date(a.lastMessageDt || a.createDt || 0).getTime()
);
}
function normalizeMessageList(payload: unknown): ChatMessage[] {
return getList<ConversationMessage>(payload)
.map(item => {
const role = String(item.role || item.messageType || item.senderType || '').toLowerCase();
return {
role: role === 'user' ? 'user' : 'assistant',
content: String(item.content ?? item.message ?? item.text ?? '')
} as ChatMessage;
})
.filter(item => !!item.content);
}
function normalizeStreamChunk(chunk: string) {
const text = String(chunk || '');
if (!text.trim()) return '';
const tryParse = (raw: string) => {
try {
const obj = JSON.parse(raw) as { content?: string };
return typeof obj.content === 'string' ? obj.content : null;
} catch {
return null;
}
};
const single = tryParse(text);
if (single !== null) return single;
const lines = text
.replace(/}\s*{/g, '}\n{')
.split('\n')
.map(line => line.trim())
.filter(Boolean);
if (!lines.length) return text;
let merged = '';
for (const line of lines) {
const parsed = tryParse(line);
if (parsed === null) return text;
merged += parsed;
}
return merged;
}
function extractSyncReply(payload: AgentChatSyncResponse) {
const candidates = [
payload.content,
payload.text,
payload.message,
payload.answer,
payload.reply,
payload.outputText,
payload.result
];
const hit = candidates.find(item => typeof item === 'string' && item.trim());
if (hit) return normalizeStreamChunk(String(hit)) || String(hit);
if (Array.isArray(payload.messages)) {
return (
normalizeMessageList(payload.messages)
.filter(item => item.role === 'assistant')
.at(-1)?.content || ''
);
}
return '';
function buildChatUrl(openId: string, trustedCredential: string) {
const params = new URLSearchParams({ openId, trustedCredential });
return `${appEnv.baseApi}/snail-chat/?${params.toString()}`;
}
export default function AiChatPage() {
const queryClient = useQueryClient();
const [currentAgentId, setCurrentAgentId] = useState<number>();
const [currentConversationId, setCurrentConversationId] = useState('');
const [messages, setMessages] = useState<ChatMessage[]>([]);
const [content, setContent] = useState('');
const [sending, setSending] = useState(false);
const abortRef = useRef<AbortController | null>(null);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [chatUrl, setChatUrl] = useState('');
const [loadError, setLoadError] = useState('');
const [loading, setLoading] = useState(false);
const bootstrapQuery = useQuery({
queryKey: aiChatKeys.bootstrap,
queryFn: async () => {
const [userRes, agentsRes, modeRes] = await Promise.all([
registerCurrentSnailUser(),
fetchMyAgents(),
fetchChatMode()
]);
return {
nickname: userRes.data?.nickname || '',
agents: normalizeAgentList(agentsRes.data),
sendMode: modeRes.data?.mode === 'sync' ? ('sync' as const) : ('stream' as const)
};
}
});
const loadChat = useCallback(async () => {
setLoading(true);
setLoadError('');
try {
const token = getToken();
if (!token) {
setChatUrl('');
setLoadError('登录凭证不存在,请重新登录后再试');
return;
}
const agents = bootstrapQuery.data?.agents || [];
const currentAgent = useMemo(
() => agents.find(agent => agent.id === currentAgentId) || agents[0] || null,
[agents, currentAgentId]
);
const sendMode = bootstrapQuery.data?.sendMode || 'stream';
const nickname = bootstrapQuery.data?.nickname || '';
const res = await registerCurrentSnailUser();
if (!res.data?.openId) {
setChatUrl('');
setLoadError('获取 AI 用户身份失败');
return;
}
const conversationsQuery = useQuery({
queryKey: aiChatKeys.conversations(currentAgent?.id),
enabled: !!currentAgent?.id,
queryFn: async () => {
if (!currentAgent?.id) return [];
const res = await fetchAgentConversations(currentAgent.id, { page: 1, size: 50 });
return normalizeConversationList(res.data);
}
});
const conversationMessagesQuery = useQuery({
queryKey: aiChatKeys.messages(currentAgent?.id, currentConversationId),
enabled: !!currentAgent?.id && !!currentConversationId,
queryFn: async () => {
if (!currentAgent?.id) return [];
const res = await fetchConversationMessages(currentAgent.id, currentConversationId);
return normalizeMessageList(res.data);
}
});
const presetQuestions = useMemo(() => (currentAgent?.presetQuestions || []).filter(Boolean), [currentAgent]);
const clearTimer = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
setChatUrl(buildChatUrl(res.data.openId, token));
} catch {
setChatUrl('');
setLoadError('加载 AI 聊天失败,请稍后重试');
} finally {
setLoading(false);
}
}, []);
const finishSending = useCallback(() => {
setSending(false);
clearTimer();
abortRef.current = null;
}, [clearTimer]);
useEffect(() => {
if (!currentAgentId && agents[0]) {
setCurrentAgentId(agents[0].id);
}
}, [agents, currentAgentId]);
useEffect(() => {
if (currentConversationId && conversationMessagesQuery.data && !sending) {
setMessages(conversationMessagesQuery.data);
}
}, [conversationMessagesQuery.data, currentConversationId, sending]);
useEffect(() => {
return () => {
abortRef.current?.abort();
clearTimer();
};
}, [clearTimer]);
const refreshConversationData = useCallback(
async (agentId: number, conversationId?: string) => {
await queryClient.invalidateQueries({ queryKey: aiChatKeys.conversations(agentId) });
if (conversationId) {
await queryClient.invalidateQueries({ queryKey: aiChatKeys.messages(agentId, conversationId) });
}
},
[queryClient]
);
const selectAgent = (agentId: string | number) => {
const nextAgentId = Number(agentId);
if (currentAgentId === nextAgentId) return;
abortRef.current?.abort();
finishSending();
setCurrentAgentId(nextAgentId);
setCurrentConversationId('');
setMessages([]);
};
const startNewConversation = () => {
abortRef.current?.abort();
finishSending();
setCurrentConversationId('');
setMessages([]);
};
const selectConversation = (conversationId: string | number) => {
const nextConversationId = String(conversationId);
if (currentConversationId === nextConversationId) return;
abortRef.current?.abort();
finishSending();
setCurrentConversationId(nextConversationId);
setMessages([]);
};
const removeConversation = async (conversationId: string) => {
if (!currentAgent) return;
await deleteConversation(currentAgent.id, conversationId);
message.success('删除成功');
if (currentConversationId === conversationId) {
abortRef.current?.abort();
finishSending();
setCurrentConversationId('');
setMessages([]);
}
await refreshConversationData(currentAgent.id);
};
const sendMessage = async (value?: string) => {
const text = (value ?? content).trim();
if (!currentAgent || !text || sending) return;
setContent('');
setSending(true);
clearTimer();
timerRef.current = setTimeout(() => {
abortRef.current?.abort();
finishSending();
message.warning('响应超时,已恢复发送按钮,请重试');
}, 300000);
let targetConversationId = currentConversationId;
if (!targetConversationId) {
try {
const res = await createConversation(currentAgent.id, { title: text.slice(0, 20) });
if (!res.data?.conversationId) {
finishSending();
message.error('创建会话失败,请稍后重试');
return;
}
targetConversationId = res.data.conversationId;
setCurrentConversationId(targetConversationId);
await refreshConversationData(currentAgent.id);
} catch (error) {
finishSending();
message.error((error as Error).message || '创建会话失败,请稍后重试');
return;
}
}
setMessages(items => [...items, { role: 'user', content: text }]);
if (sendMode === 'sync') {
try {
const res = await fetchAgentChatSync(currentAgent.id, { conversationId: targetConversationId, content: text });
setMessages(items => [
...items,
{ role: 'assistant', content: extractSyncReply(res.data) || '(后端已返回空消息)' }
]);
await refreshConversationData(currentAgent.id, targetConversationId);
} catch (error) {
message.error((error as Error).message || '对话失败,请稍后重试');
} finally {
finishSending();
}
return;
}
setMessages(items => [...items, { role: 'assistant', content: '' }]);
abortRef.current?.abort();
abortRef.current = new AbortController();
await fetchAgentChat(
currentAgent.id,
{ conversationId: targetConversationId, content: text },
{
signal: abortRef.current.signal,
onMessage(chunk) {
setMessages(items => {
const next = [...items];
const index = next.length - 1;
next[index] = { ...next[index], content: `${next[index]?.content || ''}${normalizeStreamChunk(chunk)}` };
return next;
});
},
async onDone() {
setMessages(items => {
const next = [...items];
const index = next.length - 1;
if (next[index]?.role === 'assistant' && !next[index].content.trim()) {
next[index] = { ...next[index], content: '(后端已返回空消息)' };
}
return next;
});
finishSending();
await refreshConversationData(currentAgent.id, targetConversationId);
},
onError(error) {
setMessages(items => items.filter((_, index) => index !== items.length - 1));
finishSending();
message.error(error.message || '对话失败,请稍后重试');
}
}
);
};
const agentItems = useMemo(
() =>
agents.map(agent => ({
key: String(agent.id),
label: agent.name,
icon: (
<Avatar size={20} src={agent.avatar}>
{agent.name.slice(0, 1)}
</Avatar>
)
})),
[agents]
);
const conversationItems = useMemo(
() =>
(conversationsQuery.data || []).map(conv => ({
key: conv.conversationId,
label: conv.title || '未命名会话'
})),
[conversationsQuery.data]
);
const bubbleItems = useMemo<BubbleItemType[]>(
() =>
messages.map((item, index) => ({
key: `${item.role}-${index}`,
role: item.role === 'user' ? 'user' : 'ai',
content: item.content || '正在生成...',
loading: item.role === 'assistant' && !item.content
})),
[messages]
);
loadChat();
}, [loadChat]);
return (
<PageContainer className="ai-chat-container" title={false}>
<div className="ai-chat-page">
<header className="ai-chat-header">
<div className="ai-chat-brand">
<span className="ai-chat-brand-dot" />
Snail AI
{loading ? (
<div className="ai-chat-loading">
<Spin />
</div>
</header>
<div className="ai-chat-body">
<aside className="ai-chat-sidebar">
<div className="ai-chat-side-block">
<Button
type="primary"
block
icon={<PlusOutlined />}
disabled={!agents.length}
onClick={startNewConversation}
>
) : null}
{chatUrl ? (
<iframe className="ai-chat-frame" src={chatUrl} title="Snail AI" allow="clipboard-read; clipboard-write" />
) : (
<Empty className="ai-chat-empty" description={loadError || '正在加载 Snail AI'}>
{loadError ? (
<Button type="primary" icon={<ReloadOutlined />} onClick={loadChat}>
</Button>
</div>
<Spin spinning={bootstrapQuery.isLoading || conversationsQuery.isLoading}>
<div className="ai-chat-side-scroll">
<div className="ai-chat-side-block">
<div className="ai-chat-block-title"></div>
{!agents.length ? <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无智能体" /> : null}
<Conversations
className="ai-chat-conversations"
items={agentItems}
activeKey={currentAgent ? String(currentAgent.id) : undefined}
onActiveChange={selectAgent}
/>
</div>
<div className="ai-chat-side-block">
<div className="ai-chat-block-title"></div>
{!conversationItems.length ? (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂无会话" />
) : null}
<Conversations
className="ai-chat-conversations"
items={conversationItems}
activeKey={currentConversationId}
onActiveChange={selectConversation}
menu={item => ({
items: [{ key: 'delete', label: '删除', icon: <DeleteOutlined />, danger: true }],
onClick: ({ domEvent }) => {
domEvent.stopPropagation();
removeConversation(item.key);
}
})}
/>
</div>
</div>
</Spin>
<div className="ai-chat-user">
<Avatar>{(nickname || 'U').slice(0, 1)}</Avatar>
<div>
<div className="ai-chat-user-name">{nickname || '已登录用户'}</div>
<div className="ai-chat-user-status"></div>
</div>
</div>
</aside>
<main className="ai-chat-main">
{!currentAgent ? (
<div className="ai-chat-empty"></div>
) : (
<>
<div className="ai-chat-scroll">
<div className="ai-chat-content">
{!currentConversationId && !messages.length && (
<div className="ai-chat-welcome">
<Space align="start">
<Avatar size={40} src={currentAgent.avatar}>
{currentAgent.name.slice(0, 1)}
</Avatar>
<div>
<Typography.Title level={4} style={{ margin: 0 }}>
{currentAgent.name}
</Typography.Title>
<Typography.Text type="secondary">{currentAgent.description || '暂无描述'}</Typography.Text>
</div>
</Space>
<p>{currentAgent.greeting || '你好,我是你的智能助手。'}</p>
{presetQuestions.length > 0 && (
<Prompts
wrap
items={presetQuestions.map(question => ({
key: question,
label: question,
disabled: sending
}))}
onItemClick={({ data }) => sendMessage(String(data.key))}
/>
)}
</div>
)}
<Spin
spinning={conversationMessagesQuery.isFetching && !!currentConversationId && !messages.length}
>
<Bubble.List
className="ai-chat-bubble-list"
autoScroll
items={bubbleItems}
role={{
user: { placement: 'end', variant: 'filled' },
ai: {
placement: 'start',
variant: 'outlined',
avatar: <Avatar src={currentAgent.avatar}>{currentAgent.name.slice(0, 1)}</Avatar>
}
}}
/>
</Spin>
</div>
</div>
<div className="ai-chat-input-wrap">
<Sender
className="ai-chat-sender"
value={content}
loading={sending}
disabled={sending}
placeholder="给智能体发消息"
autoSize={{ minRows: 1, maxRows: 4 }}
onChange={setContent}
onSubmit={sendMessage}
onCancel={() => {
abortRef.current?.abort();
finishSending();
}}
/>
</div>
</>
)}
</main>
</div>
) : null}
</Empty>
)}
</div>
</PageContainer>
);