feat: update frontend runtime team controls

This commit is contained in:
2026-04-03 13:48:39 +08:00
parent ecfbd87244
commit a399384e07
9 changed files with 546 additions and 192 deletions

View File

@@ -154,6 +154,32 @@ export const WS_URL =
? `${FALLBACK_WS_PROTOCOL}//${FALLBACK_WS_HOST}:8765`
: `${FALLBACK_WS_PROTOCOL}//${FALLBACK_WS_HOST}${FALLBACK_WS_PORT}/ws`);
// Dynamic Team Management API
const DEFAULT_DYNAMIC_TEAM_API_BASE = isLocalDevHost()
? "http://localhost:8003/api/dynamic-team"
: `${DEFAULT_CONTROL_API_BASE}/dynamic-team`;
export const DYNAMIC_TEAM_API_BASE =
trimTrailingSlash(import.meta.env.VITE_DYNAMIC_TEAM_API_BASE_URL || "") ||
DEFAULT_DYNAMIC_TEAM_API_BASE;
// Dynamic Team API Endpoints
export const DYNAMIC_TEAM_ENDPOINTS = {
// Get all available analyst types (builtin + runtime registered)
listTypes: () => `${DYNAMIC_TEAM_API_BASE}/types`,
// Get personas from personas.yaml
getPersonas: () => `${DYNAMIC_TEAM_API_BASE}/personas`,
// Create a new analyst
createAnalyst: (runId) => `${DYNAMIC_TEAM_API_BASE}/runs/${runId}/analysts`,
// Clone an existing analyst
cloneAnalyst: (runId) => `${DYNAMIC_TEAM_API_BASE}/runs/${runId}/analysts/clone`,
// Remove an analyst
removeAnalyst: (runId, agentId) => `${DYNAMIC_TEAM_API_BASE}/runs/${runId}/analysts/${agentId}`,
// Get analyst info
getAnalystInfo: (runId, agentId) => `${DYNAMIC_TEAM_API_BASE}/runs/${runId}/analysts/${agentId}`,
// Get team summary
getTeamSummary: (runId) => `${DYNAMIC_TEAM_API_BASE}/runs/${runId}/summary`,
};
// Initial ticker symbols for the production watchlist
export const INITIAL_TICKERS = [
{ symbol: "AAPL", price: null, change: null },
@@ -170,3 +196,191 @@ export const INITIAL_TICKERS = [
{ symbol: "COIN", price: null, change: null }
];
// ============================================
// Dynamic Analyst Team Management
// ============================================
/**
* Built-in analyst types that can be used as base for dynamic analysts
*
* IMPORTANT: When creating dynamic analysts, the agent_id MUST end with '_analyst'
* to receive analysis tool groups (fundamentals, technical, sentiment, valuation tools).
* Example: 'crypto_specialist_analyst' (correct) vs 'crypto_specialist' (incorrect)
*/
export const BUILTIN_ANALYST_TYPES = [
{
typeId: "fundamentals_analyst",
name: "基本面分析师",
description: "Uses LLM to intelligently select analysis tools, focuses on financial data and company fundamental analysis",
icon: "fundamentals",
},
{
typeId: "technical_analyst",
name: "技术分析师",
description: "Uses LLM to intelligently select analysis tools, focuses on technical indicators and chart analysis",
icon: "technical",
},
{
typeId: "sentiment_analyst",
name: "情绪分析师",
description: "Uses LLM to intelligently select analysis tools, analyzes market sentiment and news sentiment",
icon: "sentiment",
},
{
typeId: "valuation_analyst",
name: "估值分析师",
description: "Uses LLM to intelligently select analysis tools, focuses on company valuation and value assessment",
icon: "valuation",
},
];
/**
* Default colors for dynamically created analysts
* Cycles through these colors for new analysts
*/
export const DYNAMIC_ANALYST_COLORS = [
{ bg: "#F9FDFF", text: "#1565C0", accent: "#1565C0" }, // Blue
{ bg: "#FFF8F8", text: "#C62828", accent: "#C62828" }, // Red
{ bg: "#FAFFFA", text: "#2E7D32", accent: "#2E7D32" }, // Green
{ bg: "#FCFAFF", text: "#6A1B9A", accent: "#6A1B9A" }, // Purple
{ bg: "#FFFCF7", text: "#E65100", accent: "#E65100" }, // Orange
{ bg: "#F9FEFF", text: "#00838F", accent: "#00838F" }, // Cyan
{ bg: "#FFF9F5", text: "#D84315", accent: "#D84315" }, // Deep Orange
{ bg: "#F5F5FF", text: "#4527A0", accent: "#4527A0" }, // Deep Purple
];
/**
* Generate a color scheme for a dynamic analyst based on index
* @param {number} index - The index of the analyst
* @returns {Object} Color scheme object
*/
export const getDynamicAnalystColors = (index) => {
return DYNAMIC_ANALYST_COLORS[index % DYNAMIC_ANALYST_COLORS.length];
};
/**
* Generate a default avatar URL for dynamic analysts
* Uses a hash of the agentId to select from available avatars
* @param {string} agentId - The agent ID
* @returns {string} Avatar URL
*/
export const getDynamicAnalystAvatar = (agentId) => {
const avatars = [
CDN_ASSETS.companyRoom.agent_1,
CDN_ASSETS.companyRoom.agent_2,
CDN_ASSETS.companyRoom.agent_3,
CDN_ASSETS.companyRoom.agent_4,
CDN_ASSETS.companyRoom.agent_5,
CDN_ASSETS.companyRoom.agent_6,
];
// Simple hash function to consistently map agentId to an avatar
const hash = agentId.split("").reduce((acc, char) => {
return acc + char.charCodeAt(0);
}, 0);
return avatars[hash % avatars.length];
};
/**
* Create a dynamic analyst configuration object
* @param {Object} config - Configuration object
* @param {string} config.agentId - Unique identifier
* @param {string} config.baseType - Base analyst type (e.g., "technical_analyst")
* @param {string} config.name - Display name
* @param {string[]} config.focus - Focus areas
* @param {string} config.description - Description
* @param {number} index - Index for color assignment
* @returns {Object} Complete agent configuration
*/
export const createDynamicAnalystConfig = ({
agentId,
baseType,
name,
focus = [],
description = "",
index = 0,
}) => {
return {
id: agentId,
name: name || agentId,
role: name || agentId,
baseType,
focus,
description,
avatar: getDynamicAnalystAvatar(agentId),
colors: getDynamicAnalystColors(index),
isDynamic: true,
isCustom: true,
};
};
/**
* Check if an agent is a dynamic analyst
* @param {Object} agent - Agent object
* @returns {boolean}
*/
export const isDynamicAnalyst = (agent) => {
return agent?.isDynamic === true || agent?.id?.includes("_");
};
/**
* Validate agent ID format for dynamic analysts
* @param {string} agentId - Agent ID to validate
* @returns {Object} Validation result
*/
export const validateAgentId = (agentId) => {
const errors = [];
const warnings = [];
if (!agentId) {
errors.push("Agent ID is required");
} else if (typeof agentId !== "string") {
errors.push("Agent ID must be a string");
} else {
if (agentId.length < 3) {
errors.push("Agent ID must be at least 3 characters");
}
if (agentId.length > 50) {
errors.push("Agent ID must be at most 50 characters");
}
if (!/^[a-zA-Z0-9_]+$/.test(agentId)) {
errors.push("Agent ID can only contain letters, numbers, and underscores");
}
// Reserved IDs that cannot be used
const reservedIds = ["portfolio_manager", "risk_manager"];
if (reservedIds.includes(agentId)) {
errors.push(`"${agentId}" is a reserved ID and cannot be used`);
}
// Warning: agent_id should end with '_analyst' to get analysis tools
if (!agentId.endsWith("_analyst")) {
warnings.push(
"Agent ID should end with '_analyst' to receive analysis tool groups"
);
}
}
return {
valid: errors.length === 0,
errors,
warnings,
};
};
/**
* Generate a suggested agent ID from a name
* IMPORTANT: Agent ID must end with '_analyst' to receive analysis tools
* @param {string} name - Display name
* @param {string} baseType - Base analyst type
* @returns {string} Suggested agent ID (guaranteed to end with '_analyst')
*/
export const suggestAgentId = (name, baseType) => {
const timestamp = Date.now().toString(36).slice(-4);
const normalized = name
.toLowerCase()
.replace(/[^a-z0-9\s]/g, "")
.replace(/\s+/g, "_")
.replace(/_analyst$/, "") // Remove '_analyst' suffix if present to avoid duplication
.slice(0, 20);
// Must end with '_analyst' to get analysis tools registered
return `${normalized || baseType}_${timestamp}_analyst`;
};