From 9eb8b2799697f12163de507e39989d9a0b43204c Mon Sep 17 00:00:00 2001 From: arvinxx Date: Sun, 31 Aug 2025 01:20:56 +0800 Subject: [PATCH 1/2] wip add model detail --- .../ConfigPanel/components/ModelSelect.tsx | 9 +- src/components/ModelSelect/ModelHoverCard.tsx | 240 ++++++++++++++++++ src/components/ModelSelect/index.tsx | 80 ++++-- .../FunctionCallingModelSelect/index.tsx | 9 +- src/features/ModelSelect/index.tsx | 9 +- src/features/ModelSwitchPanel/index.tsx | 2 +- src/locales/default/components.ts | 14 + 7 files changed, 334 insertions(+), 29 deletions(-) create mode 100644 src/components/ModelSelect/ModelHoverCard.tsx diff --git a/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx b/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx index 522b8b33b451b..53436392ff5ad 100644 --- a/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx +++ b/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect.tsx @@ -48,7 +48,14 @@ const ModelSelect = memo(() => { const options = useMemo(() => { const getImageModels = (provider: EnabledProviderWithModels) => { const modelOptions = provider.children.map((model) => ({ - label: , + label: ( + + ), provider: provider.id, value: `${provider.id}/${model.id}`, })); diff --git a/src/components/ModelSelect/ModelHoverCard.tsx b/src/components/ModelSelect/ModelHoverCard.tsx new file mode 100644 index 0000000000000..e3d2f6491bf66 --- /dev/null +++ b/src/components/ModelSelect/ModelHoverCard.tsx @@ -0,0 +1,240 @@ +import { ModelIcon } from '@lobehub/icons'; +import { Icon, Tag } from '@lobehub/ui'; +import { createStyles } from 'antd-style'; +import { ArrowDownToDot, ArrowUpFromDot, BookUp2Icon, CircleFadingArrowUp } from 'lucide-react'; +import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; + +import { ChatModelCard } from '@/types/llm'; +import { formatPriceByCurrency, formatTokenNumber } from '@/utils/format'; +import { + getAudioInputUnitRate, + getCachedTextInputUnitRate, + getTextInputUnitRate, + getTextOutputUnitRate, + getWriteCacheInputUnitRate, +} from '@/utils/pricing'; + +import { ModelInfoTags } from './index'; + +const useStyles = createStyles(({ css, token }) => ({ + card: css` + width: 280px; + padding: 16px; + background: ${token.colorBgElevated}; + border: 1px solid ${token.colorBorderSecondary}; + border-radius: 8px; + box-shadow: ${token.boxShadowTertiary}; + `, + header: css` + margin-bottom: 12px; + padding-bottom: 12px; + border-bottom: 1px solid ${token.colorBorderSecondary}; + `, + title: css` + font-size: 14px; + font-weight: 600; + color: ${token.colorText}; + margin-bottom: 4px; + line-height: 1.3; + `, + provider: css` + font-size: 11px; + color: ${token.colorTextTertiary}; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; + `, + infoGrid: css` + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px 16px; + margin: 12px 0; + `, + infoItem: css` + display: flex; + flex-direction: column; + gap: 4px; + `, + infoLabel: css` + font-size: 10px; + color: ${token.colorTextTertiary}; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; + `, + infoValue: css` + font-size: 13px; + color: ${token.colorText}; + font-weight: 500; + display: flex; + align-items: center; + gap: 4px; + `, + pricingGrid: css` + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px; + margin-top: 12px; + `, + pricingItem: css` + background: ${token.colorFillQuaternary}; + border-radius: 6px; + padding: 8px 6px; + text-align: center; + border: 1px solid ${token.colorBorder}; + `, + pricingLabel: css` + font-size: 9px; + color: ${token.colorTextTertiary}; + text-transform: uppercase; + letter-spacing: 0.3px; + margin-bottom: 2px; + font-weight: 500; + `, + pricingValue: css` + font-size: 11px; + color: ${token.colorText}; + font-weight: 600; + font-family: ${token.fontFamilyCode}; + `, + abilities: css` + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid ${token.colorBorderSecondary}; + `, + description: css` + font-size: 11px; + color: ${token.colorTextSecondary}; + line-height: 1.4; + margin-top: 8px; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + `, +})); + +interface ModelHoverCardProps extends ChatModelCard { + provider?: string; +} + +export const ModelHoverCard = memo(({ provider, ...model }) => { + const { t } = useTranslation('components'); + const { styles } = useStyles(); + + // Format pricing information + const getPricingData = () => { + if (!model.pricing) return null; + + const inputRate = getTextInputUnitRate(model.pricing); + const outputRate = getTextOutputUnitRate(model.pricing); + const cachedInputRate = getCachedTextInputUnitRate(model.pricing); + + return { + input: inputRate ? formatPriceByCurrency(inputRate, model.pricing.currency) : null, + output: outputRate ? formatPriceByCurrency(outputRate, model.pricing.currency) : null, + cached: cachedInputRate + ? formatPriceByCurrency(cachedInputRate, model.pricing.currency) + : null, + }; + }; + + const pricing = getPricingData(); + + return ( +
+ {/* Header */} +
+ + + +
{model.displayName || model.id}
+ {provider &&
{provider}
} +
+
+
+ + {/* Info Grid */} +
+ {/* Context Window */} + {typeof model.contextWindowTokens === 'number' && ( +
+
{t('ModelSelect.contextWindow')}
+
+ {model.contextWindowTokens === 0 ? ( + + ) : ( + formatTokenNumber(model.contextWindowTokens) + )} +
+
+ )} + + {/* Max Output */} + {model.maxOutput && ( +
+
{t('ModelSelect.maxOutput').toUpperCase()}
+
{formatTokenNumber(model.maxOutput)}
+
+ )} + + {/* Released Date */} + {model.releasedAt && ( +
+
{t('ModelSelect.releasedAt').toUpperCase()}
+
+ {new Date(model.releasedAt).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + })} +
+
+ )} + + {/* Model Type */} + {model.type && ( +
+
{t('ModelSelect.type').toUpperCase()}
+
{model.type.toUpperCase()}
+
+ )} +
+ + {/* Pricing */} + {pricing && (pricing.input || pricing.output || pricing.cached) && ( +
+ {pricing.input && ( +
+
INPUT
+
${pricing.input}
+
+ )} + {pricing.output && ( +
+
OUTPUT
+
${pricing.output}
+
+ )} + {pricing.cached && ( +
+
CACHED
+
${pricing.cached}
+
+ )} +
+ )} + + {/* Abilities */} +
+ +
+ + {/* Description */} + {model.description &&
{model.description}
} +
+ ); +}); + +ModelHoverCard.displayName = 'ModelHoverCard'; diff --git a/src/components/ModelSelect/index.tsx b/src/components/ModelSelect/index.tsx index 68080a3d19137..fa2db9db80cae 100644 --- a/src/components/ModelSelect/index.tsx +++ b/src/components/ModelSelect/index.tsx @@ -1,6 +1,7 @@ import { ChatModelCard } from '@lobechat/types'; import { IconAvatarProps, ModelIcon, ProviderIcon } from '@lobehub/icons'; import { Avatar, Icon, Tag, Text, Tooltip } from '@lobehub/ui'; +import { Popover } from 'antd'; import { createStyles, useResponsive } from 'antd-style'; import { Infinity, @@ -21,6 +22,8 @@ import { Flexbox } from 'react-layout-kit'; import { AiProviderSourceType } from '@/types/aiProvider'; import { formatTokenNumber } from '@/utils/format'; +import { ModelHoverCard } from './ModelHoverCard'; + export const TAG_CLASSNAME = 'lobe-model-info-tags'; const useStyles = createStyles(({ css, token }) => ({ @@ -174,39 +177,66 @@ export const ModelInfoTags = memo( ); interface ModelItemRenderProps extends ChatModelCard { + provider?: string; showInfoTag?: boolean; } -export const ModelItemRender = memo(({ showInfoTag = true, ...model }) => { - const { mobile } = useResponsive(); - return ( - +export const ModelItemRender = memo( + ({ showInfoTag = true, provider, ...model }) => { + const { mobile } = useResponsive(); + + const content = ( - - - {model.displayName || model.id} - + + + + {model.displayName || model.id} + + + {showInfoTag && } - {showInfoTag && } - - ); -}); + ); + + // Only show hover card on desktop and when we have meaningful information to show + const shouldShowHoverCard = + !mobile && + (model.description || + model.pricing || + typeof model.contextWindowTokens === 'number' || + model.releasedAt); + + if (shouldShowHoverCard) { + return ( + } + mouseEnterDelay={0.5} + placement="right" + > + {content} + + ); + } + + return content; + }, +); interface ProviderItemRenderProps { logo?: string; diff --git a/src/features/ChatInput/ActionBar/Search/FunctionCallingModelSelect/index.tsx b/src/features/ChatInput/ActionBar/Search/FunctionCallingModelSelect/index.tsx index cd8bac2f38c9a..d4fc265780072 100644 --- a/src/features/ChatInput/ActionBar/Search/FunctionCallingModelSelect/index.tsx +++ b/src/features/ChatInput/ActionBar/Search/FunctionCallingModelSelect/index.tsx @@ -37,7 +37,14 @@ const ModelSelect = memo(({ value, onChange, ...rest }) => { provider.children .filter((model) => !!model.abilities.functionCall) .map((model) => ({ - label: , + label: ( + + ), provider: provider.id, value: `${provider.id}/${model.id}`, })); diff --git a/src/features/ModelSelect/index.tsx b/src/features/ModelSelect/index.tsx index cb875411857a1..f2f1eadf32b48 100644 --- a/src/features/ModelSelect/index.tsx +++ b/src/features/ModelSelect/index.tsx @@ -42,7 +42,14 @@ const ModelSelect = memo(({ value, onChange, showAbility = tru const options = useMemo(() => { const getChatModels = (provider: EnabledProviderWithModels) => provider.children.map((model) => ({ - label: , + label: ( + + ), provider: provider.id, value: `${provider.id}/${model.id}`, })); diff --git a/src/features/ModelSwitchPanel/index.tsx b/src/features/ModelSwitchPanel/index.tsx index 51031c7ecbd86..005f3a6b7de3f 100644 --- a/src/features/ModelSwitchPanel/index.tsx +++ b/src/features/ModelSwitchPanel/index.tsx @@ -63,7 +63,7 @@ const ModelSwitchPanel = memo(({ children, onOpenChange, open }) => { const getModelItems = (provider: EnabledProviderWithModels) => { const items = provider.children.map((model) => ({ key: menuKey(provider.id, model.id), - label: , + label: , onClick: async () => { await updateAgentConfig({ model: model.id, provider: provider.id }); }, diff --git a/src/locales/default/components.ts b/src/locales/default/components.ts index 60492df6d6efe..9830907933c12 100644 --- a/src/locales/default/components.ts +++ b/src/locales/default/components.ts @@ -108,6 +108,7 @@ export default { unlimited: '无限制', }, ModelSelect: { + contextWindow: '上下文长度', featureTag: { custom: '自定义模型,默认设定同时支持函数调用与视觉识别,请根据实际情况验证上述能力的可用性', file: '该模型支持上传文件读取与识别', @@ -119,7 +120,20 @@ export default { video: '该模型支持视频识别', vision: '该模型支持视觉识别', }, + maxOutput: '最大输出', + pricing: { + audioInput: '音频输入', + cachedInput: '缓存输入', + input: '输入', + output: '输出', + title: '价格', + writeCacheInput: '写入缓存输入', + }, + releasedAt: '发布时间', removed: '该模型不在列表中,若取消选中将会自动移除', + tokens: '{{tokens}} tokens', + type: '类型', + unlimited: '无限制', }, ModelSwitchPanel: { emptyModel: '没有启用的模型,请前往设置开启', From cad4ead93bd5d09ac2810f9ba1e82fa38083992d Mon Sep 17 00:00:00 2001 From: arvinxx Date: Tue, 14 Oct 2025 13:50:06 +0800 Subject: [PATCH 2/2] update --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index a95429fa7f2c4..3f8ccc8fc980d 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,7 @@ CLAUDE.local.md prd GEMINI.md +test-results + +prd +GEMINI.md