From 5b953b15cb58715ccdd0cda55601384e98a3323c Mon Sep 17 00:00:00 2001 From: YuTengjing Date: Mon, 26 Jan 2026 21:05:47 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20share=20page=20improvements?= =?UTF-8?q?=20and=20pg17=20docs=20update=20(#11850)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/self-hosting/advanced/knowledge-base.mdx | 2 +- .../advanced/knowledge-base.zh-CN.mdx | 2 +- .../migration/v2/breaking-changes.mdx | 8 +++ .../migration/v2/breaking-changes.zh-CN.mdx | 8 +++ .../server-database/docker-compose.mdx | 2 +- .../server-database/docker-compose.zh-CN.mdx | 2 +- docs/self-hosting/server-database/docker.mdx | 10 +--- .../server-database/docker.zh-CN.mdx | 11 +---- locales/en-US/error.json | 13 +++-- locales/zh-CN/error.json | 13 +++-- packages/database/src/models/topicShare.ts | 10 +++- .../src/modelProviders/anthropic.ts | 2 +- .../model-bank/src/modelProviders/google.ts | 2 +- .../model-bank/src/modelProviders/hunyuan.ts | 2 +- .../model-bank/src/modelProviders/minimax.ts | 2 +- .../model-bank/src/modelProviders/openai.ts | 2 +- .../src/modelProviders/siliconcloud.ts | 5 +- .../model-bank/src/modelProviders/vertexai.ts | 2 +- .../src/modelProviders/volcengine.ts | 2 +- packages/model-bank/src/modelProviders/xai.ts | 2 +- packages/model-runtime/src/types/chat.ts | 14 ++++++ .../components/ModelSelect/ImageModelItem.tsx | 40 +++++++++++---- .../components/ModelSelect/index.tsx | 11 ++++- .../features/ProviderConfig/Checker.tsx | 37 ++++++++++++-- .../Messages/Assistant/Extra/index.tsx | 30 +++++++----- .../Conversation/Messages/User/Extra.tsx | 6 ++- .../IntegrationDetailContent.tsx | 23 +++++---- src/features/SharePopover/index.tsx | 49 +++++++++++++++++-- .../GlobalProvider/useUserStateRedirect.ts | 8 ++- src/locales/default/chat.ts | 1 + src/locales/default/error.ts | 13 +++-- src/store/global/initialState.ts | 2 + 32 files changed, 249 insertions(+), 87 deletions(-) diff --git a/docs/self-hosting/advanced/knowledge-base.mdx b/docs/self-hosting/advanced/knowledge-base.mdx index 91d8532091..d9abf3fabc 100644 --- a/docs/self-hosting/advanced/knowledge-base.mdx +++ b/docs/self-hosting/advanced/knowledge-base.mdx @@ -27,7 +27,7 @@ PostgreSQL is a powerful open-source relational database system, and PGVector is Deployment script example: ``` -docker run -p 5432:5432 -d --name pg -e POSTGRES_PASSWORD=mysecretpassword pgvector/pgvector:pg16 +docker run -p 5432:5432 -d --name pg -e POSTGRES_PASSWORD=mysecretpassword pgvector/pgvector:pg17 ``` - **Note**: Ensure sufficient resources for vector operations diff --git a/docs/self-hosting/advanced/knowledge-base.zh-CN.mdx b/docs/self-hosting/advanced/knowledge-base.zh-CN.mdx index af12a4d959..349cf3865a 100644 --- a/docs/self-hosting/advanced/knowledge-base.zh-CN.mdx +++ b/docs/self-hosting/advanced/knowledge-base.zh-CN.mdx @@ -25,7 +25,7 @@ PostgreSQL 是一个强大的开源关系型数据库系统,而 PGVector 是 示例部署脚本: ``` -docker run -p 5432:5432 -d --name pg -e POSTGRES_PASSWORD=mysecretpassword pgvector/pgvector:pg16 +docker run -p 5432:5432 -d --name pg -e POSTGRES_PASSWORD=mysecretpassword pgvector/pgvector:pg17 ``` - **注意事项**:确保分配足够的资源以处理向量操作 diff --git a/docs/self-hosting/migration/v2/breaking-changes.mdx b/docs/self-hosting/migration/v2/breaking-changes.mdx index 686e23b675..e4156f317f 100644 --- a/docs/self-hosting/migration/v2/breaking-changes.mdx +++ b/docs/self-hosting/migration/v2/breaking-changes.mdx @@ -71,3 +71,11 @@ See the [Clerk Migration Guide](/docs/self-hosting/advanced/auth/clerk-to-better LobeHub 2.0 only supports Server DB mode. Client DB (PGlite) is no longer supported. If you were using `NEXT_PUBLIC_SERVICE_MODE=client`, you need to migrate to Server DB deployment. For deployment guides, see [Server Database Deployment](/docs/self-hosting/server-database). + +## PostgreSQL Version Requirements + +LobeHub 2.0 recommends using **PostgreSQL 17** or higher. + +This is because LobeHub 2.0 uses the [pg\_search](https://github.com/paradedb/paradedb/tree/main/pg_search) extension for full-text search capabilities. If you use Serverless Postgres services like Neon, the pg\_search extension is only available on PostgreSQL 17. + +If you self-host your database with Docker, we recommend using the `pgvector/pgvector:pg17` image. diff --git a/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx b/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx index 9243417b31..5baabb1127 100644 --- a/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx +++ b/docs/self-hosting/migration/v2/breaking-changes.zh-CN.mdx @@ -69,3 +69,11 @@ LobeHub 2.0 仅支持 Better Auth 认证系统,不再支持 NextAuth 和 Clerk LobeHub 2.0 仅支持 Server DB 模式,不再支持 Client DB (PGlite)。如果您之前使用 `NEXT_PUBLIC_SERVICE_MODE=client`,需要迁移到 Server DB 部署方式。 详细部署指南请参阅[服务端数据库部署](/docs/self-hosting/server-database)。 + +## PostgreSQL 版本要求 + +LobeHub 2.0 推荐使用 **PostgreSQL 17** 及以上版本。 + +这是因为 LobeHub 2.0 使用了 [pg\_search](https://github.com/paradedb/paradedb/tree/main/pg_search) 插件来提供全文搜索能力。如果您使用 Neon 等 Serverless Postgres 服务,pg\_search 插件仅在 PostgreSQL 17 上可用。 + +如果您使用 Docker 自建数据库,推荐使用 `pgvector/pgvector:pg17` 镜像。 diff --git a/docs/self-hosting/server-database/docker-compose.mdx b/docs/self-hosting/server-database/docker-compose.mdx index 0fc58f926c..cf24debd5e 100644 --- a/docs/self-hosting/server-database/docker-compose.mdx +++ b/docs/self-hosting/server-database/docker-compose.mdx @@ -768,7 +768,7 @@ services: - lobe-network postgresql: - image: pgvector/pgvector:pg16 + image: pgvector/pgvector:pg17 container_name: lobe-postgres ports: - '5432:5432' diff --git a/docs/self-hosting/server-database/docker-compose.zh-CN.mdx b/docs/self-hosting/server-database/docker-compose.zh-CN.mdx index 10b269462b..96a99195ea 100644 --- a/docs/self-hosting/server-database/docker-compose.zh-CN.mdx +++ b/docs/self-hosting/server-database/docker-compose.zh-CN.mdx @@ -743,7 +743,7 @@ services: - lobe-network postgresql: - image: pgvector/pgvector:pg16 + image: pgvector/pgvector:pg17 container_name: lobe-postgres ports: - '5432:5432' diff --git a/docs/self-hosting/server-database/docker.mdx b/docs/self-hosting/server-database/docker.mdx index ad7c1bdf98..aa4358d2c5 100644 --- a/docs/self-hosting/server-database/docker.mdx +++ b/docs/self-hosting/server-database/docker.mdx @@ -28,12 +28,6 @@ tags: Database](/docs/self-hosting/server-database) first. - - Due to the inability to expose `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` using Docker environment variables, you cannot use Clerk as an authentication service when deploying LobeHub using Docker / Docker Compose. - - If you do need Clerk as an authentication service, you might consider deploying using Vercel or building your own image. - - ## Deploying on a Linux Server Here is the process for deploying the LobeHub server database version on a Linux server: @@ -46,10 +40,10 @@ Here is the process for deploying the LobeHub server database version on a Linux ```sh docker network create pg - docker run --name my-postgres --network pg -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d pgvector/pgvector:pg16 + docker run --name my-postgres --network pg -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d pgvector/pgvector:pg17 ``` - The above command will create a PG instance named `my-postgres` on the network `pg`, where `pgvector/pgvector:pg16` is a Postgres 16 image with the pgvector plugin installed by default. + The above command will create a PG instance named `my-postgres` on the network `pg`, where `pgvector/pgvector:pg17` is a Postgres 17 image with the pgvector plugin installed by default. The pgvector plugin provides vector search capabilities for Postgres, which is an important diff --git a/docs/self-hosting/server-database/docker.zh-CN.mdx b/docs/self-hosting/server-database/docker.zh-CN.mdx index acf4b8b18f..8e99c4d740 100644 --- a/docs/self-hosting/server-database/docker.zh-CN.mdx +++ b/docs/self-hosting/server-database/docker.zh-CN.mdx @@ -26,13 +26,6 @@ tags: 存储服务](/zh/docs/self-hosting/advanced/s3/tencent-cloud)。 - - 由于无法使用 Docker 环境变量暴露 `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`,使用 Docker / Docker Compose - 部署 LobeHub 时,你不能使用 Clerk 作为登录鉴权服务。 - - 如果你确实需要 Clerk 作为登录鉴权服务,你可以考虑使用 Vercel 部署或者自行构建镜像。 - - ## 在 Linux 服务器上部署 以下是在 Linux 服务器上部署 LobeHub DB 版的流程: @@ -45,10 +38,10 @@ tags: ```sh docker network create pg - docker run --name my-postgres --network pg -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d pgvector/pgvector:pg16 + docker run --name my-postgres --network pg -e POSTGRES_PASSWORD=mysecretpassword -p 5432:5432 -d pgvector/pgvector:pg17 ``` - 上述指令会创建一个名为 `my-postgres`,并且网络为 `pg` 的 PG 实例,其中 `pgvector/pgvector:pg16` 是一个 Postgres 16 的镜像,且默认安装了 pgvector 插件。 + 上述指令会创建一个名为 `my-postgres`,并且网络为 `pg` 的 PG 实例,其中 `pgvector/pgvector:pg17` 是一个 Postgres 17 的镜像,且默认安装了 pgvector 插件。 pgvector 插件为 Postgres 提供了向量搜索的能力,是 LobeHub 实现 RAG 的重要构件之一。 diff --git a/locales/en-US/error.json b/locales/en-US/error.json index 2c76fe9aa7..bd6e433bbc 100644 --- a/locales/en-US/error.json +++ b/locales/en-US/error.json @@ -9,11 +9,18 @@ "import.importConfigFile.title": "Import Failed", "import.incompatible.description": "This file was exported from a higher version. Please try upgrading to the latest version and then re-importing.", "import.incompatible.title": "Current application does not support importing this file", + "inviteCode.continue": "Continue", "inviteCode.currentEmail": "Current account: {{email}}", "inviteCode.desc": "An invite code is required to access LobeHub. Please enter a valid invite code to continue.", - "inviteCode.friends": "Friends", - "inviteCode.getCodeHint": "Get an invite code from:", - "inviteCode.title": "Invite Code Required", + "inviteCode.friends": "Ask a friend", + "inviteCode.joinUsOn": "Join us on", + "inviteCode.lookingForInvite": "Looking for an invite?", + "inviteCode.notYou": "Not you?", + "inviteCode.openingInStages": "is opening in stages.", + "inviteCode.placeholder": "Invite code or link", + "inviteCode.pleaseEnterCode": "Enter your invite code to continue.", + "inviteCode.switchAccount": "Switch account", + "inviteCode.title": "You're Almost In", "loginRequired.desc": "You will be redirected to the login page shortly", "loginRequired.title": "Please log in to use this feature", "notFound.backHome": "Back to Home", diff --git a/locales/zh-CN/error.json b/locales/zh-CN/error.json index cc105e15ab..c446868fb7 100644 --- a/locales/zh-CN/error.json +++ b/locales/zh-CN/error.json @@ -9,11 +9,18 @@ "import.importConfigFile.title": "导入遇到了问题", "import.incompatible.description": "该文件由更高版本导出。请升级到最新版后再导入", "import.incompatible.title": "版本不兼容", + "inviteCode.continue": "继续", "inviteCode.currentEmail": "当前账号:{{email}}", "inviteCode.desc": "需要邀请码才能访问 LobeHub。请输入有效的邀请码以继续。", - "inviteCode.friends": "好友", - "inviteCode.getCodeHint": "获取邀请码:", - "inviteCode.title": "需要邀请码", + "inviteCode.friends": "问问好友", + "inviteCode.joinUsOn": "关注我们", + "inviteCode.lookingForInvite": "需要邀请码?", + "inviteCode.notYou": "不是你?", + "inviteCode.openingInStages": "正在分阶段开放中。", + "inviteCode.placeholder": "输入邀请码或链接", + "inviteCode.pleaseEnterCode": "请输入邀请码以继续。", + "inviteCode.switchAccount": "切换账号", + "inviteCode.title": "即将完成", "loginRequired.desc": "将为你跳转到登录页。登录后即可继续", "loginRequired.title": "需要登录后继续", "notFound.backHome": "返回首页", diff --git a/packages/database/src/models/topicShare.ts b/packages/database/src/models/topicShare.ts index a05704c8fc..519a070116 100644 --- a/packages/database/src/models/topicShare.ts +++ b/packages/database/src/models/topicShare.ts @@ -19,8 +19,9 @@ export class TopicShareModel { } /** - * Create a new share for a topic. + * Create or get existing share for a topic. * Each topic can only have one share record (enforced by unique constraint). + * If record already exists, returns the existing one. */ create = async (topicId: string, visibility: ShareVisibility = 'private') => { // First verify the topic belongs to the user @@ -39,8 +40,14 @@ export class TopicShareModel { userId: this.userId, visibility, }) + .onConflictDoNothing({ target: topicShares.topicId }) .returning(); + // If conflict occurred, return existing record + if (!result) { + return this.getByTopicId(topicId); + } + return result; }; @@ -74,6 +81,7 @@ export class TopicShareModel { .select({ id: topicShares.id, topicId: topicShares.topicId, + userId: topicShares.userId, visibility: topicShares.visibility, }) .from(topicShares) diff --git a/packages/model-bank/src/modelProviders/anthropic.ts b/packages/model-bank/src/modelProviders/anthropic.ts index 36bb3dd735..8aa171108d 100644 --- a/packages/model-bank/src/modelProviders/anthropic.ts +++ b/packages/model-bank/src/modelProviders/anthropic.ts @@ -2,7 +2,7 @@ import { type ModelProviderCard } from '@/types/llm'; const Anthropic: ModelProviderCard = { chatModels: [], - checkModel: 'claude-3-haiku-20240307', + checkModel: 'claude-opus-4-5-20251101', description: 'Anthropic builds advanced language models like Claude 3.5 Sonnet, Claude 3 Sonnet, Claude 3 Opus, and Claude 3 Haiku, balancing intelligence, speed, and cost for workloads from enterprise to rapid-response use cases.', enabled: true, diff --git a/packages/model-bank/src/modelProviders/google.ts b/packages/model-bank/src/modelProviders/google.ts index a999c1da63..2452d06254 100644 --- a/packages/model-bank/src/modelProviders/google.ts +++ b/packages/model-bank/src/modelProviders/google.ts @@ -3,7 +3,7 @@ import { type ModelProviderCard } from '@/types/llm'; // ref: https://ai.google.dev/gemini-api/docs/models/gemini const Google: ModelProviderCard = { chatModels: [], - checkModel: 'gemini-2.0-flash', + checkModel: 'gemini-3-flash-preview', description: "Google's Gemini family is its most advanced general-purpose AI, built by Google DeepMind for multimodal use across text, code, images, audio, and video. It scales from data centers to mobile devices with strong efficiency and reach.", enabled: true, diff --git a/packages/model-bank/src/modelProviders/hunyuan.ts b/packages/model-bank/src/modelProviders/hunyuan.ts index 9fbc349f77..815332e97d 100644 --- a/packages/model-bank/src/modelProviders/hunyuan.ts +++ b/packages/model-bank/src/modelProviders/hunyuan.ts @@ -3,7 +3,7 @@ import { type ModelProviderCard } from '@/types/llm'; // ref https://cloud.tencent.com/document/product/1729/104753 const Hunyuan: ModelProviderCard = { chatModels: [], - checkModel: 'hunyuan-lite', + checkModel: 'hunyuan-t1-latest', description: 'A Tencent-developed LLM with strong Chinese writing, solid reasoning in complex contexts, and reliable task execution.', disableBrowserRequest: true, diff --git a/packages/model-bank/src/modelProviders/minimax.ts b/packages/model-bank/src/modelProviders/minimax.ts index beef39db93..d9fd2944a1 100644 --- a/packages/model-bank/src/modelProviders/minimax.ts +++ b/packages/model-bank/src/modelProviders/minimax.ts @@ -3,7 +3,7 @@ import { type ModelProviderCard } from '@/types/llm'; // ref: https://platform.minimaxi.com/document/Models const Minimax: ModelProviderCard = { chatModels: [], - checkModel: 'MiniMax-M2', + checkModel: 'MiniMax-M2.1', description: 'Founded in 2021, MiniMax builds general-purpose AI with multimodal foundation models, including trillion-parameter MoE text models, speech models, and vision models, along with apps like Hailuo AI.', id: 'minimax', diff --git a/packages/model-bank/src/modelProviders/openai.ts b/packages/model-bank/src/modelProviders/openai.ts index 656390836a..eab035863c 100644 --- a/packages/model-bank/src/modelProviders/openai.ts +++ b/packages/model-bank/src/modelProviders/openai.ts @@ -4,7 +4,7 @@ import { type ModelProviderCard } from '@/types/llm'; const OpenAI: ModelProviderCard = { apiKeyUrl: 'https://platform.openai.com/api-keys?utm_source=lobehub', chatModels: [], - checkModel: 'gpt-5-nano', + checkModel: 'gpt-5.2', description: 'OpenAI is a leading AI research lab whose GPT models advanced natural language processing, delivering high performance and strong value across research, business, and innovation.', enabled: true, diff --git a/packages/model-bank/src/modelProviders/siliconcloud.ts b/packages/model-bank/src/modelProviders/siliconcloud.ts index d79f67e20f..10c327442a 100644 --- a/packages/model-bank/src/modelProviders/siliconcloud.ts +++ b/packages/model-bank/src/modelProviders/siliconcloud.ts @@ -3,8 +3,9 @@ import { type ModelProviderCard } from '@/types/llm'; // ref: https://siliconflow.cn/zh-cn/pricing const SiliconCloud: ModelProviderCard = { chatModels: [], - checkModel: 'Pro/Qwen/Qwen2-7B-Instruct', - description: 'SiliconCloud is a cost-effective GenAI cloud service built on strong open-source base models.', + checkModel: 'Pro/zai-org/glm-4.7', + description: + 'SiliconCloud is a cost-effective GenAI cloud service built on strong open-source base models.', id: 'siliconcloud', modelList: { showModelFetcher: true }, modelsUrl: 'https://siliconflow.cn/zh-cn/models', diff --git a/packages/model-bank/src/modelProviders/vertexai.ts b/packages/model-bank/src/modelProviders/vertexai.ts index d6ff60890c..d3127d953f 100644 --- a/packages/model-bank/src/modelProviders/vertexai.ts +++ b/packages/model-bank/src/modelProviders/vertexai.ts @@ -3,7 +3,7 @@ import { type ModelProviderCard } from '@/types/llm'; // ref: https://ai.google.dev/gemini-api/docs/models/gemini const VertexAI: ModelProviderCard = { chatModels: [], - checkModel: 'gemini-2.0-flash', + checkModel: 'gemini-3-flash-preview', description: "Google's Gemini family is its most advanced general-purpose AI, built by Google DeepMind for multimodal use across text, code, images, audio, and video. It scales from data centers to mobile devices, improving efficiency and deployment flexibility.", id: 'vertexai', diff --git a/packages/model-bank/src/modelProviders/volcengine.ts b/packages/model-bank/src/modelProviders/volcengine.ts index 3f9e7d32ff..b42a713ab2 100644 --- a/packages/model-bank/src/modelProviders/volcengine.ts +++ b/packages/model-bank/src/modelProviders/volcengine.ts @@ -3,7 +3,7 @@ import { type ModelProviderCard } from '@/types/llm'; // ref https://www.volcengine.com/docs/82379/1330310 const Doubao: ModelProviderCard = { chatModels: [], - checkModel: 'doubao-seed-1-6-flash-250828', + checkModel: 'doubao-seed-1.8', description: 'ByteDance’s model service platform offers secure, feature-rich, cost-competitive model access plus end-to-end tooling for data, fine-tuning, inference, and evaluation.', id: 'volcengine', diff --git a/packages/model-bank/src/modelProviders/xai.ts b/packages/model-bank/src/modelProviders/xai.ts index 9a4df2a8ba..82fb791ff2 100644 --- a/packages/model-bank/src/modelProviders/xai.ts +++ b/packages/model-bank/src/modelProviders/xai.ts @@ -3,7 +3,7 @@ import { type ModelProviderCard } from '@/types/llm'; // ref: https://x.ai/about const XAI: ModelProviderCard = { chatModels: [], - checkModel: 'grok-2-1212', + checkModel: 'grok-4-1-fast-non-reasoning', description: 'xAI builds AI to accelerate scientific discovery, with a mission to deepen humanity’s understanding of the universe.', id: 'xai', diff --git a/packages/model-runtime/src/types/chat.ts b/packages/model-runtime/src/types/chat.ts index 00af95d80f..85e58199cd 100644 --- a/packages/model-runtime/src/types/chat.ts +++ b/packages/model-runtime/src/types/chat.ts @@ -90,6 +90,20 @@ export interface ChatStreamPayload { * @title List of chat messages */ messages: OpenAIChatMessage[]; + /** + * @title Custom text chunks for mock response + */ + mockChunks?: string[]; + /** + * @title Delay in milliseconds between mock chunks + * @default 50 + */ + mockDelayMs?: number; + /** + * @title Enable mock response for benchmark testing + * @description When true, returns a simulated SSE stream without calling real LLM API + */ + mockResponse?: boolean; /** * @title Model name */ diff --git a/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ModelSelect/ImageModelItem.tsx b/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ModelSelect/ImageModelItem.tsx index 8a12c2c6ee..9b58fb8191 100644 --- a/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +++ b/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ModelSelect/ImageModelItem.tsx @@ -1,3 +1,5 @@ +import { BRANDING_PROVIDER } from '@lobechat/business-const'; +import { CREDITS_PER_DOLLAR } from '@lobechat/const/currency'; import { ModelIcon } from '@lobehub/icons'; import { Flexbox, Popover, Text } from '@lobehub/ui'; import { createStaticStyles, cx } from 'antd-style'; @@ -7,6 +9,8 @@ import { memo, useMemo } from 'react'; import NewModelBadge from '@/components/ModelSelect/NewModelBadge'; import { useIsDark } from '@/hooks/useIsDark'; +import { useServerConfigStore } from '@/store/serverConfig'; +import { serverConfigSelectors } from '@/store/serverConfig/selectors'; const POPOVER_MAX_WIDTH = 320; @@ -38,6 +42,10 @@ const styles = createStaticStyles(({ css, cssVar }) => ({ })); type ImageModelItemProps = AiModelForSelect & { + /** + * Provider ID for determining price display format + */ + providerId?: string; /** * Whether to show new model badge * @default true @@ -55,25 +63,39 @@ const ImageModelItem = memo( approximatePricePerImage, description, pricePerImage, + providerId, showPopover = true, showBadge = true, ...model }) => { const isDarkMode = useIsDark(); + const enableBusinessFeatures = useServerConfigStore( + serverConfigSelectors.enableBusinessFeatures, + ); const priceLabel = useMemo(() => { - // Priority 1: Use exact price - if (typeof pricePerImage === 'number') { - return `${numeral(pricePerImage).format('$0,0.00[000]')} / image`; - } - - // Priority 2: Use approximate price with prefix - if (typeof approximatePricePerImage === 'number') { - return `~ ${numeral(approximatePricePerImage).format('$0,0.00[000]')} / image`; + // Show credits only for branding provider with business features enabled + if (enableBusinessFeatures && providerId === BRANDING_PROVIDER) { + if (typeof pricePerImage === 'number') { + const credits = pricePerImage * CREDITS_PER_DOLLAR; + return `${numeral(credits).format('0,0')} credits/张`; + } + if (typeof approximatePricePerImage === 'number') { + const credits = approximatePricePerImage * CREDITS_PER_DOLLAR; + return `~ ${numeral(credits).format('0,0')} credits/张`; + } + } else { + // Show USD price for open source version or non-branding providers + if (typeof pricePerImage === 'number') { + return `${numeral(pricePerImage).format('$0,0.00[000]')} / image`; + } + if (typeof approximatePricePerImage === 'number') { + return `~ ${numeral(approximatePricePerImage).format('$0,0.00[000]')} / image`; + } } return undefined; - }, [approximatePricePerImage, pricePerImage]); + }, [approximatePricePerImage, enableBusinessFeatures, pricePerImage, providerId]); const popoverContent = useMemo(() => { if (!description && !priceLabel) return null; diff --git a/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ModelSelect/index.tsx b/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ModelSelect/index.tsx index 529221386d..2a74ababab 100644 --- a/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ModelSelect/index.tsx +++ b/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/ModelSelect/index.tsx @@ -46,7 +46,7 @@ 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}`, })); @@ -130,7 +130,14 @@ const ModelSelect = memo(() => { if (!modelInfo) return props.label; - return ; + return ( + + ); }; return ( diff --git a/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx b/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx index 7a64747ed8..90ff66d40c 100644 --- a/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx +++ b/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/Checker.tsx @@ -6,12 +6,12 @@ import { ModelIcon } from '@lobehub/icons'; import { Alert, Button, Flexbox, Highlighter, Icon, LobeSelect as Select } from '@lobehub/ui'; import { cssVar } from 'antd-style'; import { Loader2Icon } from 'lucide-react'; -import { type ReactNode, memo, useEffect, useState } from 'react'; +import { type ReactNode, memo, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useProviderName } from '@/hooks/useProviderName'; import { chatService } from '@/services/chat'; -import { aiModelSelectors, aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra'; +import { aiProviderSelectors, useAiInfraStore } from '@/store/aiInfra'; const Error = memo<{ error: ChatMessageError }>(({ error }) => { const { t } = useTranslation('error'); @@ -62,7 +62,36 @@ const Checker = memo( aiProviderSelectors.isProviderConfigUpdating(provider)(s), s.updateAiProviderConfig, ]); - const totalModels = useAiInfraStore(aiModelSelectors.aiProviderChatModelListIds); + const aiProviderModelList = useAiInfraStore((s) => s.aiProviderModelList); + + // Sort models for better UX: + // 1. checkModel first (provider's recommended test model) + // 2. enabled models (user is actively using) + // 3. by releasedAt descending (newer models first) + // 4. models without releasedAt last + const sortedModels = useMemo(() => { + const chatModels = aiProviderModelList.filter((m) => m.type === 'chat'); + + const sorted = [...chatModels].sort((a, b) => { + // checkModel always first + if (a.id === model) return -1; + if (b.id === model) return 1; + + // enabled models come before disabled + if (a.enabled !== b.enabled) return a.enabled ? -1 : 1; + + // sort by releasedAt descending, models without releasedAt go last + if (a.releasedAt && b.releasedAt) { + return new Date(b.releasedAt).getTime() - new Date(a.releasedAt).getTime(); + } + if (a.releasedAt && !b.releasedAt) return -1; + if (!a.releasedAt && b.releasedAt) return 1; + + return 0; + }); + + return sorted.map((m) => m.id); + }, [aiProviderModelList, model]); const [loading, setLoading] = useState(false); const [pass, setPass] = useState(false); @@ -154,7 +183,7 @@ const Checker = memo( ); }} - options={totalModels.map((id) => ({ label: id, value: id }))} + options={sortedModels.map((id) => ({ label: id, value: id }))} style={{ flex: 1, overflow: 'hidden', diff --git a/src/features/Conversation/Messages/Assistant/Extra/index.tsx b/src/features/Conversation/Messages/Assistant/Extra/index.tsx index 8c8221b71b..ce52285984 100644 --- a/src/features/Conversation/Messages/Assistant/Extra/index.tsx +++ b/src/features/Conversation/Messages/Assistant/Extra/index.tsx @@ -3,6 +3,9 @@ import { type ModelPerformance, type ModelUsage } from '@lobechat/types'; import { Flexbox } from '@lobehub/ui'; import { memo } from 'react'; +import { useUserStore } from '@/store/user'; +import { authSelectors } from '@/store/user/slices/auth/selectors'; + import { messageStateSelectors, useConversationStore } from '../../../store'; import ExtraContainer from '../../components/Extras/ExtraContainer'; import TTS from '../../components/Extras/TTS'; @@ -23,24 +26,27 @@ interface AssistantMessageExtraProps { export const AssistantMessageExtra = memo( ({ extra, id, content, performance, usage, tools, provider, model }) => { const loading = useConversationStore(messageStateSelectors.isMessageGenerating(id)); + const isLogin = useUserStore(authSelectors.isLogin); return ( {content !== LOADING_FLAT && model && ( )} - <> - {!!extra?.tts && ( - - - - )} - {!!extra?.translate && ( - - - - )} - + {isLogin && ( + <> + {!!extra?.tts && ( + + + + )} + {!!extra?.translate && ( + + + + )} + + )} ); }, diff --git a/src/features/Conversation/Messages/User/Extra.tsx b/src/features/Conversation/Messages/User/Extra.tsx index c785822c90..4a82695f02 100644 --- a/src/features/Conversation/Messages/User/Extra.tsx +++ b/src/features/Conversation/Messages/User/Extra.tsx @@ -1,6 +1,9 @@ import { Flexbox } from '@lobehub/ui'; import { memo } from 'react'; +import { useUserStore } from '@/store/user'; +import { authSelectors } from '@/store/user/slices/auth/selectors'; + import { messageStateSelectors, useConversationStore } from '../../store'; import ExtraContainer from '../components/Extras/ExtraContainer'; import TTS from '../components/Extras/TTS'; @@ -14,11 +17,12 @@ interface UserMessageExtraProps { export const UserMessageExtra = memo(({ extra, id, content }) => { const loading = useConversationStore(messageStateSelectors.isMessageGenerating(id)); + const isLogin = useUserStore(authSelectors.isLogin); const showTranslate = !!extra?.translate; const showTTS = !!extra?.tts; - const showExtra = showTranslate || showTTS; + const showExtra = isLogin && (showTranslate || showTTS); if (!showExtra) return; diff --git a/src/features/IntegrationDetailModal/IntegrationDetailContent.tsx b/src/features/IntegrationDetailModal/IntegrationDetailContent.tsx index 8a92407b26..8fc02c9e3e 100644 --- a/src/features/IntegrationDetailModal/IntegrationDetailContent.tsx +++ b/src/features/IntegrationDetailModal/IntegrationDetailContent.tsx @@ -8,7 +8,7 @@ import { } from '@lobechat/const'; import { Flexbox, Icon, Image, Tag, Text, Typography, useModalContext } from '@lobehub/ui'; import { Button, Divider } from 'antd'; -import { createStyles, cssVar } from 'antd-style'; +import { createStaticStyles, cssVar } from 'antd-style'; import type { Klavis } from 'klavis'; import { ExternalLink, Loader2, SquareArrowOutUpRight } from 'lucide-react'; import { useEffect, useMemo, useRef } from 'react'; @@ -20,7 +20,7 @@ import { klavisStoreSelectors, lobehubSkillStoreSelectors } from '@/store/tool/s import { KlavisServerStatus } from '@/store/tool/slices/klavisStore'; import { LobehubSkillStatus } from '@/store/tool/slices/lobehubSkillStore/types'; -const useStyles = createStyles(({ css, token }) => ({ +const styles = createStaticStyles(({ css, cssVar }) => ({ authorLink: css` cursor: pointer; @@ -28,7 +28,7 @@ const useStyles = createStyles(({ css, token }) => ({ gap: 4px; align-items: center; - color: ${token.colorPrimary}; + color: ${cssVar.colorPrimary}; &:hover { text-decoration: underline; @@ -41,7 +41,7 @@ const useStyles = createStyles(({ css, token }) => ({ `, detailLabel: css` font-size: 12px; - color: ${token.colorTextTertiary}; + color: ${cssVar.colorTextTertiary}; `, header: css` display: flex; @@ -51,7 +51,7 @@ const useStyles = createStyles(({ css, token }) => ({ padding: 16px; border-radius: 12px; - background: ${token.colorFillTertiary}; + background: ${cssVar.colorFillTertiary}; `, icon: css` display: flex; @@ -63,25 +63,25 @@ const useStyles = createStyles(({ css, token }) => ({ height: 56px; border-radius: 12px; - background: ${token.colorBgContainer}; + background: ${cssVar.colorBgContainer}; `, introduction: css` font-size: 14px; line-height: 1.8; - color: ${token.colorText}; + color: ${cssVar.colorText}; `, sectionTitle: css` font-size: 14px; font-weight: 600; - color: ${token.colorText}; + color: ${cssVar.colorText}; `, title: css` font-size: 18px; font-weight: 600; - color: ${token.colorText}; + color: ${cssVar.colorText}; `, toolTag: css` - font-family: ${token.fontFamilyCode}; + font-family: ${cssVar.fontFamilyCode}; font-size: 12px; `, toolsContainer: css` @@ -92,7 +92,7 @@ const useStyles = createStyles(({ css, token }) => ({ trustWarning: css` font-size: 12px; line-height: 1.6; - color: ${token.colorTextTertiary}; + color: ${cssVar.colorTextTertiary}; `, })); @@ -109,7 +109,6 @@ export const IntegrationDetailContent = ({ identifier, serverName, }: IntegrationDetailContentProps) => { - const { styles } = useStyles(); const { t } = useTranslation(['plugin', 'setting']); const { close } = useModalContext(); diff --git a/src/features/SharePopover/index.tsx b/src/features/SharePopover/index.tsx index b0844cddc6..c796ef04ce 100644 --- a/src/features/SharePopover/index.tsx +++ b/src/features/SharePopover/index.tsx @@ -2,6 +2,7 @@ import { Button, + Checkbox, Flexbox, LobeSelect, Popover, @@ -20,6 +21,8 @@ import { useAppOrigin } from '@/hooks/useAppOrigin'; import { useIsMobile } from '@/hooks/useIsMobile'; import { topicService } from '@/services/topic'; import { useChatStore } from '@/store/chat'; +import { useGlobalStore } from '@/store/global'; +import { systemStatusSelectors } from '@/store/global/selectors'; import { styles } from './style'; @@ -38,6 +41,10 @@ const SharePopoverContent = memo(({ onOpenModal }) => const appOrigin = useAppOrigin(); const activeTopicId = useChatStore((s) => s.activeTopicId); + const [hideTopicSharePrivacyWarning, updateSystemStatus] = useGlobalStore((s) => [ + systemStatusSelectors.systemStatus(s).hideTopicSharePrivacyWarning ?? false, + s.updateSystemStatus, + ]); const { data: shareInfo, @@ -79,13 +86,38 @@ const SharePopoverContent = memo(({ onOpenModal }) => const handleVisibilityChange = useCallback( (visibility: Visibility) => { - // Show confirmation when changing from private to link - if (currentVisibility === 'private' && visibility === 'link') { + // Show confirmation when changing from private to link (unless user has dismissed it) + if ( + currentVisibility === 'private' && + visibility === 'link' && + !hideTopicSharePrivacyWarning + ) { + let doNotShowAgain = false; + modal.confirm({ cancelText: t('cancel', { ns: 'common' }), - content: t('shareModal.popover.privacyWarning.content'), + centered: true, + content: ( +
+

{t('shareModal.popover.privacyWarning.content')}

+
+ { + doNotShowAgain = v; + }} + > + {t('shareModal.popover.privacyWarning.doNotShowAgain')} + +
+
+ ), okText: t('shareModal.popover.privacyWarning.confirm'), - onOk: () => updateVisibility(visibility), + onOk: () => { + if (doNotShowAgain) { + updateSystemStatus({ hideTopicSharePrivacyWarning: true }); + } + updateVisibility(visibility); + }, title: t('shareModal.popover.privacyWarning.title'), type: 'warning', }); @@ -93,7 +125,14 @@ const SharePopoverContent = memo(({ onOpenModal }) => updateVisibility(visibility); } }, - [currentVisibility, modal, t, updateVisibility], + [ + currentVisibility, + hideTopicSharePrivacyWarning, + modal, + t, + updateSystemStatus, + updateVisibility, + ], ); const handleCopyLink = useCallback(async () => { diff --git a/src/layout/GlobalProvider/useUserStateRedirect.ts b/src/layout/GlobalProvider/useUserStateRedirect.ts index e104016ee3..5ad55e93b2 100644 --- a/src/layout/GlobalProvider/useUserStateRedirect.ts +++ b/src/layout/GlobalProvider/useUserStateRedirect.ts @@ -70,7 +70,7 @@ export const useDesktopUserStateRedirect = () => { export const useWebUserStateRedirect = () => useCallback((state: UserInitializationState) => { - const pathname = window.location.pathname; + const { pathname } = window.location; if (state.isInWaitList === true) { redirectIfNotOn(pathname, '/waitlist'); return; @@ -81,6 +81,12 @@ export const useWebUserStateRedirect = () => return; } + // Redirect away from invite-code page if no longer required + if (pathname.startsWith('/invite-code')) { + window.location.href = '/'; + return; + } + if (!onboardingSelectors.needsOnboarding(state)) return; redirectIfNotOn(pathname, '/onboarding'); diff --git a/src/locales/default/chat.ts b/src/locales/default/chat.ts index 37c1222af4..cd6e5e49b3 100644 --- a/src/locales/default/chat.ts +++ b/src/locales/default/chat.ts @@ -326,6 +326,7 @@ export default { 'shareModal.popover.privacyWarning.confirm': 'I understand, continue', 'shareModal.popover.privacyWarning.content': "Please make sure your conversation doesn't contain any personal or sensitive information. You are responsible for any content you choose to share and its consequences.", + 'shareModal.popover.privacyWarning.doNotShowAgain': "Don't show this again", 'shareModal.popover.privacyWarning.title': 'Privacy Notice', 'shareModal.popover.title': 'Share Topic', 'shareModal.popover.visibility': 'Visibility', diff --git a/src/locales/default/error.ts b/src/locales/default/error.ts index 92c4ac3e19..d11b35e6b1 100644 --- a/src/locales/default/error.ts +++ b/src/locales/default/error.ts @@ -12,12 +12,19 @@ export default { 'import.incompatible.description': 'This file was exported from a higher version. Please try upgrading to the latest version and then re-importing.', 'import.incompatible.title': 'Current application does not support importing this file', + 'inviteCode.continue': 'Continue', 'inviteCode.currentEmail': 'Current account: {{email}}', 'inviteCode.desc': 'An invite code is required to access LobeHub. Please enter a valid invite code to continue.', - 'inviteCode.friends': 'Friends', - 'inviteCode.getCodeHint': 'Get an invite code from:', - 'inviteCode.title': 'Invite Code Required', + 'inviteCode.friends': 'Ask a friend', + 'inviteCode.joinUsOn': 'Join us on', + 'inviteCode.lookingForInvite': 'Looking for an invite?', + 'inviteCode.notYou': 'Not you?', + 'inviteCode.openingInStages': 'is opening in stages.', + 'inviteCode.placeholder': 'Invite code or link', + 'inviteCode.pleaseEnterCode': 'Enter your invite code to continue.', + 'inviteCode.switchAccount': 'Switch account', + 'inviteCode.title': "You're Almost In", 'loginRequired.desc': 'You will be redirected to the login page shortly', 'loginRequired.title': 'Please log in to use this feature', 'notFound.backHome': 'Back to Home', diff --git a/src/store/global/initialState.ts b/src/store/global/initialState.ts index 5739010f8d..deb62e4981 100644 --- a/src/store/global/initialState.ts +++ b/src/store/global/initialState.ts @@ -101,6 +101,7 @@ export interface SystemStatus { hideGemini2_5FlashImagePreviewChineseWarning?: boolean; hidePWAInstaller?: boolean; hideThreadLimitAlert?: boolean; + hideTopicSharePrivacyWarning?: boolean; imagePanelWidth: number; imageTopicPanelWidth?: number; /** @@ -212,6 +213,7 @@ export const INITIAL_STATUS = { hideGemini2_5FlashImagePreviewChineseWarning: false, hidePWAInstaller: false, hideThreadLimitAlert: false, + hideTopicSharePrivacyWarning: false, imagePanelWidth: 320, imageTopicPanelWidth: 80, knowledgeBaseModalViewMode: 'list' as const,