Add dynamic analyst runtime updates and deployment guides
BIN
frontend/public/media/0.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
frontend/public/media/1.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
frontend/public/media/10.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
frontend/public/media/11.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
frontend/public/media/2.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
frontend/public/media/3.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
frontend/public/media/4.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
frontend/public/media/5.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
frontend/public/media/6.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
frontend/public/media/7.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
frontend/public/media/8.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
BIN
frontend/public/media/9.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
@@ -8,12 +8,17 @@ import { useFeedProcessor } from './hooks/useFeedProcessor';
|
||||
import { useRuntimeControls } from './hooks/useRuntimeControls';
|
||||
import { useStockDataRequests } from './hooks/useStockDataRequests';
|
||||
import { useWebSocketConnection } from './hooks/useWebSocketConnection';
|
||||
import { fetchRuntimeLogs } from './services/runtimeApi';
|
||||
import { fetchRuntimeAgents, fetchRuntimeLogs } from './services/runtimeApi';
|
||||
import { useAgentRunFileState, useAgentStore } from './store/agentStore';
|
||||
import { useMarketStore } from './store/marketStore';
|
||||
import { usePortfolioStore } from './store/portfolioStore';
|
||||
import { useRuntimeStore } from './store/runtimeStore';
|
||||
import { useUIStore } from './store/uiStore';
|
||||
import {
|
||||
buildRuntimeAgentMeta,
|
||||
findAgentByIdOrName,
|
||||
sortRuntimeAgents,
|
||||
} from './utils/agentDisplay';
|
||||
|
||||
const EDITABLE_AGENT_WORKSPACE_FILES = [
|
||||
'SOUL.md',
|
||||
@@ -174,21 +179,57 @@ export default function LiveTradingApp() {
|
||||
const [isRuntimeLogsLoading, setIsRuntimeLogsLoading] = useState(false);
|
||||
const [runtimeLogsPayload, setRuntimeLogsPayload] = useState(null);
|
||||
const [runtimeLogsError, setRuntimeLogsError] = useState(null);
|
||||
const [runtimeAgents, setRuntimeAgents] = useState([]);
|
||||
const agentFeedRef = useRef(null);
|
||||
const isSocketReady = isConnected && connectionStatus === 'connected';
|
||||
|
||||
const selectedAgentId = selectedSkillAgentId || AGENTS[0]?.id || null;
|
||||
const resolvedAgents = useMemo(() => {
|
||||
if (!Array.isArray(runtimeAgents) || runtimeAgents.length === 0) {
|
||||
return AGENTS;
|
||||
}
|
||||
|
||||
return sortRuntimeAgents(runtimeAgents).map((agentState, index) => {
|
||||
const agentId = String(agentState?.agent_id || agentState?.id || '').trim();
|
||||
const base = buildRuntimeAgentMeta(agentId, index);
|
||||
const displayName = typeof agentState?.display_name === 'string' ? agentState.display_name.trim() : '';
|
||||
return {
|
||||
...base,
|
||||
id: agentId,
|
||||
name: displayName || base.name,
|
||||
runtimeStatus: agentState?.status || null,
|
||||
lastSession: agentState?.last_session || null,
|
||||
lastUpdated: agentState?.last_updated || null,
|
||||
};
|
||||
}).filter((agent) => agent.id);
|
||||
}, [runtimeAgents]);
|
||||
|
||||
const selectedAgentId = selectedSkillAgentId || resolvedAgents[0]?.id || null;
|
||||
const selectedAgentProfile = selectedAgentId ? (agentProfilesByAgent[selectedAgentId] || null) : null;
|
||||
const selectedAgentSkills = selectedAgentId ? (agentSkillsByAgent[selectedAgentId] || []) : [];
|
||||
const selectedRunFileContent = selectedAgentId && selectedRunFile
|
||||
? (runFilesByAgent[selectedAgentId]?.[selectedRunFile] || '')
|
||||
: '';
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSkillAgentId && AGENTS.length > 0) {
|
||||
setSelectedSkillAgentId(AGENTS[0].id);
|
||||
const loadRuntimeAgentsList = useCallback(async () => {
|
||||
try {
|
||||
const payload = await fetchRuntimeAgents();
|
||||
setRuntimeAgents(Array.isArray(payload?.agents) ? payload.agents : []);
|
||||
} catch {
|
||||
setRuntimeAgents([]);
|
||||
}
|
||||
}, [selectedSkillAgentId, setSelectedSkillAgentId]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSkillAgentId && resolvedAgents.length > 0) {
|
||||
setSelectedSkillAgentId(resolvedAgents[0].id);
|
||||
}
|
||||
}, [resolvedAgents, selectedSkillAgentId, setSelectedSkillAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSkillAgentId && !resolvedAgents.some((agent) => agent.id === selectedSkillAgentId)) {
|
||||
setSelectedSkillAgentId(resolvedAgents[0]?.id || null);
|
||||
}
|
||||
}, [resolvedAgents, selectedSkillAgentId, setSelectedSkillAgentId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedRunFile) {
|
||||
@@ -196,6 +237,37 @@ export default function LiveTradingApp() {
|
||||
}
|
||||
}, [selectedRunFile, setSelectedWorkspaceFile]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadRuntimeAgentsList();
|
||||
}, [loadRuntimeAgentsList]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleRuntimeAgentsUpdated = () => {
|
||||
void loadRuntimeAgentsList();
|
||||
};
|
||||
window.addEventListener('runtime-agents-updated', handleRuntimeAgentsUpdated);
|
||||
return () => {
|
||||
window.removeEventListener('runtime-agents-updated', handleRuntimeAgentsUpdated);
|
||||
};
|
||||
}, [loadRuntimeAgentsList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSocketReady) {
|
||||
return;
|
||||
}
|
||||
void loadRuntimeAgentsList();
|
||||
}, [isSocketReady, loadRuntimeAgentsList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAgentId || !selectedRunFile) {
|
||||
setRunDraftContent('');
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedContent = runFilesByAgent[selectedAgentId]?.[selectedRunFile];
|
||||
setRunDraftContent(typeof cachedContent === 'string' ? cachedContent : '');
|
||||
}, [runFilesByAgent, selectedAgentId, selectedRunFile, setRunDraftContent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSocketReady || !selectedAgentId || !clientRef.current) {
|
||||
return;
|
||||
@@ -233,7 +305,7 @@ export default function LiveTradingApp() {
|
||||
return;
|
||||
}
|
||||
|
||||
AGENTS.forEach((agent) => {
|
||||
resolvedAgents.forEach((agent) => {
|
||||
if (!agent?.id) {
|
||||
return;
|
||||
}
|
||||
@@ -246,6 +318,7 @@ export default function LiveTradingApp() {
|
||||
clientRef,
|
||||
isSocketReady,
|
||||
requestAgentProfile,
|
||||
resolvedAgents,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -326,13 +399,13 @@ export default function LiveTradingApp() {
|
||||
const bubbleFor = useCallback((idOrName) => {
|
||||
let bubble = bubbles[idOrName];
|
||||
if (bubble) return bubble;
|
||||
const agent = AGENTS.find((item) => item.name === idOrName || item.id === idOrName);
|
||||
const agent = findAgentByIdOrName(resolvedAgents, idOrName);
|
||||
if (agent) {
|
||||
bubble = bubbles[agent.id];
|
||||
if (bubble) return bubble;
|
||||
}
|
||||
return null;
|
||||
}, [bubbles]);
|
||||
}, [bubbles, resolvedAgents]);
|
||||
|
||||
const handleManualTrigger = useCallback(() => {
|
||||
if (!isSocketReady || !clientRef.current) {
|
||||
@@ -361,7 +434,7 @@ export default function LiveTradingApp() {
|
||||
}, []);
|
||||
|
||||
const agentRequests = {
|
||||
agents: AGENTS,
|
||||
agents: resolvedAgents,
|
||||
agentProfilesByAgent,
|
||||
agentSkillsByAgent,
|
||||
runFilesByAgent,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useState, useRef, useImperativeHandle, forwardRef } from 'react';
|
||||
import { formatTime } from '../utils/formatters';
|
||||
import { MESSAGE_COLORS, getAgentColors, AGENTS, ASSETS } from '../config/constants';
|
||||
import { MESSAGE_COLORS, getAgentColors, ASSETS } from '../config/constants';
|
||||
import { getModelIcon } from '../utils/modelIcons';
|
||||
import MarkdownModal from './MarkdownModal';
|
||||
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||
import { findAgentByIdOrName, humanizeAgentId } from '../utils/agentDisplay';
|
||||
|
||||
const isAnalyst = (agentId, agentName) => {
|
||||
if (agentId && agentId.includes('analyst')) return true;
|
||||
@@ -36,7 +37,7 @@ const stripMarkdown = (text) => {
|
||||
.replace(/^[-=]+$/gm, '');
|
||||
};
|
||||
|
||||
const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref) => {
|
||||
const AgentFeed = forwardRef(({ agents = [], feed, leaderboard, agentProfilesByAgent }, ref) => {
|
||||
const feedContentRef = useRef(null);
|
||||
const [highlightedId, setHighlightedId] = useState(null);
|
||||
const [selectedAgent, setSelectedAgent] = useState('all');
|
||||
@@ -62,7 +63,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
// Get agent info by name
|
||||
const getAgentInfoByName = (agentName) => {
|
||||
if (!agentName) return null;
|
||||
const agentConfig = AGENTS.find((agent) => agent.name === agentName);
|
||||
const agentConfig = findAgentByIdOrName(agents, agentName);
|
||||
const profile = agentConfig ? agentProfilesByAgent?.[agentConfig.id] : null;
|
||||
if (agentConfig && profile?.model_name) {
|
||||
return {
|
||||
@@ -81,7 +82,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
};
|
||||
};
|
||||
|
||||
// Get unique agent names from feed (only registered agents in AGENTS)
|
||||
// Get unique agent names from feed using the current runtime agent list.
|
||||
const getUniqueAgents = () => {
|
||||
const agentNamesInFeed = new Set();
|
||||
|
||||
@@ -98,9 +99,10 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
}
|
||||
});
|
||||
|
||||
// Filter to only include registered agents and sort by AGENTS array order
|
||||
const registeredAgentNames = AGENTS.map(a => a.name);
|
||||
return registeredAgentNames.filter(name => agentNamesInFeed.has(name));
|
||||
const orderedRuntimeNames = agents.map((agent) => agent.name);
|
||||
const knownNames = orderedRuntimeNames.filter(name => agentNamesInFeed.has(name));
|
||||
const extraNames = [...agentNamesInFeed].filter(name => !orderedRuntimeNames.includes(name));
|
||||
return [...knownNames, ...extraNames];
|
||||
};
|
||||
|
||||
// Filter feed based on selected agent
|
||||
@@ -177,6 +179,12 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
|
||||
const currentSelection = getCurrentSelectionInfo();
|
||||
|
||||
const resolveAgentDisplayName = (name, agentId) => {
|
||||
if (name) return name;
|
||||
const agent = findAgentByIdOrName(agents, agentId);
|
||||
return agent?.name || humanizeAgentId(agentId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="agent-feed">
|
||||
<div className="agent-feed-header">
|
||||
@@ -241,7 +249,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
type="color"
|
||||
/>
|
||||
)}
|
||||
<span>{agent}</span>
|
||||
<span>{resolveAgentDisplayName(agent, agentInfo?.agentId)}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -255,7 +263,7 @@ const AgentFeed = forwardRef(({ feed, leaderboard, agentProfilesByAgent }, ref)
|
||||
<div className="empty-state">
|
||||
{selectedAgent === 'all'
|
||||
? '等待系统更新...'
|
||||
: `${selectedAgent} 没有消息`}
|
||||
: `${resolveAgentDisplayName(selectedAgent, currentSelection.agentInfo?.agentId)} 没有消息`}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ import GlobalStyles from '../styles/GlobalStyles';
|
||||
import Header from './Header.jsx';
|
||||
import RuntimeSettingsPanel from './RuntimeSettingsPanel.jsx';
|
||||
import NetValueChart from './NetValueChart.jsx';
|
||||
import { AGENTS } from '../config/constants';
|
||||
import { useRuntimeStore } from '../store/runtimeStore';
|
||||
import { useUIStore } from '../store/uiStore';
|
||||
import { formatNumber, formatTickerPrice } from '../utils/formatters';
|
||||
@@ -401,6 +400,7 @@ export default function AppShell({
|
||||
<div className="view-panel">
|
||||
<Suspense fallback={<ViewLoadingFallback label="加载交易室..." />}>
|
||||
<RoomView
|
||||
agents={agentRequests.agents}
|
||||
bubbles={bubbles}
|
||||
bubbleFor={bubbleFor}
|
||||
leaderboard={leaderboard}
|
||||
@@ -501,7 +501,7 @@ export default function AppShell({
|
||||
{/* Right Panel: Agent Feed */}
|
||||
<div className="right-panel" style={{ width: `${100 - leftWidth}%` }}>
|
||||
<Suspense fallback={<ViewLoadingFallback label="加载消息流..." />}>
|
||||
<AgentFeed ref={agentFeedRef} feed={feed} leaderboard={leaderboard} agentProfilesByAgent={agentProfilesByAgent} />
|
||||
<AgentFeed ref={agentFeedRef} agents={agentRequests.agents} feed={feed} leaderboard={leaderboard} agentProfilesByAgent={agentProfilesByAgent} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
||||
import { ASSETS, SCENE_NATIVE, AGENT_SEATS, AGENTS } from '../config/constants';
|
||||
import { ASSETS, SCENE_NATIVE, AGENT_SEATS } from '../config/constants';
|
||||
import AgentCard from './AgentCard';
|
||||
import { getModelIcon } from '../utils/modelIcons';
|
||||
import LobeModelLogo from './LobeModelLogo.jsx';
|
||||
import { findAgentByIdOrName } from '../utils/agentDisplay';
|
||||
|
||||
/**
|
||||
* Custom hook to load an image
|
||||
@@ -48,7 +49,22 @@ function getRankMedal(rank) {
|
||||
* Supports click and hover (1.5s) to show agent performance cards
|
||||
* Supports replay mode - completely independent from live mode
|
||||
*/
|
||||
export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfilesByAgent, feed, onJumpToMessage, onOpenLaunchConfig }) {
|
||||
function getSeatPosition(index) {
|
||||
if (AGENT_SEATS[index]) {
|
||||
return AGENT_SEATS[index];
|
||||
}
|
||||
|
||||
const overflowIndex = index - AGENT_SEATS.length;
|
||||
const columns = 3;
|
||||
const row = Math.floor(overflowIndex / columns);
|
||||
const column = overflowIndex % columns;
|
||||
return {
|
||||
x: 0.18 + (column * 0.18),
|
||||
y: Math.max(0.14, 0.22 - (row * 0.1)),
|
||||
};
|
||||
}
|
||||
|
||||
export default function RoomView({ agents = [], bubbles, bubbleFor, leaderboard, agentProfilesByAgent, feed, onJumpToMessage, onOpenLaunchConfig }) {
|
||||
const canvasRef = useRef(null);
|
||||
const containerRef = useRef(null);
|
||||
|
||||
@@ -152,16 +168,16 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
// Determine which agents are speaking
|
||||
const speakingAgents = useMemo(() => {
|
||||
const speaking = {};
|
||||
AGENTS.forEach(agent => {
|
||||
agents.forEach(agent => {
|
||||
const bubble = bubbleFor(agent.name);
|
||||
speaking[agent.id] = !!bubble;
|
||||
});
|
||||
return speaking;
|
||||
}, [bubbles, bubbleFor]);
|
||||
}, [agents, bubbleFor, bubbles]);
|
||||
|
||||
// Find agent data from leaderboard
|
||||
const getAgentData = (agentId) => {
|
||||
const agent = AGENTS.find(a => a.id === agentId);
|
||||
const agent = agents.find(a => a.id === agentId);
|
||||
if (!agent) return null;
|
||||
const profile = agentProfilesByAgent?.[agentId] || null;
|
||||
|
||||
@@ -195,7 +211,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
};
|
||||
}
|
||||
|
||||
// Merge data but preserve the correct avatar from AGENTS config
|
||||
// Merge data but preserve the configured visual metadata from frontend.
|
||||
return {
|
||||
...agent,
|
||||
...leaderboardData,
|
||||
@@ -317,10 +333,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
// Skip system messages
|
||||
if (msg.agent === 'System') return;
|
||||
// Find matching agent
|
||||
const agent = AGENTS.find(a =>
|
||||
a.id === msg.agentId ||
|
||||
a.name === msg.agent
|
||||
);
|
||||
const agent = findAgentByIdOrName(agents, msg.agentId || msg.agent);
|
||||
if (agent) {
|
||||
messages.push({
|
||||
feedItemId: item.id,
|
||||
@@ -333,10 +346,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
} else if (item.type === 'conference' && item.data?.messages) {
|
||||
item.data.messages.forEach((msg, msgIndex) => {
|
||||
if (msg.agent === 'System') return;
|
||||
const agent = AGENTS.find(a =>
|
||||
a.id === msg.agentId ||
|
||||
a.name === msg.agent
|
||||
);
|
||||
const agent = findAgentByIdOrName(agents, msg.agentId || msg.agent);
|
||||
if (agent) {
|
||||
messages.push({
|
||||
feedItemId: item.id,
|
||||
@@ -479,7 +489,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
if (isReplaying) {
|
||||
// Find replay bubble for this agent
|
||||
const bubble = Object.values(replayBubbles).find(b => {
|
||||
const agent = AGENTS.find(a => a.id === b.agentId);
|
||||
const agent = agents.find(a => a.id === b.agentId);
|
||||
return agent && agent.name === agentName;
|
||||
});
|
||||
return bubble || null;
|
||||
@@ -487,13 +497,13 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
// Use normal bubbleFor function
|
||||
return bubbleFor(agentName);
|
||||
}
|
||||
}, [isReplaying, replayBubbles, bubbleFor]);
|
||||
}, [agents, isReplaying, replayBubbles, bubbleFor]);
|
||||
|
||||
return (
|
||||
<div className="room-view">
|
||||
{/* Agents Indicator Bar */}
|
||||
<div className="room-agents-indicator">
|
||||
{AGENTS.map((agent, index) => {
|
||||
{agents.map((agent, index) => {
|
||||
const rank = getAgentRank(agent.id);
|
||||
const medal = rank ? getRankMedal(rank) : null;
|
||||
const agentData = getAgentData(agent.id);
|
||||
@@ -572,7 +582,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
<canvas ref={canvasRef} className="room-canvas" />
|
||||
|
||||
{/* Speech Bubbles */}
|
||||
{AGENTS.map((agent, idx) => {
|
||||
{agents.map((agent, idx) => {
|
||||
const bubble = getBubbleForAgent(agent.name);
|
||||
if (!bubble) return null;
|
||||
|
||||
@@ -581,7 +591,7 @@ export default function RoomView({ bubbles, bubbleFor, leaderboard, agentProfile
|
||||
// Check if bubble is hidden
|
||||
if (hiddenBubbles[bubbleKey]) return null;
|
||||
|
||||
const pos = AGENT_SEATS[idx];
|
||||
const pos = getSeatPosition(idx);
|
||||
const scaledWidth = SCENE_NATIVE.width * scale;
|
||||
const scaledHeight = SCENE_NATIVE.height * scale;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
const trimTrailingSlash = (value) => value.replace(/\/+$/, "");
|
||||
const mediaAsset = (filename) => `/media/${filename}`;
|
||||
const isLocalDevHost = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return false;
|
||||
@@ -14,12 +15,12 @@ const isLocalDevHost = () => {
|
||||
// Centralized CDN asset URLs
|
||||
export const CDN_ASSETS = {
|
||||
companyRoom: {
|
||||
agent_1: "https://img.alicdn.com/imgextra/i4/O1CN01Lr7SOl1lSExV0tOwv_!!6000000004817-2-tps-370-320.png",
|
||||
agent_2: "https://img.alicdn.com/imgextra/i3/O1CN017Kb8cY1VQNUmuK47o_!!6000000002647-2-tps-368-312.png",
|
||||
agent_3: "https://img.alicdn.com/imgextra/i3/O1CN010Fp55w1YqtGpVjgsS_!!6000000003111-2-tps-370-320.png",
|
||||
agent_4: "https://img.alicdn.com/imgextra/i3/O1CN01VnUsML1Dkq6fHw3ks_!!6000000000255-2-tps-366-316.png",
|
||||
agent_5: "https://img.alicdn.com/imgextra/i4/O1CN01o0kCQw1kyvbulBSl7_!!6000000004753-2-tps-370-314.png",
|
||||
agent_6: "https://img.alicdn.com/imgextra/i2/O1CN01cLV0zl1FI6ULAunTp_!!6000000000463-2-tps-368-320.png",
|
||||
agent_1: mediaAsset("0.png"),
|
||||
agent_2: mediaAsset("1.png"),
|
||||
agent_3: mediaAsset("2.png"),
|
||||
agent_4: mediaAsset("3.png"),
|
||||
agent_5: mediaAsset("4.png"),
|
||||
agent_6: mediaAsset("5.png"),
|
||||
team_logo: "https://img.alicdn.com/imgextra/i2/O1CN01n2S8aV25hcZhhNH95_!!6000000007558-2-tps-616-700.png",
|
||||
reme_logo: "https://img.alicdn.com/imgextra/i2/O1CN01FhncuT1Tqp8LfCaft_!!6000000002434-2-tps-915-250.png",
|
||||
full_room_dark: "https://img.alicdn.com/imgextra/i2/O1CN014sOgzK28re5haGC3X_!!6000000007986-2-tps-1248-832.png",
|
||||
@@ -45,6 +46,14 @@ export const ASSETS = {
|
||||
remeLogo: CDN_ASSETS.companyRoom.reme_logo,
|
||||
};
|
||||
|
||||
export const NON_MANAGER_AVATAR_POOL = Array.from({ length: 10 }, (_, index) => (
|
||||
mediaAsset(`${index + 2}.png`)
|
||||
));
|
||||
|
||||
export const DYNAMIC_ANALYST_AVATAR_POOL = Array.from({ length: 6 }, (_, index) => (
|
||||
mediaAsset(`${index + 6}.png`)
|
||||
));
|
||||
|
||||
// Scene dimensions (actual image size)
|
||||
export const SCENE_NATIVE = { width: 1248, height: 832 };
|
||||
|
||||
@@ -383,4 +392,3 @@ export const suggestAgentId = (name, baseType) => {
|
||||
// Must end with '_analyst' to get analysis tools registered
|
||||
return `${normalized || baseType}_${timestamp}_analyst`;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { AGENTS } from "../config/constants";
|
||||
import { humanizeAgentId } from "../utils/agentDisplay";
|
||||
|
||||
const MAX_FEED_ITEMS = 200;
|
||||
|
||||
@@ -108,7 +109,7 @@ const eventToMessage = (evt) => {
|
||||
id: generateId("msg"),
|
||||
timestamp,
|
||||
agentId: evt.agentId,
|
||||
agent: normalizeAgentLabel(agent?.name || evt.agentName || evt.agentId || "Agent", evt.agentId),
|
||||
agent: normalizeAgentLabel(agent?.name || evt.agentName || humanizeAgentId(evt.agentId) || "Agent", evt.agentId),
|
||||
role: agent?.role || evt.role || "Agent",
|
||||
content: evt.content
|
||||
};
|
||||
@@ -118,7 +119,7 @@ const eventToMessage = (evt) => {
|
||||
id: generateId("memory"),
|
||||
timestamp,
|
||||
agentId: evt.agentId,
|
||||
agent: agent?.name || evt.agentId || "Memory",
|
||||
agent: agent?.name || humanizeAgentId(evt.agentId) || "Memory",
|
||||
role: "Memory",
|
||||
content: evt.content || evt.text || ""
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useCallback } from 'react';
|
||||
import { AGENTS } from '../config/constants';
|
||||
import { fetchRuntimeAgents } from '../services/runtimeApi';
|
||||
import { ReadOnlyClient } from '../services/websocket';
|
||||
import { useRuntimeStore } from '../store/runtimeStore';
|
||||
import { useOpenClawStore } from '../store/openclawStore';
|
||||
@@ -8,6 +9,7 @@ import { usePortfolioStore } from '../store/portfolioStore';
|
||||
import { useAgentStore } from '../store/agentStore';
|
||||
import { useUIStore } from '../store/uiStore';
|
||||
import { normalizeTickerSymbols } from '../services/runtimeControls';
|
||||
import { humanizeAgentId } from '../utils/agentDisplay';
|
||||
|
||||
/**
|
||||
* Normalize price history from server format
|
||||
@@ -401,7 +403,7 @@ export function useWebSocketConnection({
|
||||
setLocalSkillDraftsByKey, setIsAgentSkillsLoading, setSkillDetailLoadingKey,
|
||||
setAgentSkillsSavingKey, setAgentSkillsFeedback, setIsWorkspaceFileLoading,
|
||||
setWorkspaceFileSavingKey, setWorkspaceFilesByAgent, setWorkspaceFileFeedback,
|
||||
selectedSkillAgentId } = useAgentStore();
|
||||
setWorkspaceDraftContent, selectedSkillAgentId } = useAgentStore();
|
||||
|
||||
const { setBubbles } = useUIStore();
|
||||
|
||||
@@ -705,14 +707,19 @@ export function useWebSocketConnection({
|
||||
agent_workspace_file_loaded: (e) => {
|
||||
const agentId = typeof e.agent_id === 'string' ? e.agent_id.trim() : '';
|
||||
const filename = typeof e.filename === 'string' ? e.filename.trim() : '';
|
||||
const content = typeof e.content === 'string' ? e.content : '';
|
||||
if (!agentId || !filename) {
|
||||
setIsWorkspaceFileLoading(false);
|
||||
return;
|
||||
}
|
||||
setWorkspaceFilesByAgent((prev) => ({
|
||||
...prev,
|
||||
[agentId]: { ...(prev[agentId] || {}), [filename]: typeof e.content === 'string' ? e.content : '' }
|
||||
[agentId]: { ...(prev[agentId] || {}), [filename]: content }
|
||||
}));
|
||||
const { selectedSkillAgentId: currentAgentId, selectedWorkspaceFile: currentFilename } = useAgentStore.getState();
|
||||
if (currentAgentId === agentId && currentFilename === filename) {
|
||||
setWorkspaceDraftContent(content);
|
||||
}
|
||||
setIsWorkspaceFileLoading(false);
|
||||
setWorkspaceFileSavingKey(null);
|
||||
},
|
||||
@@ -1018,16 +1025,25 @@ export function useWebSocketConnection({
|
||||
|
||||
agent_message: (e) => {
|
||||
const agent = AGENTS.find((item) => item.id === e.agentId);
|
||||
setBubbles({ [e.agentId]: { text: e.content, ts: Date.now(), agentName: agent?.name || e.agentName || e.agentId } });
|
||||
setBubbles({ [e.agentId]: { text: e.content, ts: Date.now(), agentName: agent?.name || e.agentName || humanizeAgentId(e.agentId) } });
|
||||
processFeedEvent(e);
|
||||
},
|
||||
|
||||
conference_message: (e) => {
|
||||
const agent = AGENTS.find((item) => item.id === e.agentId);
|
||||
setBubbles({ [e.agentId]: { text: e.content, ts: Date.now(), agentName: agent?.name || e.agentName || e.agentId } });
|
||||
setBubbles({ [e.agentId]: { text: e.content, ts: Date.now(), agentName: agent?.name || e.agentName || humanizeAgentId(e.agentId) } });
|
||||
processFeedEvent(e);
|
||||
},
|
||||
|
||||
runtime_agents_updated: async () => {
|
||||
try {
|
||||
await fetchRuntimeAgents();
|
||||
window.dispatchEvent(new CustomEvent('runtime-agents-updated'));
|
||||
} catch {
|
||||
// Ignore refresh failures; next manual/runtime refresh will recover.
|
||||
}
|
||||
},
|
||||
|
||||
memory: (e) => processFeedEvent(e),
|
||||
|
||||
team_summary: (e) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { create } from 'zustand';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
const resolveValue = (updater, currentValue) => (
|
||||
typeof updater === 'function' ? updater(currentValue) : updater
|
||||
@@ -66,13 +67,15 @@ export const useAgentStore = create((set) => ({
|
||||
* Run-scoped file editing state currently reuses legacy `workspace*` field
|
||||
* names inside the store. Prefer this selector for new runtime UI code.
|
||||
*/
|
||||
export const useAgentRunFileState = () => useAgentStore((state) => ({
|
||||
selectedRunFile: state.selectedWorkspaceFile,
|
||||
runFilesByAgent: state.workspaceFilesByAgent,
|
||||
runDraftContent: state.workspaceDraftContent,
|
||||
isRunFileLoading: state.isWorkspaceFileLoading,
|
||||
runFileSavingKey: state.workspaceFileSavingKey,
|
||||
runFileFeedback: state.workspaceFileFeedback,
|
||||
setSelectedRunFile: state.setSelectedWorkspaceFile,
|
||||
setRunDraftContent: state.setWorkspaceDraftContent,
|
||||
}));
|
||||
export const useAgentRunFileState = () => useAgentStore(
|
||||
useShallow((state) => ({
|
||||
selectedRunFile: state.selectedWorkspaceFile,
|
||||
runFilesByAgent: state.workspaceFilesByAgent,
|
||||
runDraftContent: state.workspaceDraftContent,
|
||||
isRunFileLoading: state.isWorkspaceFileLoading,
|
||||
runFileSavingKey: state.workspaceFileSavingKey,
|
||||
runFileFeedback: state.workspaceFileFeedback,
|
||||
setSelectedRunFile: state.setSelectedWorkspaceFile,
|
||||
setRunDraftContent: state.setWorkspaceDraftContent,
|
||||
}))
|
||||
);
|
||||
|
||||
77
frontend/src/utils/agentDisplay.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import { AGENTS, DYNAMIC_ANALYST_AVATAR_POOL } from '../config/constants';
|
||||
|
||||
export const STATIC_AGENT_INDEX = new Map(AGENTS.map((agent, index) => [agent.id, { agent, index }]));
|
||||
const ANALYST_COLOR_PALETTE = AGENTS.filter((agent) => agent.id.endsWith('_analyst')).map((agent) => agent.colors);
|
||||
|
||||
const FALLBACK_AGENT_VISUALS = DYNAMIC_ANALYST_AVATAR_POOL.map((avatar, index) => {
|
||||
return {
|
||||
avatar,
|
||||
colors: ANALYST_COLOR_PALETTE[index % Math.max(ANALYST_COLOR_PALETTE.length, 1)] || AGENTS[0].colors,
|
||||
};
|
||||
});
|
||||
|
||||
export function humanizeAgentId(agentId) {
|
||||
if (!agentId) return '未知 Agent';
|
||||
const normalized = String(agentId).trim();
|
||||
const staticAgent = STATIC_AGENT_INDEX.get(normalized)?.agent;
|
||||
if (staticAgent?.name) {
|
||||
return staticAgent.name;
|
||||
}
|
||||
const label = normalized
|
||||
.replace(/_/g, ' ')
|
||||
.replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
return label || normalized;
|
||||
}
|
||||
|
||||
export function inferAgentRole(agentId) {
|
||||
const normalized = String(agentId || '').trim();
|
||||
const staticAgent = STATIC_AGENT_INDEX.get(normalized)?.agent;
|
||||
if (staticAgent?.role) {
|
||||
return staticAgent.role;
|
||||
}
|
||||
if (normalized === 'portfolio_manager') return '投资经理';
|
||||
if (normalized === 'risk_manager') return '风控经理';
|
||||
if (normalized.endsWith('_analyst')) return '分析师';
|
||||
return 'Agent';
|
||||
}
|
||||
|
||||
export function buildRuntimeAgentMeta(agentId, runtimeIndex = 0) {
|
||||
const normalized = String(agentId || '').trim();
|
||||
const staticAgent = STATIC_AGENT_INDEX.get(normalized)?.agent;
|
||||
if (staticAgent) {
|
||||
return staticAgent;
|
||||
}
|
||||
|
||||
const fallback = FALLBACK_AGENT_VISUALS[runtimeIndex % Math.max(FALLBACK_AGENT_VISUALS.length, 1)] || {
|
||||
avatar: AGENTS[0].avatar,
|
||||
colors: AGENTS[0].colors,
|
||||
};
|
||||
return {
|
||||
id: normalized,
|
||||
name: humanizeAgentId(normalized),
|
||||
role: inferAgentRole(normalized),
|
||||
avatar: fallback.avatar,
|
||||
colors: fallback.colors,
|
||||
};
|
||||
}
|
||||
|
||||
export function sortRuntimeAgents(agents) {
|
||||
const staticOrder = new Map(AGENTS.map((agent, index) => [agent.id, index]));
|
||||
return [...agents].sort((left, right) => {
|
||||
const leftId = String(left?.agent_id || left?.id || '').trim();
|
||||
const rightId = String(right?.agent_id || right?.id || '').trim();
|
||||
const leftStatic = staticOrder.has(leftId) ? staticOrder.get(leftId) : Number.MAX_SAFE_INTEGER;
|
||||
const rightStatic = staticOrder.has(rightId) ? staticOrder.get(rightId) : Number.MAX_SAFE_INTEGER;
|
||||
if (leftStatic !== rightStatic) {
|
||||
return leftStatic - rightStatic;
|
||||
}
|
||||
return leftId.localeCompare(rightId);
|
||||
});
|
||||
}
|
||||
|
||||
export function findAgentByIdOrName(agents, idOrName) {
|
||||
if (!Array.isArray(agents) || !idOrName) {
|
||||
return null;
|
||||
}
|
||||
return agents.find((agent) => agent.id === idOrName || agent.name === idOrName) || null;
|
||||
}
|
||||