diff --git a/src/api.ts b/src/api.ts index 93c4806..80f14d8 100644 --- a/src/api.ts +++ b/src/api.ts @@ -147,6 +147,9 @@ export const AgentAPI = { deleteKnowledge: (agentId: string, fileId: string) => api.delete(`/agents/${agentId}/knowledge/${fileId}`).then((r) => r.data), + getKnowledgeStatus: (agentId: string) => + api.get<{ files: KnowledgeFile[] }>(`/agents/${agentId}/knowledge/status`).then((r) => r.data), + uploadSkills: (id: string, files: File[]) => { const fd = new FormData(); files.forEach((f) => fd.append('files', f)); diff --git a/src/components/ChatPreview.tsx b/src/components/ChatPreview.tsx index 37cec53..e392ef8 100644 --- a/src/components/ChatPreview.tsx +++ b/src/components/ChatPreview.tsx @@ -132,7 +132,7 @@ export default function ChatPreview({ agent, agentId }: Props) { return (
-
+
void; @@ -70,9 +70,11 @@ export default function CapabilitySettings({
-
- {currentName} -
+ {currentName && ( +
+ {currentName} +
+ )}
{agent?.description || '补充基础资料后,这里会更像一个完整可运营的产品角色。'}
@@ -229,9 +231,20 @@ export default function CapabilitySettings({ {item.originalName} {item.size ? `${(item.size / 1024).toFixed(1)} KB` : ''} ·{' '} - - {STATUS_TAG[(item.status || 'ready')].text} - + {item.status === 'indexing' ? ( + + + 索引中… + + + ) : ( + + {STATUS_TAG[(item.status || 'ready')].text} + + )}
diff --git a/src/pages/AgentEditor/components/Header.tsx b/src/pages/AgentEditor/components/Header.tsx index 906af88..5ec3128 100644 --- a/src/pages/AgentEditor/components/Header.tsx +++ b/src/pages/AgentEditor/components/Header.tsx @@ -3,7 +3,7 @@ import { ArrowLeftOutlined, FileTextOutlined, SaveOutlined, RocketOutlined } fro interface HeaderProps { isNew: boolean; - currentName: string; + currentName?: string; navigate: any; autoSaveStatus: 'saved' | 'dirty' | 'saving' | 'error'; saving: boolean; @@ -25,7 +25,7 @@ export default function Header({ isNew, currentName, navigate, autoSaveStatus, s
- {isNew ? '创建新智能体' : currentName} + {isNew ? '创建新智能体' : currentName || ''}
diff --git a/src/pages/AgentEditor/hooks/useAgentEditor.ts b/src/pages/AgentEditor/hooks/useAgentEditor.ts index d330da5..4be33c5 100644 --- a/src/pages/AgentEditor/hooks/useAgentEditor.ts +++ b/src/pages/AgentEditor/hooks/useAgentEditor.ts @@ -26,8 +26,59 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE const [editingSkillId, setEditingSkillId] = useState(null); const pollTimer = useRef(null); + const pollCount = useRef(0); const hydratingRef = useRef(false); + const MAX_POLL_COUNT = 30; + + const stopPolling = () => { + if (pollTimer.current) { + window.clearInterval(pollTimer.current); + pollTimer.current = null; + } + pollCount.current = 0; + }; + + const startPolling = () => { + pollCount.current = 0; + if (pollTimer.current) { + window.clearInterval(pollTimer.current); + } + pollTimer.current = window.setInterval(pollKnowledgeStatus, 4000); + pollKnowledgeStatus(); + }; + + const pollKnowledgeStatus = async () => { + if (!id) return; + try { + const result = await AgentAPI.getKnowledgeStatus(id); + const files = result.files || []; + + setAgent((prev) => { + if (!prev) return prev; + return { ...prev, knowledge: files }; + }); + + const indexing = files.some((k) => k.status === 'pending' || k.status === 'indexing'); + pollCount.current++; + + if (!indexing || pollCount.current >= MAX_POLL_COUNT) { + stopPolling(); + if (!indexing) { + refresh(false); + } else { + message.warning('部分文件处理超时,请稍后刷新页面查看结果'); + } + } + } catch (e) { + pollCount.current++; + if (pollCount.current >= MAX_POLL_COUNT) { + stopPolling(); + message.warning('知识库状态查询超时,请稍后刷新页面查看结果'); + } + } + }; + const refresh = async (force = false) => { if (!id) return; const data = await AgentAPI.detail(id); @@ -45,10 +96,7 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE const indexing = data.knowledge?.some((k) => k.status === 'pending' || k.status === 'indexing'); if (indexing && !pollTimer.current) { - pollTimer.current = window.setInterval(refresh, 2000); - } else if (!indexing && pollTimer.current) { - window.clearInterval(pollTimer.current); - pollTimer.current = null; + startPolling(); } }; @@ -111,7 +159,20 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE if (!silent) setSaving(true); setAutoSaveStatus('saving'); try { - const updatedAgent = await AgentAPI.update(id!, values); + const changedFields: Record = {}; + Object.keys(values).forEach((key) => { + const formValue = (values as any)[key]; + const originalValue = (agent as any)?.[key]; + if (formValue !== originalValue) { + changedFields[key] = formValue; + } + }); + if (Object.keys(changedFields).length === 0) { + if (!silent) message.info('无变化'); + setAutoSaveStatus('saved'); + return; + } + const updatedAgent = await AgentAPI.update(id!, changedFields); setAgent(updatedAgent); form.setFieldsValue(updatedAgent); if (!silent) message.success('已保存'); @@ -171,6 +232,7 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE knowledge: [...uniqueNewFiles, ...(prev.knowledge || [])], }; }); + startPolling(); message.success(`${file.name} 已上传,正在建索引…`); } catch (e: any) { message.error('上传失败:' + (e?.message ?? e)); @@ -192,8 +254,12 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE const handleAvatarSelect = async (avatarUrl: string) => { if (!id) return; try { - const updatedAgent = await AgentAPI.updateAvatar(id, avatarUrl); - setAgent(updatedAgent); + await AgentAPI.updateAvatar(id, avatarUrl); + setAgent((prev) => { + if (!prev) return prev; + return { ...prev, avatar: avatarUrl }; + }); + setSelectedAvatar(avatarUrl); message.success('头像已更新'); setAvatarSelectorOpen(false); } catch (e: any) { @@ -205,8 +271,12 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE if (!id) return false; try { const url = await uploadAvatar(file as File); - const updatedAgent = await AgentAPI.updateAvatar(id, url); - setAgent(updatedAgent); + await AgentAPI.updateAvatar(id, url); + setAgent((prev) => { + if (!prev) return prev; + return { ...prev, avatar: url }; + }); + setSelectedAvatar(url); message.success('头像已更新'); setAvatarSelectorOpen(false); } catch (e: any) { @@ -216,7 +286,7 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE }; const liveAgent = { ...(agent ?? {}), ...(form.getFieldsValue() as Agent) } as Agent; - const currentName = liveAgent?.name || agentName || '未命名智能体'; + const currentName = agent?.name || liveAgent?.name || agentName || undefined; const markDirty = () => { if (hydratingRef.current) return; diff --git a/src/pages/AgentEditor/index.tsx b/src/pages/AgentEditor/index.tsx index 18394e8..7c4c833 100644 --- a/src/pages/AgentEditor/index.tsx +++ b/src/pages/AgentEditor/index.tsx @@ -17,7 +17,6 @@ export default function AgentEditor() { const navigate = useNavigate(); const { message } = AntApp.useApp(); const [form] = Form.useForm(); - const pollTimer = useRef(null); const { agent, @@ -51,15 +50,6 @@ export default function AgentEditor() { markDirty, } = useAgentEditor({ id, isNew, form, message, navigate }); - useEffect(() => { - return () => { - if (pollTimer.current) { - window.clearInterval(pollTimer.current); - pollTimer.current = null; - } - }; - }, []); - return ( <>
diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 4e38f61..ec052e8 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -75,7 +75,7 @@ export default function ChatPage() { const autoScrollRef = useRef(true); const initialScrollDoneRef = useRef(false); const scrollRafRef = useRef(null); - const scrollProgrammaticRef = useRef(false); + const scrollProgrammaticAtRef = useRef(0); const lastScrollTopRef = useRef(0); const userScrollLockRef = useRef(false); @@ -108,7 +108,7 @@ export default function ChatPage() { scrollRafRef.current = null; const el = bodyRef.current; if (!el) return; - scrollProgrammaticRef.current = true; + scrollProgrammaticAtRef.current = Date.now(); el.scrollTop = el.scrollHeight; }); }; @@ -117,19 +117,25 @@ export default function ChatPage() { const el = bodyRef.current; if (!el) return; const onScroll = () => { - if (scrollProgrammaticRef.current) { - scrollProgrammaticRef.current = false; - lastScrollTopRef.current = el.scrollTop; - } else { - const nextTop = el.scrollTop; - const lastTop = lastScrollTopRef.current; - if (nextTop < lastTop - 2) { + const now = Date.now(); + const isProgrammatic = now - scrollProgrammaticAtRef.current < 50; + const nextTop = el.scrollTop; + const lastTop = lastScrollTopRef.current; + const scrollHeight = el.scrollHeight; + const clientHeight = el.clientHeight; + const distance = scrollHeight - nextTop - clientHeight; + + if (!isProgrammatic) { + const scrollDelta = nextTop - lastTop; + if (scrollDelta < -5) { userScrollLockRef.current = true; } - lastScrollTopRef.current = nextTop; } - const distance = el.scrollHeight - el.scrollTop - el.clientHeight; - if (distance < 8) userScrollLockRef.current = false; + lastScrollTopRef.current = nextTop; + + if (distance < 8) { + userScrollLockRef.current = false; + } autoScrollRef.current = distance < 32 && !userScrollLockRef.current; if ((!autoScrollRef.current || userScrollLockRef.current) && scrollRafRef.current) { cancelAnimationFrame(scrollRafRef.current); @@ -137,7 +143,7 @@ export default function ChatPage() { } }; el.addEventListener('scroll', onScroll, { passive: true }); - onScroll(); + setTimeout(onScroll, 100); return () => el.removeEventListener('scroll', onScroll); }, []); @@ -810,7 +816,7 @@ export default function ChatPage() { {(streaming.retrieved.length > 0 || streaming.toolCalls.length > 0) && (
{streaming.retrieved.length > 0 && ( - + )} {streaming.toolCalls.length > 0 && ( @@ -1324,12 +1330,11 @@ function ReasoningView({ reasoning }: { reasoning: string }) { ); } -function RetrievedView({ retrieved, liveStyle }: { retrieved: RetrievedSnippet[]; liveStyle?: boolean }) { +function RetrievedView({ retrieved }: { retrieved: RetrievedSnippet[] }) { return (