mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat: share page improvements and pg17 docs update (#11850)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
- **注意事项**:确保分配足够的资源以处理向量操作
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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` 镜像。
|
||||
|
||||
@@ -768,7 +768,7 @@ services:
|
||||
- lobe-network
|
||||
|
||||
postgresql:
|
||||
image: pgvector/pgvector:pg16
|
||||
image: pgvector/pgvector:pg17
|
||||
container_name: lobe-postgres
|
||||
ports:
|
||||
- '5432:5432'
|
||||
|
||||
@@ -743,7 +743,7 @@ services:
|
||||
- lobe-network
|
||||
|
||||
postgresql:
|
||||
image: pgvector/pgvector:pg16
|
||||
image: pgvector/pgvector:pg17
|
||||
container_name: lobe-postgres
|
||||
ports:
|
||||
- '5432:5432'
|
||||
|
||||
@@ -28,12 +28,6 @@ tags:
|
||||
Database](/docs/self-hosting/server-database) first.
|
||||
</Callout>
|
||||
|
||||
<Callout type="warning">
|
||||
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.
|
||||
</Callout>
|
||||
|
||||
## 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.
|
||||
|
||||
<Callout type="info">
|
||||
The pgvector plugin provides vector search capabilities for Postgres, which is an important
|
||||
|
||||
@@ -26,13 +26,6 @@ tags:
|
||||
存储服务](/zh/docs/self-hosting/advanced/s3/tencent-cloud)。
|
||||
</Callout>
|
||||
|
||||
<Callout type="warning">
|
||||
由于无法使用 Docker 环境变量暴露 `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`,使用 Docker / Docker Compose
|
||||
部署 LobeHub 时,你不能使用 Clerk 作为登录鉴权服务。
|
||||
|
||||
如果你确实需要 Clerk 作为登录鉴权服务,你可以考虑使用 Vercel 部署或者自行构建镜像。
|
||||
</Callout>
|
||||
|
||||
## 在 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 插件。
|
||||
|
||||
<Callout type="info">
|
||||
pgvector 插件为 Postgres 提供了向量搜索的能力,是 LobeHub 实现 RAG 的重要构件之一。
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "返回首页",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<ImageModelItemProps>(
|
||||
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;
|
||||
|
||||
@@ -46,7 +46,7 @@ const ModelSelect = memo(() => {
|
||||
const options = useMemo<SelectProps['options']>(() => {
|
||||
const getImageModels = (provider: EnabledProviderWithModels) => {
|
||||
const modelOptions = provider.children.map((model) => ({
|
||||
label: <ImageModelItem {...model} />,
|
||||
label: <ImageModelItem {...model} providerId={provider.id} />,
|
||||
provider: provider.id,
|
||||
value: `${provider.id}/${model.id}`,
|
||||
}));
|
||||
@@ -130,7 +130,14 @@ const ModelSelect = memo(() => {
|
||||
|
||||
if (!modelInfo) return props.label;
|
||||
|
||||
return <ImageModelItem {...modelInfo} showBadge={false} showPopover={false} />;
|
||||
return (
|
||||
<ImageModelItem
|
||||
{...modelInfo}
|
||||
providerId={modelInfo.providerId}
|
||||
showBadge={false}
|
||||
showPopover={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -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<ConnectionCheckerProps>(
|
||||
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<ConnectionCheckerProps>(
|
||||
</Flexbox>
|
||||
);
|
||||
}}
|
||||
options={totalModels.map((id) => ({ label: id, value: id }))}
|
||||
options={sortedModels.map((id) => ({ label: id, value: id }))}
|
||||
style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
|
||||
@@ -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<AssistantMessageExtraProps>(
|
||||
({ extra, id, content, performance, usage, tools, provider, model }) => {
|
||||
const loading = useConversationStore(messageStateSelectors.isMessageGenerating(id));
|
||||
const isLogin = useUserStore(authSelectors.isLogin);
|
||||
|
||||
return (
|
||||
<Flexbox gap={8} style={{ marginTop: !!tools?.length ? 8 : 4 }}>
|
||||
{content !== LOADING_FLAT && model && (
|
||||
<Usage model={model} performance={performance} provider={provider!} usage={usage} />
|
||||
)}
|
||||
<>
|
||||
{!!extra?.tts && (
|
||||
<ExtraContainer>
|
||||
<TTS content={content} id={id} loading={loading} {...extra?.tts} />
|
||||
</ExtraContainer>
|
||||
)}
|
||||
{!!extra?.translate && (
|
||||
<ExtraContainer>
|
||||
<Translate id={id} loading={loading} {...extra?.translate} />
|
||||
</ExtraContainer>
|
||||
)}
|
||||
</>
|
||||
{isLogin && (
|
||||
<>
|
||||
{!!extra?.tts && (
|
||||
<ExtraContainer>
|
||||
<TTS content={content} id={id} loading={loading} {...extra?.tts} />
|
||||
</ExtraContainer>
|
||||
)}
|
||||
{!!extra?.translate && (
|
||||
<ExtraContainer>
|
||||
<Translate id={id} loading={loading} {...extra?.translate} />
|
||||
</ExtraContainer>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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<UserMessageExtraProps>(({ 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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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<SharePopoverContentProps>(({ 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<SharePopoverContentProps>(({ 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: (
|
||||
<div>
|
||||
<p>{t('shareModal.popover.privacyWarning.content')}</p>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Checkbox
|
||||
onChange={(v) => {
|
||||
doNotShowAgain = v;
|
||||
}}
|
||||
>
|
||||
{t('shareModal.popover.privacyWarning.doNotShowAgain')}
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
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<SharePopoverContentProps>(({ onOpenModal }) =>
|
||||
updateVisibility(visibility);
|
||||
}
|
||||
},
|
||||
[currentVisibility, modal, t, updateVisibility],
|
||||
[
|
||||
currentVisibility,
|
||||
hideTopicSharePrivacyWarning,
|
||||
modal,
|
||||
t,
|
||||
updateSystemStatus,
|
||||
updateVisibility,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCopyLink = useCallback(async () => {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user