From a56a578e914cfb6d350315a45c9e2a55d23ff4e8 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Fri, 21 Nov 2025 23:20:39 +0800 Subject: [PATCH] feat(ui): add icon picker, color picker and provider icon components Implement comprehensive icon selection system for provider customization: ## New Components ### ProviderIcon (src/components/ProviderIcon.tsx) - Render SVG icons by name with automatic fallback - Display provider initials when icon not found - Support custom sizing via size prop - Use dangerouslySetInnerHTML for inline SVG rendering ### IconPicker (src/components/IconPicker.tsx) - Grid-based icon selection with visual preview - Real-time search filtering by name and keywords - Integration with icon metadata for display names - Responsive grid layout (6-10 columns based on screen) ### ColorPicker (src/components/ColorPicker.tsx) - 12 preset colors for quick selection - Native color input for custom color picking - Hex input field for precise color entry - Visual feedback for selected color state ## Icon Assets (src/icons/extracted/) - 38 high-quality SVG icons for AI providers and platforms - Includes: OpenAI, Claude, DeepSeek, Qwen, Kimi, Gemini, etc. - Cloud platforms: AWS, Azure, Google Cloud, Cloudflare - Auto-generated index.ts with getIcon/hasIcon helpers - Metadata system with searchable keywords per icon ## Build Scripts - scripts/extract-icons.js: Extract icons from simple-icons - scripts/generate-icon-index.js: Generate TypeScript index file --- scripts/extract-icons.js | 208 +++++++++++++++++ scripts/generate-icon-index.js | 113 +++++++++ src/components/ColorPicker.tsx | 76 +++++++ src/components/IconPicker.tsx | 85 +++++++ src/components/ProviderIcon.tsx | 81 +++++++ src/icons/extracted/alibaba.svg | 1 + src/icons/extracted/anthropic.svg | 1 + src/icons/extracted/aws.svg | 1 + src/icons/extracted/azure.svg | 1 + src/icons/extracted/baidu.svg | 1 + src/icons/extracted/bytedance.svg | 1 + src/icons/extracted/chatglm.svg | 1 + src/icons/extracted/claude.svg | 1 + src/icons/extracted/cloudflare.svg | 1 + src/icons/extracted/cohere.svg | 1 + src/icons/extracted/copilot.svg | 1 + src/icons/extracted/deepseek.svg | 1 + src/icons/extracted/doubao.svg | 1 + src/icons/extracted/gemini.svg | 1 + src/icons/extracted/gemma.svg | 1 + src/icons/extracted/github.svg | 1 + src/icons/extracted/githubcopilot.svg | 1 + src/icons/extracted/google.svg | 1 + src/icons/extracted/googlecloud.svg | 1 + src/icons/extracted/grok.svg | 1 + src/icons/extracted/huawei.svg | 1 + src/icons/extracted/huggingface.svg | 1 + src/icons/extracted/hunyuan.svg | 1 + src/icons/extracted/index.ts | 57 +++++ src/icons/extracted/kimi.svg | 1 + src/icons/extracted/meta.svg | 1 + src/icons/extracted/metadata.ts | 315 ++++++++++++++++++++++++++ src/icons/extracted/midjourney.svg | 1 + src/icons/extracted/minimax.svg | 1 + src/icons/extracted/mistral.svg | 1 + src/icons/extracted/notion.svg | 1 + src/icons/extracted/ollama.svg | 1 + src/icons/extracted/openai.svg | 1 + src/icons/extracted/palm.svg | 1 + src/icons/extracted/perplexity.svg | 1 + src/icons/extracted/qwen.svg | 1 + src/icons/extracted/stability.svg | 1 + src/icons/extracted/tencent.svg | 1 + src/icons/extracted/vercel.svg | 1 + src/icons/extracted/wenxin.svg | 1 + src/icons/extracted/xai.svg | 1 + src/icons/extracted/yi.svg | 1 + src/icons/extracted/zeroone.svg | 1 + src/icons/extracted/zhipu.svg | 1 + 49 files changed, 977 insertions(+) create mode 100644 scripts/extract-icons.js create mode 100644 scripts/generate-icon-index.js create mode 100644 src/components/ColorPicker.tsx create mode 100644 src/components/IconPicker.tsx create mode 100644 src/components/ProviderIcon.tsx create mode 100644 src/icons/extracted/alibaba.svg create mode 100644 src/icons/extracted/anthropic.svg create mode 100644 src/icons/extracted/aws.svg create mode 100644 src/icons/extracted/azure.svg create mode 100644 src/icons/extracted/baidu.svg create mode 100644 src/icons/extracted/bytedance.svg create mode 100644 src/icons/extracted/chatglm.svg create mode 100644 src/icons/extracted/claude.svg create mode 100644 src/icons/extracted/cloudflare.svg create mode 100644 src/icons/extracted/cohere.svg create mode 100644 src/icons/extracted/copilot.svg create mode 100644 src/icons/extracted/deepseek.svg create mode 100644 src/icons/extracted/doubao.svg create mode 100644 src/icons/extracted/gemini.svg create mode 100644 src/icons/extracted/gemma.svg create mode 100644 src/icons/extracted/github.svg create mode 100644 src/icons/extracted/githubcopilot.svg create mode 100644 src/icons/extracted/google.svg create mode 100644 src/icons/extracted/googlecloud.svg create mode 100644 src/icons/extracted/grok.svg create mode 100644 src/icons/extracted/huawei.svg create mode 100644 src/icons/extracted/huggingface.svg create mode 100644 src/icons/extracted/hunyuan.svg create mode 100644 src/icons/extracted/index.ts create mode 100644 src/icons/extracted/kimi.svg create mode 100644 src/icons/extracted/meta.svg create mode 100644 src/icons/extracted/metadata.ts create mode 100644 src/icons/extracted/midjourney.svg create mode 100644 src/icons/extracted/minimax.svg create mode 100644 src/icons/extracted/mistral.svg create mode 100644 src/icons/extracted/notion.svg create mode 100644 src/icons/extracted/ollama.svg create mode 100644 src/icons/extracted/openai.svg create mode 100644 src/icons/extracted/palm.svg create mode 100644 src/icons/extracted/perplexity.svg create mode 100644 src/icons/extracted/qwen.svg create mode 100644 src/icons/extracted/stability.svg create mode 100644 src/icons/extracted/tencent.svg create mode 100644 src/icons/extracted/vercel.svg create mode 100644 src/icons/extracted/wenxin.svg create mode 100644 src/icons/extracted/xai.svg create mode 100644 src/icons/extracted/yi.svg create mode 100644 src/icons/extracted/zeroone.svg create mode 100644 src/icons/extracted/zhipu.svg diff --git a/scripts/extract-icons.js b/scripts/extract-icons.js new file mode 100644 index 0000000..2e14d4c --- /dev/null +++ b/scripts/extract-icons.js @@ -0,0 +1,208 @@ +const fs = require('fs'); +const path = require('path'); + +// 要提取的图标列表(按分类组织) +const ICONS_TO_EXTRACT = { + // AI 服务商(必需) + aiProviders: [ + 'openai', 'anthropic', 'claude', 'google', 'gemini', + 'deepseek', 'kimi', 'moonshot', 'zhipu', 'minimax', + 'baidu', 'alibaba', 'tencent', 'meta', 'microsoft', + 'cohere', 'perplexity', 'mistral', 'huggingface' + ], + + // 云平台 + cloudPlatforms: [ + 'aws', 'azure', 'huawei', 'cloudflare' + ], + + // 开发工具 + devTools: [ + 'github', 'gitlab', 'docker', 'kubernetes', 'vscode' + ], + + // 其他 + others: [ + 'settings', 'folder', 'file', 'link' + ] +}; + +// 合并所有图标 +const ALL_ICONS = [ + ...ICONS_TO_EXTRACT.aiProviders, + ...ICONS_TO_EXTRACT.cloudPlatforms, + ...ICONS_TO_EXTRACT.devTools, + ...ICONS_TO_EXTRACT.others +]; + +// 提取逻辑 +const OUTPUT_DIR = path.join(__dirname, '../src/icons/extracted'); +const SOURCE_DIR = path.join(__dirname, '../node_modules/@lobehub/icons-static-svg/icons'); + +// 确保输出目录存在 +if (!fs.existsSync(OUTPUT_DIR)) { + fs.mkdirSync(OUTPUT_DIR, { recursive: true }); +} + +console.log('🎨 CC-Switch Icon Extractor\n'); +console.log('========================================'); +console.log('📦 Extracting icons...\n'); + +let extracted = 0; +let notFound = []; + +// 提取图标 +ALL_ICONS.forEach(iconName => { + const sourceFile = path.join(SOURCE_DIR, `${iconName}.svg`); + const targetFile = path.join(OUTPUT_DIR, `${iconName}.svg`); + + if (fs.existsSync(sourceFile)) { + fs.copyFileSync(sourceFile, targetFile); + console.log(` ✓ ${iconName}.svg`); + extracted++; + } else { + console.log(` ✗ ${iconName}.svg (not found)`); + notFound.push(iconName); + } +}); + +// 生成索引文件 +console.log('\n📝 Generating index file...\n'); + +const indexContent = `// Auto-generated icon index +// Do not edit manually + +export const icons: Record = { +${ALL_ICONS.filter(name => !notFound.includes(name)) + .map(name => { + const svg = fs.readFileSync(path.join(OUTPUT_DIR, `${name}.svg`), 'utf-8'); + const escaped = svg.replace(/`/g, '\\`').replace(/\$/g, '\\$'); + return ` '${name}': \`${escaped}\`,`; + }) + .join('\n')} +}; + +export const iconList = Object.keys(icons); + +export function getIcon(name: string): string { + return icons[name.toLowerCase()] || ''; +} + +export function hasIcon(name: string): boolean { + return name.toLowerCase() in icons; +} +`; + +fs.writeFileSync(path.join(OUTPUT_DIR, 'index.ts'), indexContent); +console.log('✓ Generated: src/icons/extracted/index.ts'); + +// 生成图标元数据 +const metadataContent = `// Icon metadata for search and categorization +import { IconMetadata } from '@/types/icon'; + +export const iconMetadata: Record = { + // AI Providers + openai: { name: 'openai', displayName: 'OpenAI', category: 'ai-provider', keywords: ['gpt', 'chatgpt'], defaultColor: '#00A67E' }, + anthropic: { name: 'anthropic', displayName: 'Anthropic', category: 'ai-provider', keywords: ['claude'], defaultColor: '#D4915D' }, + claude: { name: 'claude', displayName: 'Claude', category: 'ai-provider', keywords: ['anthropic'], defaultColor: '#D4915D' }, + google: { name: 'google', displayName: 'Google', category: 'ai-provider', keywords: ['gemini', 'bard'], defaultColor: '#4285F4' }, + gemini: { name: 'gemini', displayName: 'Gemini', category: 'ai-provider', keywords: ['google'], defaultColor: '#4285F4' }, + deepseek: { name: 'deepseek', displayName: 'DeepSeek', category: 'ai-provider', keywords: ['deep', 'seek'], defaultColor: '#1E88E5' }, + moonshot: { name: 'moonshot', displayName: 'Moonshot', category: 'ai-provider', keywords: ['kimi', 'moonshot'], defaultColor: '#6366F1' }, + kimi: { name: 'kimi', displayName: 'Kimi', category: 'ai-provider', keywords: ['moonshot'], defaultColor: '#6366F1' }, + zhipu: { name: 'zhipu', displayName: 'Zhipu AI', category: 'ai-provider', keywords: ['chatglm', 'glm'], defaultColor: '#0F62FE' }, + minimax: { name: 'minimax', displayName: 'MiniMax', category: 'ai-provider', keywords: ['minimax'], defaultColor: '#FF6B6B' }, + baidu: { name: 'baidu', displayName: 'Baidu', category: 'ai-provider', keywords: ['ernie', 'wenxin'], defaultColor: '#2932E1' }, + alibaba: { name: 'alibaba', displayName: 'Alibaba', category: 'ai-provider', keywords: ['qwen', 'tongyi'], defaultColor: '#FF6A00' }, + tencent: { name: 'tencent', displayName: 'Tencent', category: 'ai-provider', keywords: ['hunyuan'], defaultColor: '#00A4FF' }, + meta: { name: 'meta', displayName: 'Meta', category: 'ai-provider', keywords: ['facebook', 'llama'], defaultColor: '#0081FB' }, + microsoft: { name: 'microsoft', displayName: 'Microsoft', category: 'ai-provider', keywords: ['copilot', 'azure'], defaultColor: '#00A4EF' }, + cohere: { name: 'cohere', displayName: 'Cohere', category: 'ai-provider', keywords: ['cohere'], defaultColor: '#39594D' }, + perplexity: { name: 'perplexity', displayName: 'Perplexity', category: 'ai-provider', keywords: ['perplexity'], defaultColor: '#20808D' }, + mistral: { name: 'mistral', displayName: 'Mistral', category: 'ai-provider', keywords: ['mistral'], defaultColor: '#FF7000' }, + huggingface: { name: 'huggingface', displayName: 'Hugging Face', category: 'ai-provider', keywords: ['huggingface', 'hf'], defaultColor: '#FFD21E' }, + + // Cloud Platforms + aws: { name: 'aws', displayName: 'AWS', category: 'cloud', keywords: ['amazon', 'cloud'], defaultColor: '#FF9900' }, + azure: { name: 'azure', displayName: 'Azure', category: 'cloud', keywords: ['microsoft', 'cloud'], defaultColor: '#0078D4' }, + huawei: { name: 'huawei', displayName: 'Huawei', category: 'cloud', keywords: ['huawei', 'cloud'], defaultColor: '#FF0000' }, + cloudflare: { name: 'cloudflare', displayName: 'Cloudflare', category: 'cloud', keywords: ['cloudflare', 'cdn'], defaultColor: '#F38020' }, + + // Dev Tools + github: { name: 'github', displayName: 'GitHub', category: 'tool', keywords: ['git', 'version control'], defaultColor: '#181717' }, + gitlab: { name: 'gitlab', displayName: 'GitLab', category: 'tool', keywords: ['git', 'version control'], defaultColor: '#FC6D26' }, + docker: { name: 'docker', displayName: 'Docker', category: 'tool', keywords: ['container'], defaultColor: '#2496ED' }, + kubernetes: { name: 'kubernetes', displayName: 'Kubernetes', category: 'tool', keywords: ['k8s', 'container'], defaultColor: '#326CE5' }, + vscode: { name: 'vscode', displayName: 'VS Code', category: 'tool', keywords: ['editor', 'ide'], defaultColor: '#007ACC' }, + + // Others + settings: { name: 'settings', displayName: 'Settings', category: 'other', keywords: ['config', 'preferences'], defaultColor: '#6B7280' }, + folder: { name: 'folder', displayName: 'Folder', category: 'other', keywords: ['directory'], defaultColor: '#6B7280' }, + file: { name: 'file', displayName: 'File', category: 'other', keywords: ['document'], defaultColor: '#6B7280' }, + link: { name: 'link', displayName: 'Link', category: 'other', keywords: ['url', 'hyperlink'], defaultColor: '#6B7280' }, +}; + +export function getIconMetadata(name: string): IconMetadata | undefined { + return iconMetadata[name.toLowerCase()]; +} + +export function searchIcons(query: string): string[] { + const lowerQuery = query.toLowerCase(); + return Object.values(iconMetadata) + .filter(meta => + meta.name.includes(lowerQuery) || + meta.displayName.toLowerCase().includes(lowerQuery) || + meta.keywords.some(k => k.includes(lowerQuery)) + ) + .map(meta => meta.name); +} +`; + +fs.writeFileSync(path.join(OUTPUT_DIR, 'metadata.ts'), metadataContent); +console.log('✓ Generated: src/icons/extracted/metadata.ts'); + +// 生成 README +const readmeContent = `# Extracted Icons + +This directory contains extracted icons from @lobehub/icons-static-svg. + +## Statistics +- Total extracted: ${extracted} icons +- Not found: ${notFound.length} icons + +## Extracted Icons +${ALL_ICONS.filter(name => !notFound.includes(name)).map(name => `- ${name}`).join('\n')} + +${notFound.length > 0 ? `\n## Not Found\n${notFound.map(name => `- ${name}`).join('\n')}` : ''} + +## Usage + +\`\`\`typescript +import { getIcon, hasIcon, iconList } from './extracted'; + +// Get icon SVG +const svg = getIcon('openai'); + +// Check if icon exists +if (hasIcon('openai')) { + // ... +} + +// Get all available icons +console.log(iconList); +\`\`\` + +--- +Last updated: ${new Date().toISOString()} +Generated by: scripts/extract-icons.js +`; + +fs.writeFileSync(path.join(OUTPUT_DIR, 'README.md'), readmeContent); +console.log('✓ Generated: src/icons/extracted/README.md'); + +console.log('\n========================================'); +console.log('✅ Extraction complete!\n'); +console.log(` ✓ Extracted: ${extracted} icons`); +console.log(` ✗ Not found: ${notFound.length} icons`); +console.log(` 📉 Bundle size reduction: ~${Math.round((1 - extracted / 723) * 100)}%`); +console.log('========================================\n'); diff --git a/scripts/generate-icon-index.js b/scripts/generate-icon-index.js new file mode 100644 index 0000000..897a065 --- /dev/null +++ b/scripts/generate-icon-index.js @@ -0,0 +1,113 @@ +const fs = require('fs'); +const path = require('path'); + +const ICONS_DIR = path.join(__dirname, '../src/icons/extracted'); +const INDEX_FILE = path.join(ICONS_DIR, 'index.ts'); +const METADATA_FILE = path.join(ICONS_DIR, 'metadata.ts'); + +// Known metadata from previous configuration +const KNOWN_METADATA = { + openai: { name: 'openai', displayName: 'OpenAI', category: 'ai-provider', keywords: ['gpt', 'chatgpt'], defaultColor: '#00A67E' }, + anthropic: { name: 'anthropic', displayName: 'Anthropic', category: 'ai-provider', keywords: ['claude'], defaultColor: '#D4915D' }, + claude: { name: 'claude', displayName: 'Claude', category: 'ai-provider', keywords: ['anthropic'], defaultColor: '#D4915D' }, + google: { name: 'google', displayName: 'Google', category: 'ai-provider', keywords: ['gemini', 'bard'], defaultColor: '#4285F4' }, + gemini: { name: 'gemini', displayName: 'Gemini', category: 'ai-provider', keywords: ['google'], defaultColor: '#4285F4' }, + deepseek: { name: 'deepseek', displayName: 'DeepSeek', category: 'ai-provider', keywords: ['deep', 'seek'], defaultColor: '#1E88E5' }, + moonshot: { name: 'moonshot', displayName: 'Moonshot', category: 'ai-provider', keywords: ['kimi', 'moonshot'], defaultColor: '#6366F1' }, + kimi: { name: 'kimi', displayName: 'Kimi', category: 'ai-provider', keywords: ['moonshot'], defaultColor: '#6366F1' }, + zhipu: { name: 'zhipu', displayName: 'Zhipu AI', category: 'ai-provider', keywords: ['chatglm', 'glm'], defaultColor: '#0F62FE' }, + minimax: { name: 'minimax', displayName: 'MiniMax', category: 'ai-provider', keywords: ['minimax'], defaultColor: '#FF6B6B' }, + baidu: { name: 'baidu', displayName: 'Baidu', category: 'ai-provider', keywords: ['ernie', 'wenxin'], defaultColor: '#2932E1' }, + alibaba: { name: 'alibaba', displayName: 'Alibaba', category: 'ai-provider', keywords: ['qwen', 'tongyi'], defaultColor: '#FF6A00' }, + tencent: { name: 'tencent', displayName: 'Tencent', category: 'ai-provider', keywords: ['hunyuan'], defaultColor: '#00A4FF' }, + meta: { name: 'meta', displayName: 'Meta', category: 'ai-provider', keywords: ['facebook', 'llama'], defaultColor: '#0081FB' }, + microsoft: { name: 'microsoft', displayName: 'Microsoft', category: 'ai-provider', keywords: ['copilot', 'azure'], defaultColor: '#00A4EF' }, + cohere: { name: 'cohere', displayName: 'Cohere', category: 'ai-provider', keywords: ['cohere'], defaultColor: '#39594D' }, + perplexity: { name: 'perplexity', displayName: 'Perplexity', category: 'ai-provider', keywords: ['perplexity'], defaultColor: '#20808D' }, + mistral: { name: 'mistral', displayName: 'Mistral', category: 'ai-provider', keywords: ['mistral'], defaultColor: '#FF7000' }, + huggingface: { name: 'huggingface', displayName: 'Hugging Face', category: 'ai-provider', keywords: ['huggingface', 'hf'], defaultColor: '#FFD21E' }, + aws: { name: 'aws', displayName: 'AWS', category: 'cloud', keywords: ['amazon', 'cloud'], defaultColor: '#FF9900' }, + azure: { name: 'azure', displayName: 'Azure', category: 'cloud', keywords: ['microsoft', 'cloud'], defaultColor: '#0078D4' }, + huawei: { name: 'huawei', displayName: 'Huawei', category: 'cloud', keywords: ['huawei', 'cloud'], defaultColor: '#FF0000' }, + cloudflare: { name: 'cloudflare', displayName: 'Cloudflare', category: 'cloud', keywords: ['cloudflare', 'cdn'], defaultColor: '#F38020' }, + github: { name: 'github', displayName: 'GitHub', category: 'tool', keywords: ['git', 'version control'], defaultColor: '#181717' }, + gitlab: { name: 'gitlab', displayName: 'GitLab', category: 'tool', keywords: ['git', 'version control'], defaultColor: '#FC6D26' }, + docker: { name: 'docker', displayName: 'Docker', category: 'tool', keywords: ['container'], defaultColor: '#2496ED' }, + kubernetes: { name: 'kubernetes', displayName: 'Kubernetes', category: 'tool', keywords: ['k8s', 'container'], defaultColor: '#326CE5' }, + vscode: { name: 'vscode', displayName: 'VS Code', category: 'tool', keywords: ['editor', 'ide'], defaultColor: '#007ACC' }, + settings: { name: 'settings', displayName: 'Settings', category: 'other', keywords: ['config', 'preferences'], defaultColor: '#6B7280' }, + folder: { name: 'folder', displayName: 'Folder', category: 'other', keywords: ['directory'], defaultColor: '#6B7280' }, + file: { name: 'file', displayName: 'File', category: 'other', keywords: ['document'], defaultColor: '#6B7280' }, + link: { name: 'link', displayName: 'Link', category: 'other', keywords: ['url', 'hyperlink'], defaultColor: '#6B7280' }, +}; + +// Get all SVG files +const files = fs.readdirSync(ICONS_DIR).filter(file => file.endsWith('.svg')); + +console.log(`Found ${files.length} SVG files.`); + +// Generate index.ts +const indexContent = `// Auto-generated icon index +// Do not edit manually + +export const icons: Record = { +${files.map(file => { + const name = path.basename(file, '.svg'); + const svg = fs.readFileSync(path.join(ICONS_DIR, file), 'utf-8'); + const escaped = svg.replace(/`/g, '\\`').replace(/\$/g, '\\$'); + return ` '${name}': \`${escaped}\`,`; +}).join('\n')} +}; + +export const iconList = Object.keys(icons); + +export function getIcon(name: string): string { + return icons[name.toLowerCase()] || ''; +} + +export function hasIcon(name: string): boolean { + return name.toLowerCase() in icons; +} +`; + +fs.writeFileSync(INDEX_FILE, indexContent); +console.log(`Generated ${INDEX_FILE}`); + +// Generate metadata.ts +const metadataEntries = files.map(file => { + const name = path.basename(file, '.svg').toLowerCase(); + const known = KNOWN_METADATA[name]; + + if (known) { + return ` ${name}: ${JSON.stringify(known)},`; + } + + // Default metadata for unknown icons + return ` '${name}': { name: '${name}', displayName: '${name}', category: 'other', keywords: [], defaultColor: 'currentColor' },`; +}); + +const metadataContent = `// Icon metadata for search and categorization +import { IconMetadata } from '@/types/icon'; + +export const iconMetadata: Record = { +${metadataEntries.join('\n')} +}; + +export function getIconMetadata(name: string): IconMetadata | undefined { + return iconMetadata[name.toLowerCase()]; +} + +export function searchIcons(query: string): string[] { + const lowerQuery = query.toLowerCase(); + return Object.values(iconMetadata) + .filter(meta => + meta.name.includes(lowerQuery) || + meta.displayName.toLowerCase().includes(lowerQuery) || + meta.keywords.some(k => k.includes(lowerQuery)) + ) + .map(meta => meta.name); +} +`; + +fs.writeFileSync(METADATA_FILE, metadataContent); +console.log(`Generated ${METADATA_FILE}`); diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx new file mode 100644 index 0000000..9948c6d --- /dev/null +++ b/src/components/ColorPicker.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; + +interface ColorPickerProps { + value?: string; + onValueChange: (color: string) => void; + label?: string; + presets?: string[]; +} + +const DEFAULT_PRESETS = [ + "#00A67E", + "#D4915D", + "#4285F4", + "#FF6A00", + "#00A4FF", + "#FF9900", + "#0078D4", + "#FF0000", + "#1E88E5", + "#6366F1", + "#0F62FE", + "#2932E1", +]; + +export const ColorPicker: React.FC = ({ + value = "#4285F4", + onValueChange, + label = "图标颜色", + presets = DEFAULT_PRESETS, +}) => { + return ( +
+ + + {/* 颜色预设 */} +
+ {presets.map((color) => ( +
+ + {/* 自定义颜色输入 */} +
+ onValueChange(e.target.value)} + className="w-16 h-10 p-1 cursor-pointer" + /> + onValueChange(e.target.value)} + placeholder="#4285F4" + className="flex-1 font-mono" + /> +
+
+ ); +}; diff --git a/src/components/IconPicker.tsx b/src/components/IconPicker.tsx new file mode 100644 index 0000000..6c8f5f8 --- /dev/null +++ b/src/components/IconPicker.tsx @@ -0,0 +1,85 @@ +import React, { useState, useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { ProviderIcon } from "./ProviderIcon"; +import { iconList } from "@/icons/extracted"; +import { searchIcons, getIconMetadata } from "@/icons/extracted/metadata"; +import { cn } from "@/lib/utils"; + +interface IconPickerProps { + value?: string; // 当前选中的图标 + onValueChange: (icon: string) => void; // 选择回调 + color?: string; // 预览颜色 +} + +export const IconPicker: React.FC = ({ + value, + onValueChange, +}) => { + const { t } = useTranslation(); + const [searchQuery, setSearchQuery] = useState(""); + + // 过滤图标列表 + const filteredIcons = useMemo(() => { + if (!searchQuery) return iconList; + return searchIcons(searchQuery); + }, [searchQuery]); + + return ( +
+
+ + setSearchQuery(e.target.value)} + className="mt-2" + /> +
+ +
+
+ {filteredIcons.map((iconName) => { + const meta = getIconMetadata(iconName); + const isSelected = value === iconName; + + return ( + + ); + })} +
+
+ + {filteredIcons.length === 0 && ( +
+ {t("iconPicker.noResults", { defaultValue: "未找到匹配的图标" })} +
+ )} +
+ ); +}; diff --git a/src/components/ProviderIcon.tsx b/src/components/ProviderIcon.tsx new file mode 100644 index 0000000..788efb0 --- /dev/null +++ b/src/components/ProviderIcon.tsx @@ -0,0 +1,81 @@ +import React, { useMemo } from "react"; +import { getIcon, hasIcon } from "@/icons/extracted"; +import { cn } from "@/lib/utils"; + +interface ProviderIconProps { + icon?: string; // 图标名称 + name: string; // 供应商名称(用于 fallback) + color?: string; // 自定义颜色 (Deprecated, kept for compatibility but ignored for SVG) + size?: number | string; // 尺寸 + className?: string; + showFallback?: boolean; // 是否显示 fallback +} + +export const ProviderIcon: React.FC = ({ + icon, + name, + size = 32, + className, + showFallback = true, +}) => { + // 获取图标 SVG + const iconSvg = useMemo(() => { + if (icon && hasIcon(icon)) { + return getIcon(icon); + } + return ""; + }, [icon]); + + // 计算尺寸样式 + const sizeStyle = useMemo(() => { + const sizeValue = typeof size === "number" ? `${size}px` : size; + return { + width: sizeValue, + height: sizeValue, + }; + }, [size]); + + // 如果有图标,显示图标 + if (iconSvg) { + return ( + + ); + } + + // Fallback:显示首字母 + if (showFallback) { + const initials = name + .split(" ") + .map((word) => word[0]) + .join("") + .toUpperCase() + .slice(0, 2); + return ( + + + {initials} + + + ); + } + + return null; +}; diff --git a/src/icons/extracted/alibaba.svg b/src/icons/extracted/alibaba.svg new file mode 100644 index 0000000..bb458d7 --- /dev/null +++ b/src/icons/extracted/alibaba.svg @@ -0,0 +1 @@ +Alibaba \ No newline at end of file diff --git a/src/icons/extracted/anthropic.svg b/src/icons/extracted/anthropic.svg new file mode 100644 index 0000000..5b81844 --- /dev/null +++ b/src/icons/extracted/anthropic.svg @@ -0,0 +1 @@ +Anthropic \ No newline at end of file diff --git a/src/icons/extracted/aws.svg b/src/icons/extracted/aws.svg new file mode 100644 index 0000000..495b475 --- /dev/null +++ b/src/icons/extracted/aws.svg @@ -0,0 +1 @@ +AWS \ No newline at end of file diff --git a/src/icons/extracted/azure.svg b/src/icons/extracted/azure.svg new file mode 100644 index 0000000..ed50209 --- /dev/null +++ b/src/icons/extracted/azure.svg @@ -0,0 +1 @@ +Azure \ No newline at end of file diff --git a/src/icons/extracted/baidu.svg b/src/icons/extracted/baidu.svg new file mode 100644 index 0000000..ead7f89 --- /dev/null +++ b/src/icons/extracted/baidu.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/src/icons/extracted/bytedance.svg b/src/icons/extracted/bytedance.svg new file mode 100644 index 0000000..6921097 --- /dev/null +++ b/src/icons/extracted/bytedance.svg @@ -0,0 +1 @@ +ByteDance \ No newline at end of file diff --git a/src/icons/extracted/chatglm.svg b/src/icons/extracted/chatglm.svg new file mode 100644 index 0000000..681c04a --- /dev/null +++ b/src/icons/extracted/chatglm.svg @@ -0,0 +1 @@ +ChatGLM \ No newline at end of file diff --git a/src/icons/extracted/claude.svg b/src/icons/extracted/claude.svg new file mode 100644 index 0000000..62dc0db --- /dev/null +++ b/src/icons/extracted/claude.svg @@ -0,0 +1 @@ +Claude \ No newline at end of file diff --git a/src/icons/extracted/cloudflare.svg b/src/icons/extracted/cloudflare.svg new file mode 100644 index 0000000..d555b6f --- /dev/null +++ b/src/icons/extracted/cloudflare.svg @@ -0,0 +1 @@ +Cloudflare \ No newline at end of file diff --git a/src/icons/extracted/cohere.svg b/src/icons/extracted/cohere.svg new file mode 100644 index 0000000..94bcb82 --- /dev/null +++ b/src/icons/extracted/cohere.svg @@ -0,0 +1 @@ +Cohere \ No newline at end of file diff --git a/src/icons/extracted/copilot.svg b/src/icons/extracted/copilot.svg new file mode 100644 index 0000000..4f4031a --- /dev/null +++ b/src/icons/extracted/copilot.svg @@ -0,0 +1 @@ +Copilot \ No newline at end of file diff --git a/src/icons/extracted/deepseek.svg b/src/icons/extracted/deepseek.svg new file mode 100644 index 0000000..3fc2302 --- /dev/null +++ b/src/icons/extracted/deepseek.svg @@ -0,0 +1 @@ +DeepSeek \ No newline at end of file diff --git a/src/icons/extracted/doubao.svg b/src/icons/extracted/doubao.svg new file mode 100644 index 0000000..e251145 --- /dev/null +++ b/src/icons/extracted/doubao.svg @@ -0,0 +1 @@ +Doubao \ No newline at end of file diff --git a/src/icons/extracted/gemini.svg b/src/icons/extracted/gemini.svg new file mode 100644 index 0000000..f1cf357 --- /dev/null +++ b/src/icons/extracted/gemini.svg @@ -0,0 +1 @@ +Gemini \ No newline at end of file diff --git a/src/icons/extracted/gemma.svg b/src/icons/extracted/gemma.svg new file mode 100644 index 0000000..ed81051 --- /dev/null +++ b/src/icons/extracted/gemma.svg @@ -0,0 +1 @@ +Gemma \ No newline at end of file diff --git a/src/icons/extracted/github.svg b/src/icons/extracted/github.svg new file mode 100644 index 0000000..7a51b8e --- /dev/null +++ b/src/icons/extracted/github.svg @@ -0,0 +1 @@ +Github \ No newline at end of file diff --git a/src/icons/extracted/githubcopilot.svg b/src/icons/extracted/githubcopilot.svg new file mode 100644 index 0000000..3cbf22a --- /dev/null +++ b/src/icons/extracted/githubcopilot.svg @@ -0,0 +1 @@ +GithubCopilot \ No newline at end of file diff --git a/src/icons/extracted/google.svg b/src/icons/extracted/google.svg new file mode 100644 index 0000000..e8e0f86 --- /dev/null +++ b/src/icons/extracted/google.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/src/icons/extracted/googlecloud.svg b/src/icons/extracted/googlecloud.svg new file mode 100644 index 0000000..80def4a --- /dev/null +++ b/src/icons/extracted/googlecloud.svg @@ -0,0 +1 @@ +GoogleCloud \ No newline at end of file diff --git a/src/icons/extracted/grok.svg b/src/icons/extracted/grok.svg new file mode 100644 index 0000000..efb1a61 --- /dev/null +++ b/src/icons/extracted/grok.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/src/icons/extracted/huawei.svg b/src/icons/extracted/huawei.svg new file mode 100644 index 0000000..d55df45 --- /dev/null +++ b/src/icons/extracted/huawei.svg @@ -0,0 +1 @@ +Huawei \ No newline at end of file diff --git a/src/icons/extracted/huggingface.svg b/src/icons/extracted/huggingface.svg new file mode 100644 index 0000000..dc1cf3f --- /dev/null +++ b/src/icons/extracted/huggingface.svg @@ -0,0 +1 @@ +HuggingFace \ No newline at end of file diff --git a/src/icons/extracted/hunyuan.svg b/src/icons/extracted/hunyuan.svg new file mode 100644 index 0000000..42edd6c --- /dev/null +++ b/src/icons/extracted/hunyuan.svg @@ -0,0 +1 @@ +Hunyuan \ No newline at end of file diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts new file mode 100644 index 0000000..b79e304 --- /dev/null +++ b/src/icons/extracted/index.ts @@ -0,0 +1,57 @@ +// Auto-generated icon index +// Do not edit manually + +export const icons: Record = { + alibaba: `Alibaba`, + anthropic: `Anthropic`, + aws: `AWS`, + azure: `Azure`, + baidu: `Baidu`, + bytedance: `ByteDance`, + chatglm: `ChatGLM`, + claude: `Claude`, + cloudflare: `Cloudflare`, + cohere: `Cohere`, + copilot: `Copilot`, + deepseek: `DeepSeek`, + doubao: `Doubao`, + gemini: `Gemini`, + gemma: `Gemma`, + github: `Github`, + githubcopilot: `GithubCopilot`, + google: `Google`, + googlecloud: `GoogleCloud`, + grok: `Grok`, + huawei: `Huawei`, + huggingface: `HuggingFace`, + hunyuan: `Hunyuan`, + kimi: `Kimi`, + meta: `Meta`, + midjourney: `Midjourney`, + minimax: `Minimax`, + mistral: `Mistral`, + notion: `Notion`, + ollama: `Ollama`, + openai: `OpenAI`, + palm: `PaLM`, + perplexity: `Perplexity`, + qwen: `Qwen`, + stability: `Stability`, + tencent: `Tencent`, + vercel: `Vercel`, + wenxin: `Wenxin`, + xai: `Grok`, + yi: `Yi`, + zeroone: `01.AI`, + zhipu: `Zhipu`, +}; + +export const iconList = Object.keys(icons); + +export function getIcon(name: string): string { + return icons[name.toLowerCase()] || ""; +} + +export function hasIcon(name: string): boolean { + return name.toLowerCase() in icons; +} diff --git a/src/icons/extracted/kimi.svg b/src/icons/extracted/kimi.svg new file mode 100644 index 0000000..ec5db53 --- /dev/null +++ b/src/icons/extracted/kimi.svg @@ -0,0 +1 @@ +Kimi \ No newline at end of file diff --git a/src/icons/extracted/meta.svg b/src/icons/extracted/meta.svg new file mode 100644 index 0000000..01fed4c --- /dev/null +++ b/src/icons/extracted/meta.svg @@ -0,0 +1 @@ +Meta \ No newline at end of file diff --git a/src/icons/extracted/metadata.ts b/src/icons/extracted/metadata.ts new file mode 100644 index 0000000..7fe3bc6 --- /dev/null +++ b/src/icons/extracted/metadata.ts @@ -0,0 +1,315 @@ +// Icon metadata for search and categorization +import { IconMetadata } from "@/types/icon"; + +export const iconMetadata: Record = { + alibaba: { + name: "alibaba", + displayName: "Alibaba", + category: "ai-provider", + keywords: ["qwen", "tongyi"], + defaultColor: "#FF6A00", + }, + anthropic: { + name: "anthropic", + displayName: "Anthropic", + category: "ai-provider", + keywords: ["claude"], + defaultColor: "#D4915D", + }, + aws: { + name: "aws", + displayName: "AWS", + category: "cloud", + keywords: ["amazon", "cloud"], + defaultColor: "#FF9900", + }, + azure: { + name: "azure", + displayName: "Azure", + category: "cloud", + keywords: ["microsoft", "cloud"], + defaultColor: "#0078D4", + }, + baidu: { + name: "baidu", + displayName: "Baidu", + category: "ai-provider", + keywords: ["ernie", "wenxin"], + defaultColor: "#2932E1", + }, + bytedance: { + name: "bytedance", + displayName: "bytedance", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + chatglm: { + name: "chatglm", + displayName: "chatglm", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + claude: { + name: "claude", + displayName: "Claude", + category: "ai-provider", + keywords: ["anthropic"], + defaultColor: "#D4915D", + }, + cloudflare: { + name: "cloudflare", + displayName: "Cloudflare", + category: "cloud", + keywords: ["cloudflare", "cdn"], + defaultColor: "#F38020", + }, + cohere: { + name: "cohere", + displayName: "Cohere", + category: "ai-provider", + keywords: ["cohere"], + defaultColor: "#39594D", + }, + copilot: { + name: "copilot", + displayName: "copilot", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + deepseek: { + name: "deepseek", + displayName: "DeepSeek", + category: "ai-provider", + keywords: ["deep", "seek"], + defaultColor: "#1E88E5", + }, + doubao: { + name: "doubao", + displayName: "doubao", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + gemini: { + name: "gemini", + displayName: "Gemini", + category: "ai-provider", + keywords: ["google"], + defaultColor: "#4285F4", + }, + gemma: { + name: "gemma", + displayName: "gemma", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + github: { + name: "github", + displayName: "GitHub", + category: "tool", + keywords: ["git", "version control"], + defaultColor: "#181717", + }, + githubcopilot: { + name: "githubcopilot", + displayName: "githubcopilot", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + google: { + name: "google", + displayName: "Google", + category: "ai-provider", + keywords: ["gemini", "bard"], + defaultColor: "#4285F4", + }, + googlecloud: { + name: "googlecloud", + displayName: "googlecloud", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + grok: { + name: "grok", + displayName: "grok", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + huawei: { + name: "huawei", + displayName: "Huawei", + category: "cloud", + keywords: ["huawei", "cloud"], + defaultColor: "#FF0000", + }, + huggingface: { + name: "huggingface", + displayName: "Hugging Face", + category: "ai-provider", + keywords: ["huggingface", "hf"], + defaultColor: "#FFD21E", + }, + hunyuan: { + name: "hunyuan", + displayName: "hunyuan", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + kimi: { + name: "kimi", + displayName: "Kimi", + category: "ai-provider", + keywords: ["moonshot"], + defaultColor: "#6366F1", + }, + meta: { + name: "meta", + displayName: "Meta", + category: "ai-provider", + keywords: ["facebook", "llama"], + defaultColor: "#0081FB", + }, + midjourney: { + name: "midjourney", + displayName: "midjourney", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + minimax: { + name: "minimax", + displayName: "MiniMax", + category: "ai-provider", + keywords: ["minimax"], + defaultColor: "#FF6B6B", + }, + mistral: { + name: "mistral", + displayName: "Mistral", + category: "ai-provider", + keywords: ["mistral"], + defaultColor: "#FF7000", + }, + notion: { + name: "notion", + displayName: "notion", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + ollama: { + name: "ollama", + displayName: "ollama", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + openai: { + name: "openai", + displayName: "OpenAI", + category: "ai-provider", + keywords: ["gpt", "chatgpt"], + defaultColor: "#00A67E", + }, + palm: { + name: "palm", + displayName: "palm", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + perplexity: { + name: "perplexity", + displayName: "Perplexity", + category: "ai-provider", + keywords: ["perplexity"], + defaultColor: "#20808D", + }, + qwen: { + name: "qwen", + displayName: "qwen", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + stability: { + name: "stability", + displayName: "stability", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + tencent: { + name: "tencent", + displayName: "Tencent", + category: "ai-provider", + keywords: ["hunyuan"], + defaultColor: "#00A4FF", + }, + vercel: { + name: "vercel", + displayName: "vercel", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + wenxin: { + name: "wenxin", + displayName: "wenxin", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + xai: { + name: "xai", + displayName: "xai", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + yi: { + name: "yi", + displayName: "yi", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + zeroone: { + name: "zeroone", + displayName: "zeroone", + category: "other", + keywords: [], + defaultColor: "currentColor", + }, + zhipu: { + name: "zhipu", + displayName: "Zhipu AI", + category: "ai-provider", + keywords: ["chatglm", "glm"], + defaultColor: "#0F62FE", + }, +}; + +export function getIconMetadata(name: string): IconMetadata | undefined { + return iconMetadata[name.toLowerCase()]; +} + +export function searchIcons(query: string): string[] { + const lowerQuery = query.toLowerCase(); + return Object.values(iconMetadata) + .filter( + (meta) => + meta.name.includes(lowerQuery) || + meta.displayName.toLowerCase().includes(lowerQuery) || + meta.keywords.some((k) => k.includes(lowerQuery)), + ) + .map((meta) => meta.name); +} diff --git a/src/icons/extracted/midjourney.svg b/src/icons/extracted/midjourney.svg new file mode 100644 index 0000000..e59e3b0 --- /dev/null +++ b/src/icons/extracted/midjourney.svg @@ -0,0 +1 @@ +Midjourney \ No newline at end of file diff --git a/src/icons/extracted/minimax.svg b/src/icons/extracted/minimax.svg new file mode 100644 index 0000000..2a60bd4 --- /dev/null +++ b/src/icons/extracted/minimax.svg @@ -0,0 +1 @@ +Minimax \ No newline at end of file diff --git a/src/icons/extracted/mistral.svg b/src/icons/extracted/mistral.svg new file mode 100644 index 0000000..8e03e24 --- /dev/null +++ b/src/icons/extracted/mistral.svg @@ -0,0 +1 @@ +Mistral \ No newline at end of file diff --git a/src/icons/extracted/notion.svg b/src/icons/extracted/notion.svg new file mode 100644 index 0000000..e92465e --- /dev/null +++ b/src/icons/extracted/notion.svg @@ -0,0 +1 @@ +Notion \ No newline at end of file diff --git a/src/icons/extracted/ollama.svg b/src/icons/extracted/ollama.svg new file mode 100644 index 0000000..cc887e3 --- /dev/null +++ b/src/icons/extracted/ollama.svg @@ -0,0 +1 @@ +Ollama \ No newline at end of file diff --git a/src/icons/extracted/openai.svg b/src/icons/extracted/openai.svg new file mode 100644 index 0000000..50d94d6 --- /dev/null +++ b/src/icons/extracted/openai.svg @@ -0,0 +1 @@ +OpenAI \ No newline at end of file diff --git a/src/icons/extracted/palm.svg b/src/icons/extracted/palm.svg new file mode 100644 index 0000000..8e6af5b --- /dev/null +++ b/src/icons/extracted/palm.svg @@ -0,0 +1 @@ +PaLM \ No newline at end of file diff --git a/src/icons/extracted/perplexity.svg b/src/icons/extracted/perplexity.svg new file mode 100644 index 0000000..5f5a5ab --- /dev/null +++ b/src/icons/extracted/perplexity.svg @@ -0,0 +1 @@ +Perplexity \ No newline at end of file diff --git a/src/icons/extracted/qwen.svg b/src/icons/extracted/qwen.svg new file mode 100644 index 0000000..33b3f64 --- /dev/null +++ b/src/icons/extracted/qwen.svg @@ -0,0 +1 @@ +Qwen \ No newline at end of file diff --git a/src/icons/extracted/stability.svg b/src/icons/extracted/stability.svg new file mode 100644 index 0000000..eb70e98 --- /dev/null +++ b/src/icons/extracted/stability.svg @@ -0,0 +1 @@ +Stability \ No newline at end of file diff --git a/src/icons/extracted/tencent.svg b/src/icons/extracted/tencent.svg new file mode 100644 index 0000000..98da272 --- /dev/null +++ b/src/icons/extracted/tencent.svg @@ -0,0 +1 @@ +Tencent \ No newline at end of file diff --git a/src/icons/extracted/vercel.svg b/src/icons/extracted/vercel.svg new file mode 100644 index 0000000..486cb95 --- /dev/null +++ b/src/icons/extracted/vercel.svg @@ -0,0 +1 @@ +Vercel \ No newline at end of file diff --git a/src/icons/extracted/wenxin.svg b/src/icons/extracted/wenxin.svg new file mode 100644 index 0000000..563e833 --- /dev/null +++ b/src/icons/extracted/wenxin.svg @@ -0,0 +1 @@ +Wenxin \ No newline at end of file diff --git a/src/icons/extracted/xai.svg b/src/icons/extracted/xai.svg new file mode 100644 index 0000000..536e713 --- /dev/null +++ b/src/icons/extracted/xai.svg @@ -0,0 +1 @@ +Grok \ No newline at end of file diff --git a/src/icons/extracted/yi.svg b/src/icons/extracted/yi.svg new file mode 100644 index 0000000..8d0c647 --- /dev/null +++ b/src/icons/extracted/yi.svg @@ -0,0 +1 @@ +Yi \ No newline at end of file diff --git a/src/icons/extracted/zeroone.svg b/src/icons/extracted/zeroone.svg new file mode 100644 index 0000000..ab67ec5 --- /dev/null +++ b/src/icons/extracted/zeroone.svg @@ -0,0 +1 @@ +01.AI \ No newline at end of file diff --git a/src/icons/extracted/zhipu.svg b/src/icons/extracted/zhipu.svg new file mode 100644 index 0000000..0c6e61c --- /dev/null +++ b/src/icons/extracted/zhipu.svg @@ -0,0 +1 @@ +Zhipu \ No newline at end of file