From 830eacbf025f6446f2c89e78890c755450e88bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?= <15040126243@163.com> Date: Thu, 9 Jul 2026 13:22:42 +0800 Subject: [PATCH] =?UTF-8?q?update=20=E4=BC=98=E5=8C=96=E5=B0=86=E8=87=AA?= =?UTF-8?q?=E8=A1=8C=E5=AE=9E=E7=8E=B0=E7=9A=84ai=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E5=AE=A4=E9=A1=B5=E9=9D=A2=E6=94=B9=E4=B8=BA=E5=86=85=E5=B5=8C?= =?UTF-8?q?=20snail-ai=20=E8=87=AA=E5=B8=A6=E7=9A=84=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E5=AE=A4=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/ai/agent/index.ts | 146 +------ src/api/ai/agent/types.ts | 55 --- src/assets/styles/modules/ai-chat.less | 207 +-------- src/pages/ai/chat/index.tsx | 561 +++---------------------- 4 files changed, 69 insertions(+), 900 deletions(-) diff --git a/src/api/ai/agent/index.ts b/src/api/ai/agent/index.ts index 04a87fe7..725e361b 100644 --- a/src/api/ai/agent/index.ts +++ b/src/api/ai/agent/index.ts @@ -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>>({ - url: '/snail-ai/agents', - method: 'get' - }); -} - -export function fetchAgentDetail(id: number) { - return request>({ - url: `/snail-ai/agent/${id}`, - method: 'get' - }); -} - -export function fetchAgentConversations( - id: number, - params: { page?: number; size?: number; start?: string; end?: string } -) { - return request | ConversationSummaryItem[]>>({ - url: `/snail-ai/agent/${id}/conversations`, - method: 'get', - params - }); -} - -export function fetchConversationMessages(agentId: number, conversationId: string) { - return request>>({ - url: `/snail-ai/agent/${agentId}/conversation/${conversationId}/messages`, - method: 'get' - }); -} - -export function createConversation(agentId: number, data: { title?: string }) { - return request>({ - url: `/snail-ai/agent/${agentId}/conversation`, - method: 'post', - data - }); -} - -export function deleteConversation(agentId: number, conversationId: string) { - return request({ - 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>({ @@ -64,87 +8,3 @@ export function registerCurrentSnailUser() { method: 'post' }); } - -export function fetchChatMode() { - return request>({ - 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>({ - url: `/snail-ai/agent/${agentId}/chat/sync`, - method: 'post', - data - }); -} diff --git a/src/api/ai/agent/types.ts b/src/api/ai/agent/types.ts index 918e6b12..5999b1c5 100644 --- a/src/api/ai/agent/types.ts +++ b/src/api/ai/agent/types.ts @@ -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; diff --git a/src/assets/styles/modules/ai-chat.less b/src/assets/styles/modules/ai-chat.less index 916c8e3b..56a3975a 100644 --- a/src/assets/styles/modules/ai-chat.less +++ b/src/assets/styles/modules/ai-chat.less @@ -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); +} diff --git a/src/pages/ai/chat/index.tsx b/src/pages/ai/chat/index.tsx index 8893eb5f..8c86b602 100644 --- a/src/pages/ai/chat/index.tsx +++ b/src/pages/ai/chat/index.tsx @@ -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(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>(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>(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(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(); - const [currentConversationId, setCurrentConversationId] = useState(''); - const [messages, setMessages] = useState([]); - const [content, setContent] = useState(''); - const [sending, setSending] = useState(false); - const abortRef = useRef(null); - const timerRef = useRef | 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: ( - - {agent.name.slice(0, 1)} - - ) - })), - [agents] - ); - - const conversationItems = useMemo( - () => - (conversationsQuery.data || []).map(conv => ({ - key: conv.conversationId, - label: conv.title || '未命名会话' - })), - [conversationsQuery.data] - ); - - const bubbleItems = useMemo( - () => - 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 (
-
-
- - Snail AI + {loading ? ( +
+
-
-
-