/* Copyright (C) 2023-2026 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ /** * LobeHub Icon Loader * Dynamically load and render icons from @lobehub/icons * * Supports: * - Basic: "OpenAI", "OpenAI.Color" * - Chained properties: "OpenAI.Avatar.type={'platform'}" * - Size parameter: getLobeIcon("OpenAI", 20) */ import * as LobeIcons from '@lobehub/icons' /** * Parse a property value from string to appropriate type * @param raw - Raw string value * @returns Parsed value (boolean, number, or string) */ function parseValue(raw: string | undefined | null): string | number | boolean { if (raw == null) return true let v = String(raw).trim() // Remove curly braces if (v.startsWith('{') && v.endsWith('}')) { v = v.slice(1, -1).trim() } // Remove quotes if ( (v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'")) ) { return v.slice(1, -1) } // Boolean if (v === 'true') return true if (v === 'false') return false // Number if (/^-?\d+(?:\.\d+)?$/.test(v)) return Number(v) // Return as string return v } /** * Get LobeHub icon component by name * @param iconName - Icon name/description (e.g., "OpenAI", "OpenAI.Color", "Claude.Avatar") * @param size - Icon size (default: 20) * @returns Icon component or fallback * * @example * getLobeIcon("OpenAI", 24) * getLobeIcon("OpenAI.Color", 20) * getLobeIcon("Claude.Avatar.type={'platform'}", 32) */ export function getLobeIcon( iconName: string | undefined | null, size: number = 20 ): React.ReactNode { if (!iconName || typeof iconName !== 'string') { return (
?
) } const trimmedName = iconName.trim() if (!trimmedName) { return (
?
) } // Parse component path and chained properties const segments = trimmedName.split('.') const baseKey = segments[0] const BaseIcon = (LobeIcons as Record)[baseKey] as | Record | undefined let IconComponent: React.ComponentType> | undefined let propStartIndex: number if (BaseIcon && segments.length > 1 && BaseIcon[segments[1]]) { IconComponent = BaseIcon[segments[1]] as React.ComponentType< Record > propStartIndex = 2 } else { IconComponent = (LobeIcons as Record)[baseKey] as | React.ComponentType> | undefined propStartIndex = segments.length > 1 && /^[A-Z]/.test(segments[1]) ? 2 : 1 } // Fallback if icon not found if ( !IconComponent || (typeof IconComponent !== 'function' && typeof IconComponent !== 'object') ) { const firstLetter = trimmedName.charAt(0).toUpperCase() return (
{firstLetter}
) } // Parse chained properties (e.g., "type={'platform'}", "shape='square'") const props: Record = {} for (let i = propStartIndex; i < segments.length; i++) { const seg = segments[i] if (!seg) continue const eqIdx = seg.indexOf('=') if (eqIdx === -1) { props[seg.trim()] = true continue } const key = seg.slice(0, eqIdx).trim() const valRaw = seg.slice(eqIdx + 1).trim() props[key] = parseValue(valRaw) } // Set size if not explicitly specified in the string if (props.size == null && size != null) { props.size = size } return }