From d62848ee98edae959babe0deb38e3d5082ea48a3 Mon Sep 17 00:00:00 2001
From: qyyfw
Date: Fri, 10 Jul 2026 15:47:03 +0800
Subject: [PATCH] feat(chat): support AGNES image/video models in AI chat -
backend _generate_media (images/generations + async videos polling) and
frontend MarkdownContent media rendering + model capability badges; nginx
proxy_read_timeout raised to 600s for long media generation
---
backend/python/app/api/chat.py | 113 +-
frontend/src/pages/front/ChatPage.css | 2170 ++++++++++++------------
frontend/src/pages/front/ChatPage.tsx | 2215 +++++++++++++------------
3 files changed, 2370 insertions(+), 2128 deletions(-)
diff --git a/backend/python/app/api/chat.py b/backend/python/app/api/chat.py
index 213746d..236217d 100644
--- a/backend/python/app/api/chat.py
+++ b/backend/python/app/api/chat.py
@@ -12,6 +12,7 @@ import uuid
from datetime import datetime, timezone, timedelta
import re
import json
+import asyncio
import math
import httpx
import os
@@ -40,6 +41,108 @@ def cst_now():
return datetime.now(CST_TZ)
+def _html_escape(s: str) -> str:
+ return (str(s).replace("&", "&").replace("<", "<").replace(">", ">")
+ .replace('"', """).replace("'", "'"))
+
+
+async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str:
+ """图像/视频生成模型统一入口:按 capabilities 调用 AGNES 等媒体生成 API。
+ 返回可直接在 AI 聊天中渲染的富文本(含
/
');
+ text = text.replace(/\n/g, '
');
+
+ // 10. Restore code blocks and inline codes
+ text = text.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m: string, idx: string) => codeBlocks[parseInt(idx)] || '');
+ text = text.replace(/\x00INLINE(\d+)\x00/g, (_m: string, idx: string) => inlineCodes[parseInt(idx)] || '');
+ text = text.replace(/\x00MEDIA(\d+)\x00/g, (_m: string, idx: string) => mediaBlocks[parseInt(idx)] || '');
+
+ // 11. Wrap in paragraph and clean up
+ text = '
' + text + '
';
+ text = text.replace(/<\/p>/g, '');
+ text = text.replace(/
()<\/p>/g, '$1');
+ text = text.replace(/(
)<\/p>/g, '$1');
+ text = text.replace(/(
)<\/p>/g, '$1');
+ text = text.replace(/(
/g, '$1');
+ text = text.replace(/(
)<\/p>/g, '$1');
+
+ return text;
+ }, [content]);
+
+ return ;
+};
+
+
+function escapeHtml(text: string): string {
+ return text
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}
+
+// ===== Time Formatting =====
+function formatTime(iso: string): string {
+ const date = new Date(iso);
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const oneDay = 24 * 60 * 60 * 1000;
+
+ if (diff < oneDay) {
+ return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
+ } else if (diff < 7 * oneDay) {
+ const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
+ return days[date.getDay()];
+ } else if (date.getFullYear() === now.getFullYear()) {
+ return `${date.getMonth() + 1}/${date.getDate()}`;
+ } else {
+ return `${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`;
+ }
+}
+
+// ===== Helpers: normalize API responses =====
+function unwrapList(res: any): any[] {
+ const data = res?.data !== undefined ? res.data : res;
+ if (Array.isArray(data)) return data;
+ if (data?.items) return data.items;
+ return [];
+}
+
+function unwrapData(res: any): any {
+ return res?.data !== undefined ? res.data : res;
+}
+
+// ===== Icons =====
+const IconSearch = () => (
+
+);
+
+const IconPlus = () => (
+
+);
+
+const IconMore = () => (
+
+);
+
+const IconSend = () => (
+
+);
+
+const IconChevronDown = () => (
+
+);
+
+const IconEdit = () => (
+
+);
+
+const IconArchive = () => (
+
+);
+
+const IconTrash = () => (
+
+);
+
+const IconPin = () => (
+
+);
+
+const IconGraph = () => (
+
+);
+
+const IconKnowledge = () => (
+
+);
+
+const IconAgent = () => (
+
+);
+
+const IconSkill = () => (
+
+);
+
+const IconModel = () => (
+
+);
+
+const IconEmpty = () => (
+
+);
+
+const IconSpinner = () => (
+
+);
+
+// ===== Dropdown Component =====
+interface DropdownProps {
+ trigger: React.ReactNode;
+ children: React.ReactNode;
+ className?: string;
+ align?: 'left' | 'right';
+ dropup?: boolean;
+ closeOnClick?: boolean;
+}
+
+const Dropdown: React.FC = ({ trigger, children, className = '', align = 'left', dropup = false, closeOnClick = true }) => {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+ const menuRef = useRef(null);
+ const openRef = useRef(open);
+ openRef.current = open;
+
+ useEffect(() => {
+ const handleClickOutside = (e: MouseEvent) => {
+ const target = e.target as Node;
+ // 如果点击在wrapper内部,不关闭
+ if (ref.current?.contains(target)) return;
+ setOpen(false);
+ };
+ document.addEventListener('mousedown', handleClickOutside, true);
+ return () => document.removeEventListener('mousedown', handleClickOutside, true);
+ }, []);
+
+ // 打开时处理z-index
+ useEffect(() => {
+ if (open && dropup) {
+ const menu = menuRef.current;
+ if (menu) {
+ menu.style.zIndex = '9999';
+ }
+ }
+ }, [open, dropup]);
+
+ const alignClass = align === 'right' ? 'align-right' : 'align-left';
+ const dropupClass = dropup ? 'dropup' : '';
+
+ const handleTriggerMouseDown = (e: React.MouseEvent) => {
+ // 阻止冒泡到document,防止被handleClickOutside误关
+ e.stopPropagation();
+ // 基于当前状态切换(在mousedown阶段决定,避免与click冲突)
+ setOpen(prev => !prev);
+ };
+
+ const handleTriggerClick = (e: React.MouseEvent) => {
+ // 阻止click冒泡到父级的导航onClick
+ e.stopPropagation();
+ e.preventDefault();
+ };
+
+ return (
+
+
+ {trigger}
+
+ {open && (
+
e.stopPropagation()}
+ onClick={(e) => { e.stopPropagation(); if (closeOnClick) setOpen(false); }}
+ >
+ {children}
+
+ )}
+
+ );
+};
+
+// ===== Main Component =====
+const ChatPage: React.FC = () => {
+ const { id } = useParams<{ id?: string }>();
+ const navigate = useNavigate();
+ const [searchQuery, setSearchQuery] = useState('');
+ const [selectedModelId, setSelectedModelId] = useState('');
+ const [chatModels, setChatModels] = useState([]);
+ const [inputValue, setInputValue] = useState('');
+ const [sending, setSending] = useState(false);
+ const [conversations, setConversations] = useState([]);
+ const [messages, setMessages] = useState([]);
+ const [loadingConversations, setLoadingConversations] = useState(true);
+ const [loadingMessages, setLoadingMessages] = useState(false);
+ const [knowledgeBases, setKnowledgeBases] = useState([]);
+ const [agents, setAgents] = useState([]);
+ const [skills, setSkills] = useState([]);
+ // 多选组合模式
+ const [selectedAgentIds, setSelectedAgentIds] = useState([]);
+ const [selectedSkillIds, setSelectedSkillIds] = useState([]);
+ const [selectedKbIds, setSelectedKbIds] = useState([]);
+ const messagesEndRef = useRef(null);
+ const inputRef = useRef(null);
+
+ // Knowledge graph conversion state
+ const [kgModalOpen, setKgModalOpen] = useState(false);
+ const [kgStep, setKgStep] = useState<1 | 2 | 3>(1);
+ const [kgResult, setKgResult] = useState<{ nodes: number; edges: number; kbName: string; kbId: string } | null>(null);
+ const [kgError, setKgError] = useState(null);
+ const [kgConvId, setKgConvId] = useState(null);
+
+ // Load models from backend API
+ useEffect(() => {
+ chatAPI.getModels().then((res: any) => {
+ const data = res.data || res;
+ const models = Array.isArray(data) ? data : (data?.items || []);
+ setChatModels(models);
+ if (models.length > 0 && !selectedModelId) {
+ setSelectedModelId(models[0].id);
+ }
+ }).catch(() => {
+ setChatModels([]);
+ });
+ }, []);
+
+ // Load conversations from API
+ const loadConversations = useCallback(async () => {
+ try {
+ setLoadingConversations(true);
+ const res = await chatAPI.getConversations();
+ const convs = unwrapList(res);
+ // Sort: pinned first, then by updatedAt desc
+ const sorted = [...convs].sort((a: any, b: any) => {
+ if (a.pinned && !b.pinned) return -1;
+ if (!a.pinned && b.pinned) return 1;
+ return new Date(b.updatedAt || b.updated_at || 0).getTime() - new Date(a.updatedAt || a.updated_at || 0).getTime();
+ });
+ // Normalize field names
+ const mapped: Conversation[] = sorted.map((c: any) => ({
+ id: c.id,
+ userId: c.userId || c.user_id || '',
+ title: c.title || '未命名对话',
+ modelId: c.modelId || c.model_id || '',
+ status: c.status || 'active',
+ pinned: c.pinned || false,
+ knowledgeGraphCount: c.knowledgeGraphCount ?? c.knowledge_graph_count ?? 0,
+ lastMessage: c.lastMessage || c.last_message || '',
+ createdAt: c.createdAt || c.created_at || new Date().toISOString(),
+ updatedAt: c.updatedAt || c.updated_at || new Date().toISOString(),
+ }));
+ setConversations(mapped);
+ } catch (err) {
+ console.error('加载对话列表失败', err);
+ } finally {
+ setLoadingConversations(false);
+ }
+ }, []);
+
+ // Load conversations on mount
+ useEffect(() => {
+ loadConversations();
+ }, [loadConversations]);
+
+ // Load messages for selected conversation
+ useEffect(() => {
+ if (!id) {
+ setMessages([]);
+ return;
+ }
+ const loadMessages = async () => {
+ try {
+ setLoadingMessages(true);
+ const res = await chatAPI.getMessages(id);
+ const msgs = unwrapList(res);
+ const mapped: Message[] = msgs.map((m: any) => ({
+ id: m.id,
+ conversationId: m.conversationId || m.conversation_id || id,
+ role: m.role,
+ content: m.content,
+ tokenUsed: m.tokenUsed ?? m.token_used ?? 0,
+ createdAt: m.createdAt || m.created_at || new Date().toISOString(),
+ }));
+ setMessages(mapped);
+ } catch (err) {
+ console.error('加载消息失败', err);
+ setMessages([]);
+ } finally {
+ setLoadingMessages(false);
+ }
+ };
+ loadMessages();
+ }, [id]);
+
+ // Load knowledge bases, agents, skills for attachment dropdowns
+ useEffect(() => {
+ knowledgeAPI.getBases().then((res: any) => {
+ const data = res?.data !== undefined ? res.data : res;
+ if (data?.bases && Array.isArray(data.bases)) {
+ setKnowledgeBases(data.bases);
+ } else if (Array.isArray(data)) {
+ setKnowledgeBases(data);
+ } else if (data?.items) {
+ setKnowledgeBases(data.items);
+ } else {
+ setKnowledgeBases([]);
+ }
+ }).catch(() => {});
+ marketAPI.getApps({ type: 'agent' }).then((res: any) => {
+ const data = res?.data !== undefined ? res.data : res;
+ let apps: any[] = [];
+ if (Array.isArray(data)) apps = data;
+ else if (data?.items) apps = data.items;
+ // 显示所有已发布的Agent(不需要安装即可在聊天中使用)
+ setAgents(apps.filter((a: any) => a.status === 'published' || a.status === 'active' || !a.status));
+ }).catch(() => {});
+ marketAPI.getApps({ type: 'skill' }).then((res: any) => {
+ const data = res?.data !== undefined ? res.data : res;
+ let apps: any[] = [];
+ if (Array.isArray(data)) apps = data;
+ else if (data?.items) apps = data.items;
+ // 显示所有已发布的技能(不需要安装即可在聊天中使用)
+ setSkills(apps.filter((a: any) => a.status === 'published' || a.status === 'active' || !a.status));
+ }).catch(() => {});
+ }, []);
+
+ // Determine selected conversation
+ const selectedConversation = useMemo(() => {
+ if (id) {
+ return conversations.find(c => c.id === id) || null;
+ }
+ return conversations[0] || null;
+ }, [id, conversations]);
+
+ // Filter conversations by search
+ const filteredConversations = useMemo(() => {
+ const pinned = conversations.filter(c => c.pinned);
+ const unpinned = conversations.filter(c => !c.pinned);
+ if (!searchQuery.trim()) return [...pinned, ...unpinned];
+ const q = searchQuery.toLowerCase();
+ return [...pinned.filter(c => c.title.toLowerCase().includes(q)), ...unpinned.filter(c => c.title.toLowerCase().includes(q))];
+ }, [conversations, searchQuery]);
+
+ // Selected model info
+ const selectedModel = chatModels.find(m => m.id === selectedModelId) || chatModels[0] || { id: '', name: '选择模型', status: 'free', provider_name: '' };
+
+ // Scroll to bottom when messages change
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ // Select first conversation if no id param
+ useEffect(() => {
+ if (!id && conversations.length > 0) {
+ navigate(`/chat/${conversations[0].id}`, { replace: true });
+ }
+ }, [id, conversations, navigate]);
+
+ // Handle new conversation
+ const handleNewConversation = async () => {
+ try {
+ const res = await chatAPI.createConversation({ title: '新对话', model_id: selectedModelId });
+ const newConv = unwrapData(res);
+ const conv: Conversation = {
+ id: newConv.id,
+ userId: newConv.userId || newConv.user_id || '',
+ title: newConv.title || '新对话',
+ modelId: newConv.modelId || newConv.model_id || selectedModelId,
+ status: 'active',
+ pinned: false,
+ knowledgeGraphCount: 0,
+ createdAt: newConv.createdAt || newConv.created_at || new Date().toISOString(),
+ updatedAt: newConv.updatedAt || newConv.updated_at || new Date().toISOString(),
+ };
+ setConversations(prev => [conv, ...prev]);
+ navigate(`/chat/${conv.id}`);
+ } catch (err: any) {
+ alert('创建对话失败: ' + (err.message || '未知错误'));
+ }
+ };
+
+ // Handle rename
+ const handleRename = async (convId: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ const newTitle = prompt('请输入新名称:');
+ if (newTitle && newTitle.trim()) {
+ try {
+ await chatAPI.updateConversation(convId, { title: newTitle.trim() });
+ setConversations(prev => prev.map(c => c.id === convId ? { ...c, title: newTitle.trim() } : c));
+ } catch (err: any) {
+ alert('重命名失败: ' + (err.message || '未知错误'));
+ }
+ }
+ };
+
+ // Handle archive
+ const handleArchive = async (convId: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ const conv = conversations.find(c => c.id === convId);
+ if (!conv) return;
+ const newStatus = conv.status === 'archived' ? 'active' : 'archived';
+ try {
+ await chatAPI.updateConversation(convId, { status: newStatus });
+ setConversations(prev => prev.map(c => c.id === convId ? { ...c, status: newStatus } as Conversation : c));
+ } catch (err: any) {
+ alert('操作失败: ' + (err.message || '未知错误'));
+ }
+ };
+
+ // Handle delete
+ const handleDelete = async (convId: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (!confirm('确定要删除这个对话吗?')) return;
+ try {
+ await chatAPI.deleteConversation(convId);
+ const next = conversations.filter(c => c.id !== convId);
+ setConversations(next);
+ if (selectedConversation?.id === convId) {
+ if (next.length > 0) {
+ navigate(`/chat/${next[0].id}`);
+ } else {
+ navigate('/chat');
+ }
+ }
+ } catch (err: any) {
+ alert('删除失败: ' + (err.message || '未知错误'));
+ }
+ };
+
+ // Handle pin / unpin
+ const handlePin = async (convId: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ const conv = conversations.find(c => c.id === convId);
+ if (!conv) return;
+ const newPinned = !conv.pinned;
+ try {
+ await chatAPI.updateConversation(convId, { pinned: newPinned });
+ setConversations(prev => {
+ const updated = prev.map(c => c.id === convId ? { ...c, pinned: newPinned } : c);
+ const pinned = updated.filter(c => c.pinned);
+ const unpinned = updated.filter(c => !c.pinned).sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
+ return [...pinned, ...unpinned];
+ });
+ } catch (err: any) {
+ alert('操作失败: ' + (err.message || '未知错误'));
+ }
+ };
+
+ // Handle convert to knowledge graph
+ const handleKnowledgeGraph = async (convId: string, e: React.MouseEvent) => {
+ e.stopPropagation();
+ setKgModalOpen(true);
+ setKgStep(1);
+ setKgResult(null);
+ setKgError(null);
+ setKgConvId(convId);
+
+ try {
+ await new Promise(r => setTimeout(r, 600));
+ setKgStep(2);
+
+ const res = await chatAPI.convertToKnowledgeGraph(convId);
+ await new Promise(r => setTimeout(r, 400));
+ setKgStep(3);
+
+ if (res.code === 0) {
+ const graph = res.data.graph;
+ setKgResult({
+ nodes: graph.stats.total_nodes,
+ edges: graph.stats.total_edges,
+ kbName: res.data.knowledge_base_name,
+ kbId: res.data.knowledge_base_id,
+ });
+ // 使用后端返回的真实计数(幂等,始终为 1),避免前端重复累加导致计数虚高
+ const newCount = res.data.knowledge_graph_count ?? 1;
+ setConversations(prev =>
+ prev.map(c => c.id === convId ? { ...c, knowledgeGraphCount: newCount } : c)
+ );
+ } else {
+ setKgError(res.message || '转换失败');
+ }
+ } catch (err) {
+ setKgError((err as Error).message || '转换失败,请稍后重试');
+ }
+ };
+
+ // Handle send message - CALLS REAL API
+ const handleSend = async () => {
+ if (!inputValue.trim() || sending) return;
+ const content = inputValue.trim();
+ setInputValue('');
+ setSending(true);
+
+ // Add user message optimistically
+ const tempUserMsg: Message = {
+ id: `temp_${Date.now()}`,
+ conversationId: selectedConversation?.id || '',
+ role: 'user',
+ content,
+ tokenUsed: 0,
+ createdAt: new Date().toISOString(),
+ };
+ setMessages(prev => [...prev, tempUserMsg]);
+
+ try {
+ const res = await chatAPI.sendMessage(selectedConversation!.id, {
+ content,
+ model_id: selectedModelId || undefined,
+ agent_ids: selectedAgentIds.length > 0 ? selectedAgentIds : undefined,
+ skill_ids: selectedSkillIds.length > 0 ? selectedSkillIds : undefined,
+ knowledge_base_ids: selectedKbIds.length > 0 ? selectedKbIds : undefined,
+ use_tools: selectedSkillIds.length > 0 || selectedAgentIds.length > 0,
+ });
+
+ const data = unwrapData(res);
+
+ // Replace temporary user message and add assistant response
+ setMessages(prev => {
+ const filtered = prev.filter(m => m.id !== tempUserMsg.id);
+ const newMsgs: Message[] = [];
+ if (data.user_message) {
+ newMsgs.push({
+ id: data.user_message.id,
+ conversationId: selectedConversation?.id || '',
+ role: 'user',
+ content: data.user_message.content,
+ tokenUsed: data.user_message.token_used || 0,
+ createdAt: data.user_message.created_at || new Date().toISOString(),
+ });
+ }
+ if (data.assistant_message) {
+ newMsgs.push({
+ id: data.assistant_message.id,
+ conversationId: selectedConversation?.id || '',
+ role: 'assistant',
+ content: data.assistant_message.content,
+ tokenUsed: data.assistant_message.token_used || 0,
+ createdAt: data.assistant_message.created_at || new Date().toISOString(),
+ used_skills: data.assistant_message.used_skills || [],
+ });
+ }
+ return [...filtered, ...newMsgs];
+ });
+
+ // Refresh conversations to update lastMessage
+ loadConversations();
+
+ // GAP-07: 对话结束后异步提交轨迹到进化引擎(后台静默,不阻塞UI)
+ if (selectedConversation?.id && data.assistant_message) {
+ const recentMsgs = [
+ ...(data.user_message ? [{ role: 'user', content: data.user_message.content, tokens: data.user_message.token_used || 0 }] : []),
+ { role: 'assistant', content: data.assistant_message.content, tokens: data.assistant_message.token_used || 0 },
+ ];
+ evolutionAPI.submitTrajectory({
+ conversation_id: selectedConversation.id,
+ messages: recentMsgs,
+ }).catch(() => {});
+ }
+ } catch (err: any) {
+ // Remove optimistic message on error
+ setMessages(prev => prev.filter(m => m.id !== tempUserMsg.id));
+ alert('发送失败: ' + (err.message || err.detail || '未知错误'));
+ } finally {
+ setSending(false);
+ inputRef.current?.focus();
+ }
+ };
+
+ // Handle key down
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ };
+
+ return (
+
+ {/* Left Sidebar - Conversation List */}
+
+
+ {/* Right Main Chat Area */}
+
+ {selectedConversation ? (
+ <>
+ {/* Chat Header */}
+
+
+
+
+ {selectedModel.name}
+ {(selectedModel.status === 'free' || selectedModel.price_per_million_tokens === 0) ? '免费' : '付费'}
+
+
+ }
+ >
+ {chatModels.filter(m => m.is_enabled !== false).map(model => (
+
+ ))}
+
+
+
{selectedConversation.title}
+
+
+
+ {/* Messages Area */}
+
+ {loadingMessages ? (
+
+ ) : messages.length === 0 ? (
+
+
+
开始一段新对话
+
+ 向 {selectedModel.name} 发送消息,开始对话
+
+
+ ) : (
+ messages.map(msg => {
+ // Real used_skills from API response
+ const usedSkills: string[] = (msg.role === 'assistant' && msg.used_skills) ? msg.used_skills : [];
+ return (
+
+
+ {msg.role === 'assistant' ? (
+
+ ) : (
+ {msg.content}
+ )}
+
+ {/* Skill usage tags */}
+ {msg.role === 'assistant' && usedSkills.length > 0 && (
+
+ {usedSkills.slice(0, 3).map(skillName => (
+
window.location.hash = `#/evolution?skill=${encodeURIComponent(skillName)}`}
+ title={`使用了专属技能:${skillName}`}
+ >
+
+ {skillName}
+
+ ))}
+ {usedSkills.length > 3 && (
+
+{usedSkills.length - 3}
+ )}
+
+ )}
+
+ {formatTime(msg.createdAt)}
+ {msg.tokenUsed} tokens
+
+
+ );
+ })
+ )}
+ {sending && (
+
+ )}
+
+
+
+ {/* Input Area */}
+
+
+
+
+
+ {selectedKbIds.length > 0 ? `知识库 (${selectedKbIds.length})` : '知识库'}
+
+ }
+ >
+ 选择知识库(多选)
+ {knowledgeBases.map((kb: any) => (
+
+ ))}
+ {knowledgeBases.length > 0 && selectedKbIds.length > 0 && (
+
+ )}
+ {selectedKbIds.length > 0 && (
+
+ )}
+ {knowledgeBases.length === 0 && (
+ 暂无可用知识库
+ )}
+
+
+
+
+ {selectedAgentIds.length > 0 ? `Agent (${selectedAgentIds.length})` : 'Agent'}
+
+ }
+ >
+ 选择 Agent(多选)
+ {agents.map((agent: any) => (
+
+ ))}
+ {agents.length > 0 && selectedAgentIds.length > 0 && (
+
+ )}
+ {selectedAgentIds.length > 0 && (
+
+ )}
+ {agents.length === 0 && (
+ 暂无可用 Agent
+ )}
+
+
+
+
+ {selectedSkillIds.length > 0 ? `技能 (${selectedSkillIds.length})` : '技能'}
+
+ }
+ >
+ 选择技能(多选)
+ {skills.map((skill: any) => (
+
+ ))}
+ {skills.length > 0 && selectedSkillIds.length > 0 && (
+
+ )}
+ {selectedSkillIds.length > 0 && (
+
+ )}
+ {skills.length === 0 && (
+ 暂无可用技能
+ )}
+
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
选择或创建对话
+
从左侧列表选择一个对话,或点击上方按钮创建新对话
+
+ )}
+
+
+ {/* Knowledge Graph Conversion Progress Modal */}
+ {kgModalOpen && (
+ !kgError && kgStep !== 2 && setKgModalOpen(false)}>
+
e.stopPropagation()}>
+
+
转换知识图谱
+
+
+
+ {kgError ? (
+
+
+
+
+
{kgError}
+
+
+ ) : kgStep === 3 && kgResult ? (
+
+
+
知识图谱生成完成
+
+
+ {kgResult.nodes}
+ 节点
+
+
+
+ {kgResult.edges}
+ 条边
+
+
+
已存入知识库:{kgResult.kbName}
+
+
+
+
+
+ ) : (
+
+
+
= 1 ? 'active' : ''} ${kgStep > 1 ? 'done' : ''}`}>
+
+ {kgStep > 1 ? (
+
+ ) : kgStep === 1 ? (
+
+ ) : (
+
1
+ )}
+
+
提取对话内容
+
+
+
1 ? 'done' : ''}`}>
+ {kgStep === 2 &&
}
+
+
+
= 2 ? 'active' : ''} ${kgStep > 2 ? 'done' : ''}`}>
+
+ {kgStep > 2 ? (
+
+ ) : kgStep === 2 ? (
+
+ ) : (
+
2
+ )}
+
+
构建知识图谱
+
+
+
2 ? 'done' : ''}`} />
+
+
= 3 ? 'active' : ''} ${kgStep > 3 ? 'done' : ''}`}>
+
+ 3
+
+
完成
+
+
+
+ {kgStep === 1 ? '正在提取对话内容...' : kgStep === 2 ? '正在构建知识图谱...' : '处理完成'}
+
+
+ )}
+
+
+
+ )}
+
+ );
+};
+
+export default ChatPage;