sp mac bookpro 2605 2026-06-02 21:29:17 +08:00
parent 2a237e5ac9
commit b083d3e064
8 changed files with 137 additions and 46 deletions

View File

@ -147,6 +147,9 @@ export const AgentAPI = {
deleteKnowledge: (agentId: string, fileId: string) => deleteKnowledge: (agentId: string, fileId: string) =>
api.delete(`/agents/${agentId}/knowledge/${fileId}`).then((r) => r.data), 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[]) => { uploadSkills: (id: string, files: File[]) => {
const fd = new FormData(); const fd = new FormData();
files.forEach((f) => fd.append('files', f)); files.forEach((f) => fd.append('files', f));

View File

@ -132,7 +132,7 @@ export default function ChatPreview({ agent, agentId }: Props) {
return ( return (
<div className="flex flex-col h-full bg-gray-50 border-l"> <div className="flex flex-col h-full bg-gray-50 border-l">
<div className="p-4 border-b bg-white flex items-center gap-3"> <div className="p-4 border-b bg-white flex items-center gap-2">
<div <div
className="rounded-full text-white flex items-center justify-center font-bold overflow-hidden" className="rounded-full text-white flex items-center justify-center font-bold overflow-hidden"
style={{ width: 84, height: 84, background: agent.avatar || '#0891b2' }} style={{ width: 84, height: 84, background: agent.avatar || '#0891b2' }}

View File

@ -9,7 +9,7 @@ interface CapabilitySettingsProps {
agent: Agent | null; agent: Agent | null;
teams: Team[]; teams: Team[];
models: any[]; models: any[];
currentName: string; currentName?: string;
agentName: string; agentName: string;
selectedAvatar: string; selectedAvatar: string;
setAvatarSelectorOpen: (open: boolean) => void; setAvatarSelectorOpen: (open: boolean) => void;
@ -70,9 +70,11 @@ export default function CapabilitySettings({
</div> </div>
</div> </div>
<div style={{ minWidth: 0 }}> <div style={{ minWidth: 0 }}>
{currentName && (
<div style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4 }}> <div style={{ fontSize: 16, fontWeight: 700, color: 'var(--color-text)', marginBottom: 4 }}>
{currentName} {currentName}
</div> </div>
)}
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 8 }}> <div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginBottom: 8 }}>
{agent?.description || '补充基础资料后,这里会更像一个完整可运营的产品角色。'} {agent?.description || '补充基础资料后,这里会更像一个完整可运营的产品角色。'}
</div> </div>
@ -229,9 +231,20 @@ export default function CapabilitySettings({
<span className="text-sm font-medium truncate">{item.originalName}</span> <span className="text-sm font-medium truncate">{item.originalName}</span>
<span className="text-[10px] text-gray-400"> <span className="text-[10px] text-gray-400">
{item.size ? `${(item.size / 1024).toFixed(1)} KB` : ''} ·{' '} {item.size ? `${(item.size / 1024).toFixed(1)} KB` : ''} ·{' '}
{item.status === 'indexing' ? (
<Tag color="processing" className="m-0 text-[10px] px-1">
<span
className="inline-block"
style={{ animation: 'pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite' }}
>
</span>
</Tag>
) : (
<Tag color={STATUS_TAG[(item.status || 'ready')].color} className="m-0 text-[10px] px-1"> <Tag color={STATUS_TAG[(item.status || 'ready')].color} className="m-0 text-[10px] px-1">
{STATUS_TAG[(item.status || 'ready')].text} {STATUS_TAG[(item.status || 'ready')].text}
</Tag> </Tag>
)}
</span> </span>
</div> </div>
</List.Item> </List.Item>

View File

@ -3,7 +3,7 @@ import { ArrowLeftOutlined, FileTextOutlined, SaveOutlined, RocketOutlined } fro
interface HeaderProps { interface HeaderProps {
isNew: boolean; isNew: boolean;
currentName: string; currentName?: string;
navigate: any; navigate: any;
autoSaveStatus: 'saved' | 'dirty' | 'saving' | 'error'; autoSaveStatus: 'saved' | 'dirty' | 'saving' | 'error';
saving: boolean; saving: boolean;
@ -25,7 +25,7 @@ export default function Header({ isNew, currentName, navigate, autoSaveStatus, s
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<RocketOutlined style={{ color: 'var(--color-brand)', fontSize: 18 }} /> <RocketOutlined style={{ color: 'var(--color-brand)', fontSize: 18 }} />
<span className="font-bold text-lg text-gray-800" style={{ color: 'var(--color-text)' }}> <span className="font-bold text-lg text-gray-800" style={{ color: 'var(--color-text)' }}>
{isNew ? '创建新智能体' : currentName} {isNew ? '创建新智能体' : currentName || ''}
</span> </span>
</div> </div>
<div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginTop: 2 }}> <div style={{ fontSize: 12.5, color: 'var(--color-text-secondary)', marginTop: 2 }}>

View File

@ -26,8 +26,59 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE
const [editingSkillId, setEditingSkillId] = useState<string | null>(null); const [editingSkillId, setEditingSkillId] = useState<string | null>(null);
const pollTimer = useRef<number | null>(null); const pollTimer = useRef<number | null>(null);
const pollCount = useRef(0);
const hydratingRef = useRef(false); 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) => { const refresh = async (force = false) => {
if (!id) return; if (!id) return;
const data = await AgentAPI.detail(id); 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'); const indexing = data.knowledge?.some((k) => k.status === 'pending' || k.status === 'indexing');
if (indexing && !pollTimer.current) { if (indexing && !pollTimer.current) {
pollTimer.current = window.setInterval(refresh, 2000); startPolling();
} else if (!indexing && pollTimer.current) {
window.clearInterval(pollTimer.current);
pollTimer.current = null;
} }
}; };
@ -111,7 +159,20 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE
if (!silent) setSaving(true); if (!silent) setSaving(true);
setAutoSaveStatus('saving'); setAutoSaveStatus('saving');
try { try {
const updatedAgent = await AgentAPI.update(id!, values); const changedFields: Record<string, any> = {};
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); setAgent(updatedAgent);
form.setFieldsValue(updatedAgent); form.setFieldsValue(updatedAgent);
if (!silent) message.success('已保存'); if (!silent) message.success('已保存');
@ -171,6 +232,7 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE
knowledge: [...uniqueNewFiles, ...(prev.knowledge || [])], knowledge: [...uniqueNewFiles, ...(prev.knowledge || [])],
}; };
}); });
startPolling();
message.success(`${file.name} 已上传,正在建索引…`); message.success(`${file.name} 已上传,正在建索引…`);
} catch (e: any) { } catch (e: any) {
message.error('上传失败:' + (e?.message ?? e)); message.error('上传失败:' + (e?.message ?? e));
@ -192,8 +254,12 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE
const handleAvatarSelect = async (avatarUrl: string) => { const handleAvatarSelect = async (avatarUrl: string) => {
if (!id) return; if (!id) return;
try { try {
const updatedAgent = await AgentAPI.updateAvatar(id, avatarUrl); await AgentAPI.updateAvatar(id, avatarUrl);
setAgent(updatedAgent); setAgent((prev) => {
if (!prev) return prev;
return { ...prev, avatar: avatarUrl };
});
setSelectedAvatar(avatarUrl);
message.success('头像已更新'); message.success('头像已更新');
setAvatarSelectorOpen(false); setAvatarSelectorOpen(false);
} catch (e: any) { } catch (e: any) {
@ -205,8 +271,12 @@ export function useAgentEditor({ id, isNew, form, message, navigate }: UseAgentE
if (!id) return false; if (!id) return false;
try { try {
const url = await uploadAvatar(file as File); const url = await uploadAvatar(file as File);
const updatedAgent = await AgentAPI.updateAvatar(id, url); await AgentAPI.updateAvatar(id, url);
setAgent(updatedAgent); setAgent((prev) => {
if (!prev) return prev;
return { ...prev, avatar: url };
});
setSelectedAvatar(url);
message.success('头像已更新'); message.success('头像已更新');
setAvatarSelectorOpen(false); setAvatarSelectorOpen(false);
} catch (e: any) { } 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 liveAgent = { ...(agent ?? {}), ...(form.getFieldsValue() as Agent) } as Agent;
const currentName = liveAgent?.name || agentName || '未命名智能体'; const currentName = agent?.name || liveAgent?.name || agentName || undefined;
const markDirty = () => { const markDirty = () => {
if (hydratingRef.current) return; if (hydratingRef.current) return;

View File

@ -17,7 +17,6 @@ export default function AgentEditor() {
const navigate = useNavigate(); const navigate = useNavigate();
const { message } = AntApp.useApp(); const { message } = AntApp.useApp();
const [form] = Form.useForm(); const [form] = Form.useForm();
const pollTimer = useRef<number | null>(null);
const { const {
agent, agent,
@ -51,15 +50,6 @@ export default function AgentEditor() {
markDirty, markDirty,
} = useAgentEditor({ id, isNew, form, message, navigate }); } = useAgentEditor({ id, isNew, form, message, navigate });
useEffect(() => {
return () => {
if (pollTimer.current) {
window.clearInterval(pollTimer.current);
pollTimer.current = null;
}
};
}, []);
return ( return (
<> <>
<div className="fixed inset-0 flex flex-col bg-white z-100 agent-editor-shell" style={{ background: 'var(--color-bg)' }}> <div className="fixed inset-0 flex flex-col bg-white z-100 agent-editor-shell" style={{ background: 'var(--color-bg)' }}>

View File

@ -75,7 +75,7 @@ export default function ChatPage() {
const autoScrollRef = useRef(true); const autoScrollRef = useRef(true);
const initialScrollDoneRef = useRef(false); const initialScrollDoneRef = useRef(false);
const scrollRafRef = useRef<number | null>(null); const scrollRafRef = useRef<number | null>(null);
const scrollProgrammaticRef = useRef(false); const scrollProgrammaticAtRef = useRef(0);
const lastScrollTopRef = useRef(0); const lastScrollTopRef = useRef(0);
const userScrollLockRef = useRef(false); const userScrollLockRef = useRef(false);
@ -108,7 +108,7 @@ export default function ChatPage() {
scrollRafRef.current = null; scrollRafRef.current = null;
const el = bodyRef.current; const el = bodyRef.current;
if (!el) return; if (!el) return;
scrollProgrammaticRef.current = true; scrollProgrammaticAtRef.current = Date.now();
el.scrollTop = el.scrollHeight; el.scrollTop = el.scrollHeight;
}); });
}; };
@ -117,19 +117,25 @@ export default function ChatPage() {
const el = bodyRef.current; const el = bodyRef.current;
if (!el) return; if (!el) return;
const onScroll = () => { const onScroll = () => {
if (scrollProgrammaticRef.current) { const now = Date.now();
scrollProgrammaticRef.current = false; const isProgrammatic = now - scrollProgrammaticAtRef.current < 50;
lastScrollTopRef.current = el.scrollTop;
} else {
const nextTop = el.scrollTop; const nextTop = el.scrollTop;
const lastTop = lastScrollTopRef.current; const lastTop = lastScrollTopRef.current;
if (nextTop < lastTop - 2) { 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; userScrollLockRef.current = true;
} }
lastScrollTopRef.current = nextTop;
} }
const distance = el.scrollHeight - el.scrollTop - el.clientHeight; lastScrollTopRef.current = nextTop;
if (distance < 8) userScrollLockRef.current = false;
if (distance < 8) {
userScrollLockRef.current = false;
}
autoScrollRef.current = distance < 32 && !userScrollLockRef.current; autoScrollRef.current = distance < 32 && !userScrollLockRef.current;
if ((!autoScrollRef.current || userScrollLockRef.current) && scrollRafRef.current) { if ((!autoScrollRef.current || userScrollLockRef.current) && scrollRafRef.current) {
cancelAnimationFrame(scrollRafRef.current); cancelAnimationFrame(scrollRafRef.current);
@ -137,7 +143,7 @@ export default function ChatPage() {
} }
}; };
el.addEventListener('scroll', onScroll, { passive: true }); el.addEventListener('scroll', onScroll, { passive: true });
onScroll(); setTimeout(onScroll, 100);
return () => el.removeEventListener('scroll', onScroll); 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) && (
<div style={{ maxWidth: '85%' }}> <div style={{ maxWidth: '85%' }}>
{streaming.retrieved.length > 0 && ( {streaming.retrieved.length > 0 && (
<RetrievedView retrieved={streaming.retrieved} liveStyle /> <RetrievedView retrieved={streaming.retrieved} />
)} )}
{streaming.toolCalls.length > 0 && ( {streaming.toolCalls.length > 0 && (
<ToolCallView calls={streaming.toolCalls} liveStyle /> <ToolCallView calls={streaming.toolCalls} liveStyle />
@ -1324,12 +1330,11 @@ function ReasoningView({ reasoning }: { reasoning: string }) {
); );
} }
function RetrievedView({ retrieved, liveStyle }: { retrieved: RetrievedSnippet[]; liveStyle?: boolean }) { function RetrievedView({ retrieved }: { retrieved: RetrievedSnippet[] }) {
return ( return (
<Collapse <Collapse
size="small" size="small"
ghost ghost
defaultActiveKey={liveStyle ? ['rag'] : undefined}
items={[ items={[
{ {
key: 'rag', key: 'rag',

View File

@ -1259,3 +1259,13 @@ body {
font-weight: 500; font-weight: 500;
text-shadow: 0 1px 2px rgba(0,0,0,0.8); text-shadow: 0 1px 2px rgba(0,0,0,0.8);
} }
/* Pulse Animation for Indexing Status */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}