diff --git a/app/shared/src/ui/CodeBlock.tsx b/app/shared/src/ui/CodeBlock.tsx index 87f6bae15..a4981651e 100644 --- a/app/shared/src/ui/CodeBlock.tsx +++ b/app/shared/src/ui/CodeBlock.tsx @@ -1,6 +1,9 @@ import React, { Suspense, lazy, useState, type CSSProperties } from 'react'; import { Check, Copy } from 'lucide-react'; import { useTranslation } from 'react-i18next'; +// Type-only: pulls in the package's ambient module declarations, which is what +// makes the deep entries below typecheck. Erased at build time. +import type { SyntaxHighlighterProps } from 'react-syntax-highlighter'; import { CHATVIEW_I18N_NAMESPACE } from '../chatview/i18n/resources'; import { useCopiedFlag } from './useCopiedFlag'; import styles from './Markdown.module.css'; @@ -30,10 +33,16 @@ interface HighlighterProps { const LazyHighlighter = lazy(async (): Promise<{ default: React.FC }> => { await import('./prismRegistry'); - const [{ PrismLight: SyntaxHighlighter }, { oneDark }] = await Promise.all([ - import('react-syntax-highlighter'), - import('react-syntax-highlighter/dist/esm/styles/prism'), + // Deep entries on purpose: the package barrel also re-exports the highlight.js + // build, and the prism styles barrel carries all ~200 theme files. + const [highlighterModule, styleModule] = await Promise.all([ + import('react-syntax-highlighter/dist/esm/prism-light'), + import('react-syntax-highlighter/dist/esm/styles/prism/one-dark'), ]); + const SyntaxHighlighter: React.ComponentType = highlighterModule.default; + // The ambient declaration exposes `oneDark` as a named export, but the ESM + // file itself only has a default export. + const oneDark = (styleModule as unknown as { default: Record }).default; const Highlighter: React.FC = ({ language, code, customStyle }) => ( { }); function mkColorIcon(tag: string) { - const Icon = ({ size }: { size?: number }) => ; - Icon.Color = Icon; - return { default: Icon }; + return { + default: ({ size }: { size?: number }) => ( + + ), + }; } function mkRuntimeIcon(testid: string) { return { default: ({ size }: { size?: number }) => }; } -vi.mock('@lobehub/icons/es/Alibaba', () => mkColorIcon('alibaba')); -vi.mock('@lobehub/icons/es/AlibabaCloud', () => mkColorIcon('alibabacloud')); -vi.mock('@lobehub/icons/es/Anthropic', () => mkColorIcon('anthropic')); -vi.mock('@lobehub/icons/es/Azure', () => mkColorIcon('azure')); -vi.mock('@lobehub/icons/es/Aws', () => mkColorIcon('aws')); -vi.mock('@lobehub/icons/es/Bedrock', () => mkColorIcon('bedrock')); -vi.mock('@lobehub/icons/es/ByteDance', () => mkColorIcon('bytedance')); -vi.mock('@lobehub/icons/es/Claude', () => mkColorIcon('claude')); +vi.mock('@lobehub/icons/es/Alibaba/components/Color.js', () => mkColorIcon('alibaba')); +vi.mock('@lobehub/icons/es/AlibabaCloud/components/Color.js', () => mkColorIcon('alibabacloud')); +vi.mock('@lobehub/icons/es/Azure/components/Color.js', () => mkColorIcon('azure')); +vi.mock('@lobehub/icons/es/Aws/components/Color.js', () => mkColorIcon('aws')); +vi.mock('@lobehub/icons/es/Bedrock/components/Color.js', () => mkColorIcon('bedrock')); +vi.mock('@lobehub/icons/es/ByteDance/components/Color.js', () => mkColorIcon('bytedance')); +vi.mock('@lobehub/icons/es/Claude/components/Color.js', () => mkColorIcon('claude')); vi.mock('@lobehub/icons/es/ClaudeCode', () => mkRuntimeIcon('claude-code-icon')); vi.mock('@lobehub/icons/es/Codex', () => mkRuntimeIcon('codex-icon')); -vi.mock('@lobehub/icons/es/Cohere', () => mkColorIcon('cohere')); -vi.mock('@lobehub/icons/es/DeepSeek', () => mkColorIcon('deepseek')); -vi.mock('@lobehub/icons/es/Doubao', () => mkColorIcon('doubao')); -vi.mock('@lobehub/icons/es/Gemini', () => mkColorIcon('gemini')); +vi.mock('@lobehub/icons/es/Cohere/components/Color.js', () => mkColorIcon('cohere')); +vi.mock('@lobehub/icons/es/DeepSeek/components/Color.js', () => mkColorIcon('deepseek')); +vi.mock('@lobehub/icons/es/Doubao/components/Color.js', () => mkColorIcon('doubao')); +vi.mock('@lobehub/icons/es/Gemini/components/Color.js', () => mkColorIcon('gemini')); vi.mock('@lobehub/icons/es/GeminiCLI', () => mkRuntimeIcon('gemini-cli-icon')); -vi.mock('@lobehub/icons/es/Google', () => mkColorIcon('google')); -vi.mock('@lobehub/icons/es/Meta', () => mkColorIcon('meta')); -vi.mock('@lobehub/icons/es/Mistral', () => mkColorIcon('mistral')); -vi.mock('@lobehub/icons/es/Moonshot', () => mkColorIcon('moonshot')); -vi.mock('@lobehub/icons/es/OpenAI', () => mkColorIcon('openai')); +vi.mock('@lobehub/icons/es/Google/components/Color.js', () => mkColorIcon('google')); +vi.mock('@lobehub/icons/es/Meta/components/Color.js', () => mkColorIcon('meta')); +vi.mock('@lobehub/icons/es/Mistral/components/Color.js', () => mkColorIcon('mistral')); vi.mock('@lobehub/icons/es/OpenCode', () => mkRuntimeIcon('opencode-icon')); -vi.mock('@lobehub/icons/es/Perplexity', () => mkColorIcon('perplexity')); -vi.mock('@lobehub/icons/es/Qwen', () => mkColorIcon('qwen')); -vi.mock('@lobehub/icons/es/Volcengine', () => mkColorIcon('volcengine')); -vi.mock('@lobehub/icons/es/Zhipu', () => mkColorIcon('zhipu')); +vi.mock('@lobehub/icons/es/Perplexity/components/Color.js', () => mkColorIcon('perplexity')); +vi.mock('@lobehub/icons/es/Qwen/components/Color.js', () => mkColorIcon('qwen')); +vi.mock('@lobehub/icons/es/Volcengine/components/Color.js', () => mkColorIcon('volcengine')); +vi.mock('@lobehub/icons/es/Zhipu/components/Color.js', () => mkColorIcon('zhipu')); vi.mock('@lobehub/icons/es/Antigravity/components/Color.js', () => ({ default: () => null })); describe('RuntimeIcon', () => { @@ -166,7 +165,9 @@ describe('RuntimeIcon', () => { expect(screen.getByTestId('codex-icon')).toBeInTheDocument(); expect(screen.getByTestId('claude-code-icon')).toHaveAttribute('data-size', '24'); expect(screen.getByTestId('opencode-icon')).toBeInTheDocument(); - expect(screen.getByTestId('openai-color-icon')).toHaveAttribute('data-size', '16'); + // OpenAI ships no colour mark, so it renders the deterministic fallback glyph. + expect(screen.queryByTestId('openai-color-icon')).toBeNull(); + expect(screen.getByLabelText('OpenAI').querySelector('svg')).not.toBeNull(); expect(screen.getByTestId('claude-color-icon')).toBeInTheDocument(); expect(screen.getByLabelText('Claude')).toHaveAttribute('data-runtime-icon-value', 'claude'); }); diff --git a/app/shared/src/ui/RuntimeIcon.tsx b/app/shared/src/ui/RuntimeIcon.tsx index 071939e73..b836635a1 100644 --- a/app/shared/src/ui/RuntimeIcon.tsx +++ b/app/shared/src/ui/RuntimeIcon.tsx @@ -1,31 +1,30 @@ import React, { type ComponentType } from 'react'; -// Subpath imports — avoids @lobehub/icons barrel (index.d.ts re-exports all 302 icons -// which fails under certain tsconfig contexts like web's moduleResolution + paths setup) +// Provider marks come from the Color variant directly: the per-provider index +// module also re-exports Mono/Text/Avatar/Combine, which shipped ~5x the SVG +// payload for marks this file only ever renders in colour. +import Alibaba from '@lobehub/icons/es/Alibaba/components/Color.js'; +import AlibabaCloud from '@lobehub/icons/es/AlibabaCloud/components/Color.js'; +import Azure from '@lobehub/icons/es/Azure/components/Color.js'; +import Aws from '@lobehub/icons/es/Aws/components/Color.js'; +import Bedrock from '@lobehub/icons/es/Bedrock/components/Color.js'; +import ByteDance from '@lobehub/icons/es/ByteDance/components/Color.js'; +import Claude from '@lobehub/icons/es/Claude/components/Color.js'; +import Cohere from '@lobehub/icons/es/Cohere/components/Color.js'; +import DeepSeek from '@lobehub/icons/es/DeepSeek/components/Color.js'; +import Doubao from '@lobehub/icons/es/Doubao/components/Color.js'; +import Gemini from '@lobehub/icons/es/Gemini/components/Color.js'; +import Google from '@lobehub/icons/es/Google/components/Color.js'; +import Meta from '@lobehub/icons/es/Meta/components/Color.js'; +import Mistral from '@lobehub/icons/es/Mistral/components/Color.js'; +import Perplexity from '@lobehub/icons/es/Perplexity/components/Color.js'; +import Qwen from '@lobehub/icons/es/Qwen/components/Color.js'; +import Volcengine from '@lobehub/icons/es/Volcengine/components/Color.js'; +import Zhipu from '@lobehub/icons/es/Zhipu/components/Color.js'; +// Runtime marks keep their index default (Mono) — that is what the runtime row renders. import ClaudeCode from '@lobehub/icons/es/ClaudeCode'; import Codex from '@lobehub/icons/es/Codex'; import GeminiCLI from '@lobehub/icons/es/GeminiCLI'; import OpenCode from '@lobehub/icons/es/OpenCode'; -import Alibaba from '@lobehub/icons/es/Alibaba'; -import AlibabaCloud from '@lobehub/icons/es/AlibabaCloud'; -import Anthropic from '@lobehub/icons/es/Anthropic'; -import Azure from '@lobehub/icons/es/Azure'; -import Aws from '@lobehub/icons/es/Aws'; -import Bedrock from '@lobehub/icons/es/Bedrock'; -import ByteDance from '@lobehub/icons/es/ByteDance'; -import Claude from '@lobehub/icons/es/Claude'; -import Cohere from '@lobehub/icons/es/Cohere'; -import DeepSeek from '@lobehub/icons/es/DeepSeek'; -import Doubao from '@lobehub/icons/es/Doubao'; -import Gemini from '@lobehub/icons/es/Gemini'; -import Google from '@lobehub/icons/es/Google'; -import Meta from '@lobehub/icons/es/Meta'; -import Mistral from '@lobehub/icons/es/Mistral'; -import Moonshot from '@lobehub/icons/es/Moonshot'; -import OpenAI from '@lobehub/icons/es/OpenAI'; -import Perplexity from '@lobehub/icons/es/Perplexity'; -import Qwen from '@lobehub/icons/es/Qwen'; -import Volcengine from '@lobehub/icons/es/Volcengine'; -import Zhipu from '@lobehub/icons/es/Zhipu'; import { resolveRuntimeIconRegistry } from './runtimeIconRegistry'; export type RuntimeIconKind = 'model' | 'provider' | 'runtime' | 'tool'; @@ -123,14 +122,11 @@ export function RuntimeIcon({ // lobehub icon component with a .Color subcomponent. // ═══════════════════════════════════════════════════════════════════════ -type LobeIcon = ComponentType<{ size?: number }> & { - Color?: ComponentType<{ size?: number }>; -}; +type LobeIcon = ComponentType<{ size?: number }>; const providerIconMap: Record = { alibaba: Alibaba, alibabacloud: AlibabaCloud, - anthropic: Anthropic, azure: Azure, aws: Aws, bedrock: Bedrock, @@ -143,8 +139,6 @@ const providerIconMap: Record = { google: Google, meta: Meta, mistral: Mistral, - moonshot: Moonshot, - openai: OpenAI, perplexity: Perplexity, qwen: Qwen, volcengine: Volcengine, @@ -162,12 +156,7 @@ const modelIconMap: Record = { doubao: Doubao, gemini: Gemini, glm: Zhipu, // ChatGLM → Zhipu icon - gpt: OpenAI, - kimi: Moonshot, qwen: Qwen, - 'o1': OpenAI, - 'o3': OpenAI, - 'o4': OpenAI, }; function resolveModelIcon(modelName: string): LobeIcon | undefined { @@ -182,16 +171,12 @@ function resolveModelIcon(modelName: string): LobeIcon | undefined { function renderProviderColorIcon(provider: string, size: number): React.ReactNode { const Icon = providerIconMap[provider.toLowerCase()]; - const Color = Icon?.Color; - if (Color) return ; - return runtimeIconFallbackSvg('provider', size); + return Icon ? : runtimeIconFallbackSvg('provider', size); } function renderModelColorIcon(model: string, size: number): React.ReactNode { const Icon = resolveModelIcon(model); - const Color = Icon?.Color; - if (Color) return ; - return runtimeIconFallbackSvg('model', size); + return Icon ? : runtimeIconFallbackSvg('model', size); } function renderRuntimeIcon(resolution: RuntimeIconResolution, iconSize: number): React.ReactNode {