main
parent
2a237e5ac9
commit
b083d3e064
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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' }}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 }}>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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)' }}>
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue