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