Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions app/shared/src/ui/CodeBlock.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -30,10 +33,16 @@ interface HighlighterProps {

const LazyHighlighter = lazy(async (): Promise<{ default: React.FC<HighlighterProps> }> => {
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<SyntaxHighlighterProps> = 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<string, CSSProperties> }).default;
const Highlighter: React.FC<HighlighterProps> = ({ language, code, customStyle }) => (
<SyntaxHighlighter
language={language || 'text'}
Expand Down
51 changes: 26 additions & 25 deletions app/shared/src/ui/RuntimeIcon.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,39 +46,38 @@ vi.mock('@lobehub/icons', () => {
});

function mkColorIcon(tag: string) {
const Icon = ({ size }: { size?: number }) => <span data-size={size} data-testid={`${tag}-color-icon`} />;
Icon.Color = Icon;
return { default: Icon };
return {
default: ({ size }: { size?: number }) => (
<span data-size={size} data-testid={`${tag}-color-icon`} />
),
};
}
function mkRuntimeIcon(testid: string) {
return { default: ({ size }: { size?: number }) => <span data-size={size} data-testid={testid} /> };
}

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', () => {
Expand Down Expand Up @@ -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');
});
Expand Down
65 changes: 25 additions & 40 deletions app/shared/src/ui/RuntimeIcon.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<string, LobeIcon | undefined> = {
alibaba: Alibaba,
alibabacloud: AlibabaCloud,
anthropic: Anthropic,
azure: Azure,
aws: Aws,
bedrock: Bedrock,
Expand All @@ -143,8 +139,6 @@ const providerIconMap: Record<string, LobeIcon | undefined> = {
google: Google,
meta: Meta,
mistral: Mistral,
moonshot: Moonshot,
openai: OpenAI,
perplexity: Perplexity,
qwen: Qwen,
volcengine: Volcengine,
Expand All @@ -162,12 +156,7 @@ const modelIconMap: Record<string, LobeIcon | undefined> = {
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 {
Expand All @@ -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 <Color size={size} />;
return runtimeIconFallbackSvg('provider', size);
return Icon ? <Icon size={size} /> : runtimeIconFallbackSvg('provider', size);
}

function renderModelColorIcon(model: string, size: number): React.ReactNode {
const Icon = resolveModelIcon(model);
const Color = Icon?.Color;
if (Color) return <Color size={size} />;
return runtimeIconFallbackSvg('model', size);
return Icon ? <Icon size={size} /> : runtimeIconFallbackSvg('model', size);
}

function renderRuntimeIcon(resolution: RuntimeIconResolution, iconSize: number): React.ReactNode {
Expand Down
Loading