395 lines
14 KiB
JavaScript
395 lines
14 KiB
JavaScript
/**
|
|
* Application Configuration Constants
|
|
*/
|
|
|
|
const trimTrailingSlash = (value) => value.replace(/\/+$/, "");
|
|
const mediaAsset = (filename) => `/media/${filename}`;
|
|
const isLocalDevHost = () => {
|
|
if (typeof window === "undefined") {
|
|
return false;
|
|
}
|
|
const host = String(window.location.hostname || "").trim().toLowerCase();
|
|
return host === "localhost" || host === "127.0.0.1";
|
|
};
|
|
|
|
// Centralized CDN asset URLs
|
|
export const CDN_ASSETS = {
|
|
companyRoom: {
|
|
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",
|
|
full_room_with_roles_tech_style: "https://img.alicdn.com/imgextra/i1/O1CN01qhupIj1KU4vF3yoT2_!!6000000001166-2-tps-1248-832.png",
|
|
},
|
|
llmModelLogos: {
|
|
"Zhipu AI": "https://img.alicdn.com/imgextra/i4/O1CN01PavE4h1SdFmbeUj6h_!!6000000002269-2-tps-92-92.png",
|
|
"Alibaba": "https://img.alicdn.com/imgextra/i4/O1CN01mTs8oZ1gsHOj0xy7O_!!6000000004197-0-tps-204-192.jpg",
|
|
"DeepSeek": "https://img.alicdn.com/imgextra/i3/O1CN01ocd9iO1D7S2qgEIXQ_!!6000000000169-2-tps-203-148.png",
|
|
"Moonshot": "https://img.alicdn.com/imgextra/i3/O1CN01rFzJg01wE0QFHNGLy_!!6000000006275-0-tps-182-148.jpg",
|
|
"Anthropic": "https://img.alicdn.com/imgextra/i4/O1CN01Sg8gbo1HKVnoU16rm_!!6000000000739-2-tps-148-148.png",
|
|
"Google": "https://img.alicdn.com/imgextra/i1/O1CN01fZwVYk1caBHdzh9qh_!!6000000003616-0-tps-148-148.jpg",
|
|
"OpenAI": "https://img.alicdn.com/imgextra/i3/O1CN01T1eaM8287qU0nZm91_!!6000000007886-2-tps-148-148.png",
|
|
"Groq": "https://img.alicdn.com/imgextra/i1/O1CN01WxASMc1QjXzhVl3eQ_!!6000000002012-2-tps-170-148.png",
|
|
"Ollama": "https://img.alicdn.com/imgextra/i1/O1CN01pN615e1i4vxLkQjVd_!!6000000004360-2-tps-204-192.png",
|
|
},
|
|
};
|
|
|
|
// Derived asset shortcuts
|
|
export const ASSETS = {
|
|
roomBg: CDN_ASSETS.companyRoom.full_room_with_roles_tech_style,
|
|
teamLogo: CDN_ASSETS.companyRoom.team_logo,
|
|
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 };
|
|
|
|
// Agent seat positions (percentage relative to image, origin at bottom-left)
|
|
// Format: { x: horizontal %, y: vertical % from bottom }
|
|
export const AGENT_SEATS = [
|
|
{ x: 0.44, y: 0.58 }, // portfolio_manager
|
|
{ x: 0.55, y: 0.58 }, // risk_manager
|
|
{ x: 0.33, y: 0.52 }, // valuation_analyst
|
|
{ x: 0.42, y: 0.42 }, // sentiment_analyst
|
|
{ x: 0.56, y: 0.42 }, // fundamentals_analyst
|
|
{ x: 0.61, y: 0.49 }, // technical_analyst
|
|
];
|
|
|
|
// Agent definitions with subtle color schemes (very light backgrounds)
|
|
export const AGENTS = [
|
|
{
|
|
id: "portfolio_manager",
|
|
name: "投资经理",
|
|
role: "投资经理",
|
|
avatar: CDN_ASSETS.companyRoom.agent_1,
|
|
colors: { bg: "#F9FDFF", text: "#1565C0", accent: "#1565C0" }
|
|
},
|
|
{
|
|
id: "risk_manager",
|
|
name: "风控经理",
|
|
role: "风控经理",
|
|
avatar: CDN_ASSETS.companyRoom.agent_2,
|
|
colors: { bg: "#FFF8F8", text: "#C62828", accent: "#C62828" }
|
|
},
|
|
{
|
|
id: "valuation_analyst",
|
|
name: "估值分析师",
|
|
role: "估值分析师",
|
|
avatar: CDN_ASSETS.companyRoom.agent_3,
|
|
colors: { bg: "#FAFFFA", text: "#2E7D32", accent: "#2E7D32" }
|
|
},
|
|
{
|
|
id: "sentiment_analyst",
|
|
name: "情绪分析师",
|
|
role: "情绪分析师",
|
|
avatar: CDN_ASSETS.companyRoom.agent_4,
|
|
colors: { bg: "#FCFAFF", text: "#6A1B9A", accent: "#6A1B9A" }
|
|
},
|
|
{
|
|
id: "fundamentals_analyst",
|
|
name: "基本面分析师",
|
|
role: "基本面分析师",
|
|
avatar: CDN_ASSETS.companyRoom.agent_5,
|
|
colors: { bg: "#FFFCF7", text: "#E65100", accent: "#E65100" }
|
|
},
|
|
{
|
|
id: "technical_analyst",
|
|
name: "技术分析师",
|
|
role: "技术分析师",
|
|
avatar: CDN_ASSETS.companyRoom.agent_6,
|
|
colors: { bg: "#F9FEFF", text: "#00838F", accent: "#00838F" }
|
|
},
|
|
];
|
|
|
|
// LLM logo URLs for reuse
|
|
export const LLM_MODEL_LOGOS = { ...CDN_ASSETS.llmModelLogos };
|
|
|
|
// Message type colors (very subtle backgrounds)
|
|
export const MESSAGE_COLORS = {
|
|
system: { bg: "#FAFAFA", text: "#424242", accent: "#424242" },
|
|
memory: { bg: "#F2FDFF", text: "#00838F", accent: "#00838F" },
|
|
conference: { bg: "#F1F4FF", text: "#3949AB", accent: "#3949AB" }
|
|
};
|
|
|
|
// Helper function to get agent colors by ID or name
|
|
export const getAgentColors = (agentId, agentName) => {
|
|
const agent = AGENTS.find(a => a.id === agentId || a.name === agentName);
|
|
return agent?.colors || MESSAGE_COLORS.system;
|
|
};
|
|
|
|
// UI timing constants
|
|
export const BUBBLE_LIFETIME_MS = 8000;
|
|
export const CHART_MARGIN = { left: 60, right: 20, top: 20, bottom: 40 };
|
|
export const AXIS_TICKS = 5;
|
|
|
|
// WebSocket configuration
|
|
const DEFAULT_CONTROL_API_BASE = isLocalDevHost()
|
|
? "http://localhost:8000/api"
|
|
: "/api";
|
|
const DEFAULT_RUNTIME_API_BASE = isLocalDevHost()
|
|
? "http://localhost:8003/api/runtime"
|
|
: `${DEFAULT_CONTROL_API_BASE}/runtime`;
|
|
export const CONTROL_API_BASE =
|
|
trimTrailingSlash(import.meta.env.VITE_CONTROL_API_BASE_URL || "") || DEFAULT_CONTROL_API_BASE;
|
|
export const RUNTIME_API_BASE =
|
|
trimTrailingSlash(import.meta.env.VITE_RUNTIME_API_BASE_URL || "") ||
|
|
DEFAULT_RUNTIME_API_BASE;
|
|
const FALLBACK_WS_PROTOCOL =
|
|
typeof window !== "undefined" && window.location.protocol === "https:"
|
|
? "wss:"
|
|
: "ws:";
|
|
const FALLBACK_WS_HOST =
|
|
typeof window !== "undefined" ? window.location.hostname : "localhost";
|
|
const FALLBACK_WS_PORT =
|
|
typeof window !== "undefined" && window.location.port
|
|
? `:${window.location.port}`
|
|
: "";
|
|
export const WS_URL =
|
|
import.meta.env.VITE_WS_URL ||
|
|
(isLocalDevHost()
|
|
? `${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 },
|
|
{ symbol: "MSFT", price: null, change: null },
|
|
{ symbol: "GOOGL", price: null, change: null },
|
|
{ symbol: "AMZN", price: null, change: null },
|
|
{ symbol: "NVDA", price: null, change: null },
|
|
{ symbol: "META", price: null, change: null },
|
|
{ symbol: "TSLA", price: null, change: null },
|
|
{ symbol: "AMD", price: null, change: null },
|
|
{ symbol: "NFLX", price: null, change: null },
|
|
{ symbol: "AVGO", price: null, change: null },
|
|
{ symbol: "PLTR", price: null, change: null },
|
|
{ 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`;
|
|
};
|