feat: share page improvements and pg17 docs update (#11850)

This commit is contained in:
YuTengjing
2026-01-26 21:05:47 +08:00
committed by GitHub
parent 47afaa6b65
commit 5b953b15cb
32 changed files with 249 additions and 87 deletions

View File

@@ -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

View File

@@ -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
```
- **注意事项**:确保分配足够的资源以处理向量操作

View File

@@ -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.

View File

@@ -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` 镜像。

View File

@@ -768,7 +768,7 @@ services:
- lobe-network
postgresql:
image: pgvector/pgvector:pg16
image: pgvector/pgvector:pg17
container_name: lobe-postgres
ports:
- '5432:5432'

View File

@@ -743,7 +743,7 @@ services:
- lobe-network
postgresql:
image: pgvector/pgvector:pg16
image: pgvector/pgvector:pg17
container_name: lobe-postgres
ports:
- '5432:5432'

View File

@@ -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

View File

@@ -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 的重要构件之一。

View File

@@ -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",

View File

@@ -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": "返回首页",

View File

@@ -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)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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',

View File

@@ -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,

View File

@@ -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',

View File

@@ -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',

View File

@@ -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:
'ByteDances 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',

View File

@@ -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 humanitys understanding of the universe.',
id: 'xai',

View File

@@ -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
*/

View File

@@ -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;

View File

@@ -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 (

View File

@@ -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',

View File

@@ -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>
);
},

View File

@@ -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;

View File

@@ -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();

View File

@@ -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 () => {

View File

@@ -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');

View File

@@ -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',

View File

@@ -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',

View File

@@ -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,