/** * 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`; };