Compare commits

...

15 Commits

Author SHA1 Message Date
semantic-release-bot 2cedca58fe 🔖 chore(release): v2.0.0-next.73 [skip ci]
## [Version 2.0.0-next.73](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.72...v2.0.0-next.73)
<sup>Released on **2025-11-17**</sup>

####  Features

- **misc**: Support parallel topic agent runtime.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's improved

* **misc**: Support parallel topic agent runtime, closes [#10273](https://github.com/lobehub/lobe-chat/issues/10273) ([02eba3c](https://github.com/lobehub/lobe-chat/commit/02eba3c))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-11-17 16:11:54 +00:00
Arvin Xu 02eba3ce64 feat: support parallel topic agent runtime (#10273)
* add

* refactor to support split topic running

* refactor to support split topic running

* support loading

* fix tests

* fix tests

* fix tests

* fix getDbMessageById
2025-11-18 00:00:17 +08:00
lobehubbot 7461d4e486 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-17 13:06:05 +00:00
Shinji-Li f445ab013c ♻️ refactor: refactor the root from nextjs router to react-router-dom (#10094)
* feat: change the root path to react-router-dom to render spa

* feat: disable / to /chat rewrite

* feat: change /settings labs image profile changelog to spa mode

* feat: use loading to dynamic loading

* fix: change the goback & knowledge/base url

* feat: change some nextjs router to react-router-dom use

* feat: link replace to react-router-dom

* fix: delete useless code

* feat: fix mobile agent settings page not work problem

* fix: fix the test

* fix: slove the router back

* fix: slove ts problem

* fix: change the router judge by servers

* feat: change AppRouter to Desktop Router & mobile Router to dynamic import

* fix: refactor the memory router to browser router

* feat: /chat delete pages & layouts dir

* feat: change all discover page to the spa

* feat: discover pages layout & pages routers get done

* feat: change all routes to outer routes

* feat: change the :slug to react-router loader to get

* feat: change NextJs Link useRouter useSearchParams change to react-router way

* fix: delete some layout tsx & update the ts

* feat: change local params get use ReactRouter Outlet context

* fix: fix hydrateFallback problem

* fix: fix build problem

* fix: change the changelog pages render

* feat: delete all nuqs

* feat: change the mobile me layout back

* chore: add mobile me layout back

* fix: discover find more  link error fixed

* fix: add nuqs back & useQueryState back in oath

* fix: add files back

* fix: add files back

* feat: use starTransition to navigate url

* fix: close the loading in the layout loading

* chore: update test.ts in TopActions.tsx

* fix: delete useless code

* fix: fix mobile router goback fc

* fix: delete the changelog modal page

* feat: fix a lot router problem

* fix: fix useNav in discover page error problem

* feat: rollback some changes about layout

* fix: fixed the desktop knowledge page router

* fix: fixed usage router error

* fix: fixed router link error

* fix: fixed the url & new url not path problem

* fix: fixed the test

* feat: update the useQueryParams throttleMs params

* feat: use more simple way to update session hydration

* fix: delete useless code

* fix: delete uesless code

* fix: mobile chat settings go back

* fix: fix the reload was loading page problem

* fix: fixed the test error

* fix: add router ErrorBoundary

* test: test the loading error

* fix: try to fixed

* fix: test mobile

* feat: add loading back
2025-11-17 20:54:37 +08:00
lobehubbot f88e01e59b 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-17 11:56:28 +00:00
semantic-release-bot 8b5fc3656b 🔖 chore(release): v2.0.0-next.72 [skip ci]
## [Version&nbsp;2.0.0-next.72](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.71...v2.0.0-next.72)
<sup>Released on **2025-11-17**</sup>

#### 💄 Styles

- **misc**: Add model information for the Qiniu provider.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### Styles

* **misc**: Add model information for the Qiniu provider, closes [#10270](https://github.com/lobehub/lobe-chat/issues/10270) ([06af793](https://github.com/lobehub/lobe-chat/commit/06af793))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-11-17 11:55:22 +00:00
yliu7949 06af7939e4 💄 style: Add model information for the Qiniu provider (#10270)
style(): update qiniu.ts
2025-11-17 19:43:13 +08:00
lobehubbot e12965c7df 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-17 10:26:09 +00:00
semantic-release-bot 7afd1318db 🔖 chore(release): v2.0.0-next.71 [skip ci]
## [Version&nbsp;2.0.0-next.71](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.70...v2.0.0-next.71)
<sup>Released on **2025-11-17**</sup>

#### 🐛 Bug Fixes

- **misc**: Fix desktop user panel.

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

#### What's fixed

* **misc**: Fix desktop user panel, closes [#10272](https://github.com/lobehub/lobe-chat/issues/10272) ([6a374d2](https://github.com/lobehub/lobe-chat/commit/6a374d2))

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-11-17 10:24:55 +00:00
Arvin Xu 6a374d2f32 🐛 fix: fix desktop user panel (#10272)
fix desktop
2025-11-17 18:13:34 +08:00
renovate[bot] cec034721f Update opentelemetry-js monorepo to ^0.208.0 (#10253)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-17 13:08:48 +08:00
lobehubbot 2d70632d3e 📝 docs(bot): Auto sync agents & plugin to readme 2025-11-17 04:14:39 +00:00
semantic-release-bot 41c554d748 🔖 chore(release): v2.0.0-next.70 [skip ci]
## [Version&nbsp;2.0.0-next.70](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.69...v2.0.0-next.70)
<sup>Released on **2025-11-17**</sup>

<br/>

<details>
<summary><kbd>Improvements and Fixes</kbd></summary>

</details>

<div align="right">

[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)

</div>
2025-11-17 04:13:23 +00:00
LobeHub Bot 4e4933d861 🌐 chore: translate non-English comments to English in packages/types and packages/web-crawler (#10267)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-17 12:01:06 +08:00
René Wang a5bb31b844 ️ perf: improve Chat Screenshot and fix image geneartion (#10261)
* feat: Support narrow mode export

* feat: Replace `modern-screenshot` with `snapDom`

* feat: Add CORS proxy
2025-11-17 12:00:44 +08:00
358 changed files with 4604 additions and 3089 deletions
+92
View File
@@ -2,6 +2,98 @@
# Changelog
## [Version 2.0.0-next.73](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.72...v2.0.0-next.73)
<sup>Released on **2025-11-17**</sup>
#### ✨ Features
- **misc**: Support parallel topic agent runtime.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's improved
- **misc**: Support parallel topic agent runtime, closes [#10273](https://github.com/lobehub/lobe-chat/issues/10273) ([02eba3c](https://github.com/lobehub/lobe-chat/commit/02eba3c))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.0.0-next.72](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.71...v2.0.0-next.72)
<sup>Released on **2025-11-17**</sup>
#### 💄 Styles
- **misc**: Add model information for the Qiniu provider.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### Styles
- **misc**: Add model information for the Qiniu provider, closes [#10270](https://github.com/lobehub/lobe-chat/issues/10270) ([06af793](https://github.com/lobehub/lobe-chat/commit/06af793))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.0.0-next.71](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.70...v2.0.0-next.71)
<sup>Released on **2025-11-17**</sup>
#### 🐛 Bug Fixes
- **misc**: Fix desktop user panel.
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
#### What's fixed
- **misc**: Fix desktop user panel, closes [#10272](https://github.com/lobehub/lobe-chat/issues/10272) ([6a374d2](https://github.com/lobehub/lobe-chat/commit/6a374d2))
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.0.0-next.70](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.69...v2.0.0-next.70)
<sup>Released on **2025-11-17**</sup>
<br/>
<details>
<summary><kbd>Improvements and Fixes</kbd></summary>
</details>
<div align="right">
[![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
</div>
## [Version 2.0.0-next.69](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.68...v2.0.0-next.69)
<sup>Released on **2025-11-17**</sup>
+19
View File
@@ -1,4 +1,23 @@
[
{
"children": {
"improvements": ["Add model information for the Qiniu provider."]
},
"date": "2025-11-17",
"version": "2.0.0-next.72"
},
{
"children": {
"fixes": ["Fix desktop user panel."]
},
"date": "2025-11-17",
"version": "2.0.0-next.71"
},
{
"children": {},
"date": "2025-11-17",
"version": "2.0.0-next.70"
},
{
"children": {
"improvements": ["Remove language_model_settings and remove isDeprecatedEdition."]
+5
View File
@@ -330,6 +330,11 @@
"screenshot": "Screenshot",
"settings": "Export Settings",
"text": "Text",
"widthMode": {
"label": "Width Mode",
"narrow": "Narrow",
"wide": "Wide"
},
"withBackground": "Include Background Image",
"withFooter": "Include Footer",
"withPluginInfo": "Include Plugin Information",
+5
View File
@@ -330,6 +330,11 @@
"screenshot": "截图",
"settings": "导出设置",
"text": "文本",
"widthMode": {
"label": "宽度模式",
"narrow": "窄屏模式",
"wide": "宽屏模式"
},
"withBackground": "包含背景图片",
"withFooter": "包含页脚",
"withPluginInfo": "包含插件信息",
+5 -5
View File
@@ -249,11 +249,11 @@ const nextConfig: NextConfig = {
// permanent: true,
// source: '/settings',
// },
{
destination: '/chat',
permanent: false,
source: '/',
},
// {
// destination: '/chat',
// permanent: false,
// source: '/',
// },
{
destination: '/chat',
permanent: true,
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@lobehub/lobehub",
"version": "2.0.0-next.69",
"version": "2.0.0-next.73",
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
"keywords": [
"framework",
@@ -191,6 +191,7 @@
"@vercel/speed-insights": "^1.2.0",
"@virtuoso.dev/masonry": "^1.3.5",
"@xterm/xterm": "^5.5.0",
"@zumer/snapdom": "^1.9.14",
"ahooks": "^3.9.6",
"antd": "^5.28.1",
"antd-style": "^3.7.1",
@@ -228,7 +229,6 @@
"marked": "^16.4.2",
"mdast-util-to-markdown": "^2.1.2",
"model-bank": "workspace:*",
"modern-screenshot": "^4.6.6",
"nanoid": "^5.1.6",
"next": "^16.0.3",
"next-auth": "5.0.0-beta.30",
+126
View File
@@ -27,6 +27,132 @@ const qiniuChatModels: AIChatModelCard[] = [
id: 'deepseek-r1',
type: 'chat',
},
{
abilities: {
functionCall: true,
reasoning: true,
search: true,
},
contextWindowTokens: 204_800,
description: '专为高效编码与 Agent 工作流而生',
displayName: 'MiniMax M2',
enabled: true,
id: 'minimax/minimax-m2',
maxOutput: 131_072,
pricing: {
currency: 'CNY',
units: [
{ name: 'textInput', rate: 2.1, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 8.4, strategy: 'fixed', unit: 'millionTokens' },
],
},
releasedAt: '2025-10-27',
settings: {
searchImpl: 'params',
},
type: 'chat',
},
{
abilities: {
functionCall: true,
reasoning: true,
},
contextWindowTokens: 131_072,
description: '美团开源的专为对话交互和智能体任务优化的非思维型基础模型,在工具调用和复杂多轮交互场景中表现突出',
displayName: 'LongCat Flash Chat',
enabled: true,
id: 'meituan/longcat-flash-chat',
maxOutput: 65536,
pricing: {
currency: 'CNY',
units: [
{ name: 'textInput', rate: 0, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 0, strategy: 'fixed', unit: 'millionTokens' },
],
},
releasedAt: '2025-09-01',
settings: {
extendParams: ['enableReasoning'],
searchImpl: 'params',
},
type: 'chat',
},
{
abilities: {
functionCall: true,
reasoning: true,
search: true,
},
contextWindowTokens: 200_000,
description: '智谱最新旗舰模型 GLM-4.6,在高级编码、长文本处理、推理与智能体能力上全面超越前代。',
displayName: 'GLM-4.6',
enabled: true,
id: 'z-ai/glm-4.6',
maxOutput: 128_000,
pricing: {
currency: 'CNY',
units: [
{ name: 'textInput', rate: 7.2, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 12.6, strategy: 'fixed', unit: 'millionTokens' },
],
},
releasedAt: '2025-09-30',
settings: {
extendParams: ['enableReasoning'],
searchImpl: 'params',
},
type: 'chat',
},
{
abilities: {
functionCall: true,
reasoning: true,
search: true,
vision: true,
},
contextWindowTokens: 2_000_000,
description:
'我们很高兴发布 Grok 4 Fast,这是我们在成本效益推理模型方面的最新进展。',
displayName: 'Grok 4 Fast',
enabled: true,
id: 'x-ai/grok-4-fast',
pricing: {
currency: 'CNY',
units: [
{ name: 'textInput', rate: 7.2, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 12.6, strategy: 'fixed', unit: 'millionTokens' },
],
},
releasedAt: '2025-09-09',
settings: {
searchImpl: 'params',
},
type: 'chat',
},
{
abilities: {
functionCall: true,
reasoning: true,
},
contextWindowTokens: 256_000,
description:
'我们很高兴推出 grok-code-fast-1,这是一款快速且经济高效的推理模型,在代理编码方面表现出色。',
displayName: 'Grok Code Fast 1',
id: 'x-ai/grok-code-fast-1',
pricing: {
units: [
{ name: 'textInput_cacheRead', rate: 0.02, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textInput', rate: 0.2, strategy: 'fixed', unit: 'millionTokens' },
{ name: 'textOutput', rate: 1.5, strategy: 'fixed', unit: 'millionTokens' },
],
},
releasedAt: '2025-08-27',
// settings: {
// reasoning_effort is not supported by grok-code. Specifying reasoning_effort parameter will get an error response.
// extendParams: ['reasoningEffort'],
// },
type: 'chat',
},
];
export const allModels = [...qiniuChatModels];
+5 -5
View File
@@ -10,14 +10,14 @@
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.66.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.207.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.207.0",
"@opentelemetry/instrumentation": "^0.207.0",
"@opentelemetry/instrumentation-http": "^0.207.0",
"@opentelemetry/exporter-metrics-otlp-http": "^0.208.0",
"@opentelemetry/exporter-trace-otlp-http": "^0.208.0",
"@opentelemetry/instrumentation": "^0.208.0",
"@opentelemetry/instrumentation-http": "^0.208.0",
"@opentelemetry/instrumentation-pg": "^0.60.0",
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-metrics": "^2.2.0",
"@opentelemetry/sdk-node": "^0.207.0",
"@opentelemetry/sdk-node": "^0.208.0",
"@opentelemetry/sdk-trace-node": "^2.2.0",
"@opentelemetry/semantic-conventions": "^1.38.0",
"@vercel/otel": "^2.1.0"
+8 -8
View File
@@ -17,16 +17,16 @@ export interface LobeAgentChatConfig {
enableMaxTokens?: boolean;
/**
* 是否开启流式输出
* Whether to enable streaming output
*/
enableStreaming?: boolean;
/**
* 是否开启推理
* Whether to enable reasoning
*/
enableReasoning?: boolean;
/**
* 自定义推理强度
* Custom reasoning effort level
*/
enableReasoningEffort?: boolean;
reasoningBudgetToken?: number;
@@ -34,25 +34,25 @@ export interface LobeAgentChatConfig {
gpt5ReasoningEffort?: 'minimal' | 'low' | 'medium' | 'high';
gpt5_1ReasoningEffort?: 'none' | 'low' | 'medium' | 'high';
/**
* 输出文本详细程度控制
* Output text verbosity control
*/
textVerbosity?: 'low' | 'medium' | 'high';
thinking?: 'disabled' | 'auto' | 'enabled';
thinkingBudget?: number;
/**
* 禁用上下文缓存
* Disable context caching
*/
disableContextCaching?: boolean;
/**
* 历史消息条数
* Number of historical messages
*/
historyCount?: number;
/**
* 开启历史记录条数
* Enable historical message count
*/
enableHistoryCount?: boolean;
/**
* 历史消息长度压缩阈值
* Enable history message compression threshold
*/
enableCompressHistory?: boolean;
+4 -4
View File
@@ -56,12 +56,12 @@ export interface LobeDocument {
source: string;
/**
* 文档来源类型
* Document source type
*/
sourceType: DocumentSourceType;
/**
* 文档标题 (如果可用)。
* Document title (if available)
*/
title?: string;
@@ -168,12 +168,12 @@ export enum DocumentSourceType {
API = 'api',
/**
* 编辑器创建的文档
* Document created in editor
*/
EDITOR = 'editor',
/**
* 本地或上传的文件
* Local or uploaded file
*/
FILE = 'file',
+9 -9
View File
@@ -2,15 +2,15 @@
import type { ILobeAgentRuntimeErrorType } from '@lobechat/model-runtime';
export const ChatErrorType = {
// ******* 业务错误语义 ******* //
// ******* Business Error Semantics ******* //
InvalidAccessCode: 'InvalidAccessCode', // is in valid password
InvalidClerkUser: 'InvalidClerkUser', // is not Clerk User
FreePlanLimit: 'FreePlanLimit', // is not Clerk User
SubscriptionPlanLimit: 'SubscriptionPlanLimit', // 订阅用户超限
SubscriptionKeyMismatch: 'SubscriptionKeyMismatch', // 订阅 key 不匹配
SubscriptionPlanLimit: 'SubscriptionPlanLimit', // Subscription user limit exceeded
SubscriptionKeyMismatch: 'SubscriptionKeyMismatch', // Subscription key mismatch
SupervisorDecisionFailed: 'SupervisorDecisionFailed', // 主持人决策失败
SupervisorDecisionFailed: 'SupervisorDecisionFailed', // Supervisor decision failed
InvalidUserKey: 'InvalidUserKey', // is not valid User key
CreateMessageError: 'CreateMessageError',
@@ -18,20 +18,20 @@ export const ChatErrorType = {
* @deprecated
*/
NoOpenAIAPIKey: 'NoOpenAIAPIKey',
OllamaServiceUnavailable: 'OllamaServiceUnavailable', // 未启动/检测到 Ollama 服务
OllamaServiceUnavailable: 'OllamaServiceUnavailable', // Ollama service not started/detected
PluginFailToTransformArguments: 'PluginFailToTransformArguments',
UnknownChatFetchError: 'UnknownChatFetchError',
SystemTimeNotMatchError: 'SystemTimeNotMatchError',
// ******* 客户端错误 ******* //
// ******* Client Errors ******* //
BadRequest: 400,
Unauthorized: 401,
Forbidden: 403,
ContentNotFound: 404, // 没找到接口
MethodNotAllowed: 405, // 不支持
ContentNotFound: 404, // Endpoint not found
MethodNotAllowed: 405, // Method not supported
TooManyRequests: 429,
// ******* 服务端错误 ******* //InvalidPluginArgumentsTransform
// ******* Server Errors ******* //InvalidPluginArgumentsTransform
InternalServerError: 500,
BadGateway: 502,
ServiceUnavailable: 503,
+5 -5
View File
@@ -84,7 +84,7 @@ export const HotkeyGroupEnum = {
export const HotkeyScopeEnum = {
Chat: 'chat',
Files: 'files',
// 默认全局注册的快捷键 scope
// Default globally registered hotkey scope
// https://react-hotkeys-hook.vercel.app/docs/documentation/hotkeys-provider
Global: 'global',
@@ -96,13 +96,13 @@ export type HotkeyGroupId = (typeof HotkeyGroupEnum)[keyof typeof HotkeyGroupEnu
export type HotkeyScopeId = (typeof HotkeyScopeEnum)[keyof typeof HotkeyScopeEnum];
export interface HotkeyItem {
// 快捷键分组用于展示
// Hotkey grouping for display purposes
group: HotkeyGroupId;
id: HotkeyId;
keys: string;
// 是否为不可编辑的快捷键
// Whether the hotkey is non-editable
nonEditable?: boolean;
// 快捷键作用域
// Hotkey scope
scopes?: HotkeyScopeId[];
}
@@ -119,7 +119,7 @@ export interface DesktopHotkeyItem {
id: DesktopHotkeyId;
keys: string;
// 是否为不可编辑的快捷键
// Whether the hotkey is non-editable
nonEditable?: boolean;
}
+2 -2
View File
@@ -26,11 +26,11 @@ export interface ImportMessage {
createdAt: number;
error?: ChatMessageError;
// 扩展字段
// Extended fields
extra?: {
model?: string;
provider?: string;
// 翻译
// Translation
translate?: ChatTranslate | false | null;
// TTS
tts?: ChatTTS;
@@ -109,8 +109,8 @@ export interface MessageMetadata extends ModelUsage, ModelPerformance {
activeBranchIndex?: number;
activeColumn?: boolean;
/**
* 消息折叠状态
* true: 折叠, false/undefined: 展开
* Message collapse state
* true: collapsed, false/undefined: expanded
*/
collapsed?: boolean;
compare?: boolean;
+1 -1
View File
@@ -112,7 +112,7 @@ export const ChatToolPayloadSchema = z.object({
});
/**
* 聊天消息错误对象
* Chat message error object
*/
export interface ChatMessagePluginError {
body?: any;
+3 -3
View File
@@ -2,11 +2,11 @@ import { z } from 'zod';
export const LobeMetaDataSchema = z.object({
/**
* 角色头像
* Character avatar
*/
avatar: z.string().optional(),
/**
* 背景色
* Background color
*/
backgroundColor: z.string().optional(),
description: z.string().optional(),
@@ -17,7 +17,7 @@ export const LobeMetaDataSchema = z.object({
tags: z.array(z.string()).optional(),
/**
* 名称
* Name
*/
title: z.string().optional(),
});
+1 -1
View File
@@ -1,6 +1,6 @@
import type { BaseDataModel } from '../meta';
// 类型定义
// Type definitions
export type TimeGroupId =
| 'today'
| 'yesterday'
+4 -4
View File
@@ -16,7 +16,7 @@ export interface CrawlErrorResult {
}
export interface FilterOptions {
// 是否启用Readability
// Whether to enable Readability
enableReadability?: boolean;
pureText?: boolean;
@@ -34,12 +34,12 @@ export type CrawlImpl<Params = object> = (
) => Promise<CrawlSuccessResult | undefined>;
export interface CrawlUrlRule {
// 内容过滤配置(可选)
// Content filtering configuration (optional)
filterOptions?: FilterOptions;
impls?: CrawlImplType[];
// URL匹配模式,仅支持正则表达式
// URL matching pattern, only supports regular expressions
urlPattern: string;
// URL转换模板(可选),如果提供则进行URL转换
// URL transformation template (optional), performs URL conversion if provided
urlTransform?: string;
}
+1 -1
View File
@@ -13,7 +13,7 @@ const partialBuildPages = [
{
name: 'changelog',
disabled: isDesktop,
paths: ['src/app/[variants]/@modal/(.)changelog', 'src/app/[variants]/(main)/changelog'],
paths: ['src/app/[variants]/(main)/changelog'],
},
{
name: 'auth',
@@ -17,12 +17,12 @@ const handler = (req: NextRequest) => {
*/
createContext: () => createLambdaContext(req),
endpoint: '/trpc/desktop',
endpoint: '/trpc/desktop',
onError: ({ error, path, type }) => {
pino.info(`Error in tRPC handler (desktop) on path: ${path}, type: ${type}`);
console.error(error);
},
onError: ({ error, path, type }) => {
pino.info(`Error in tRPC handler (desktop) on path: ${path}, type: ${type}`);
console.error(error);
},
req: preparedReq,
responseMeta({ ctx }) {
@@ -34,4 +34,4 @@ const handler = (req: NextRequest) => {
});
};
export { handler as GET, handler as POST };
export { handler as GET, handler as POST };
@@ -6,10 +6,12 @@ import { useUserStore } from '@/store/user';
import UserBanner from '../features/UserBanner';
// Mock dependencies
vi.mock('next/navigation', () => ({
useRouter: vi.fn(() => ({
push: vi.fn(),
})),
const mockNavigate = vi.fn();
vi.mock('react-router-dom', () => ({
Link: ({ to, children }: { to: string; children: React.ReactNode }) => (
<a href={to}>{children}</a>
),
useNavigate: () => mockNavigate,
}));
vi.mock('@/features/User/UserInfo', () => ({
@@ -45,6 +47,7 @@ vi.mock('@/const/auth', () => ({
afterEach(() => {
enableAuth = true;
mockNavigate.mockReset();
});
describe('UserBanner', () => {
@@ -11,10 +11,9 @@ const wrapper: React.JSXElementConstructor<{ children: React.ReactNode }> = ({ c
);
// Mock dependencies
vi.mock('next/navigation', () => ({
useRouter: vi.fn(() => ({
push: vi.fn(),
})),
const mockNavigate = vi.fn();
vi.mock('react-router-dom', () => ({
useNavigate: () => mockNavigate,
}));
vi.mock('react-i18next', () => ({
@@ -48,6 +47,7 @@ vi.mock('@/const/version', async (importOriginal) => {
afterEach(() => {
enableAuth = true;
enableClerk = true;
mockNavigate.mockReset();
});
describe('useCategory', () => {
@@ -1,8 +1,7 @@
'use client';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { Flexbox } from 'react-layout-kit';
import { enableAuth, enableNextAuth } from '@/const/auth';
@@ -13,7 +12,7 @@ import { useUserStore } from '@/store/user';
import { authSelectors } from '@/store/user/selectors';
const UserBanner = memo(() => {
const router = useRouter();
const navigate = useNavigate();
const isLoginWithAuth = useUserStore(authSelectors.isLoginWithAuth);
const [signIn] = useUserStore((s) => [s.openLogin]);
@@ -21,10 +20,10 @@ const UserBanner = memo(() => {
<Flexbox gap={12} paddingBlock={8}>
{!enableAuth || (enableAuth && isLoginWithAuth) ? (
<>
<Link href={'/profile'} style={{ color: 'inherit' }}>
<Link style={{ color: 'inherit' }} to="/profile">
<UserInfo />
</Link>
<Link href={'/profile/stats'} style={{ color: 'inherit' }}>
<Link style={{ color: 'inherit' }} to="/profile/stats">
<DataStatistics paddingInline={12} />
</Link>
</>
@@ -36,7 +35,7 @@ const UserBanner = memo(() => {
signIn();
return;
}
router.push('/login');
navigate('/login');
}}
/>
)}
@@ -8,8 +8,8 @@ import {
FileClockIcon,
Settings2,
} from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { CellProps } from '@/components/Cell';
import { usePWAInstall } from '@/hooks/usePWAInstall';
@@ -18,7 +18,7 @@ import { useUserStore } from '@/store/user';
import { authSelectors } from '@/store/user/selectors';
export const useCategory = () => {
const router = useRouter();
const navigate = useNavigate();
const { canInstall, install } = usePWAInstall();
const { t } = useTranslation(['common', 'setting', 'auth']);
const { showCloudPromotion, hideDocs } = useServerConfigStore(featureFlagsSelectors);
@@ -29,7 +29,7 @@ export const useCategory = () => {
icon: CircleUserRound,
key: 'profile',
label: t('userPanel.profile'),
onClick: () => router.push('/me/profile'),
onClick: () => navigate('/me/profile'),
},
];
@@ -38,7 +38,7 @@ export const useCategory = () => {
icon: Settings2,
key: 'setting',
label: t('userPanel.setting'),
onClick: () => router.push('/me/settings'),
onClick: () => navigate('/me/settings'),
},
{
type: 'divider',
@@ -58,9 +58,6 @@ export const useCategory = () => {
];
/* ↓ cloud slot ↓ */
/* ↑ cloud slot ↑ */
const helps: CellProps[] = [
showCloudPromotion && {
icon: Cloudy,
@@ -84,7 +81,7 @@ export const useCategory = () => {
icon: FileClockIcon,
key: 'changelog',
label: t('changelog'),
onClick: () => router.push('/changelog'),
onClick: () => navigate('/changelog'),
},
].filter(Boolean) as CellProps[];
@@ -0,0 +1,25 @@
'use client';
import { memo } from 'react';
import { Center } from 'react-layout-kit';
import BrandWatermark from '@/components/BrandWatermark';
import Category from './features/Category';
import UserBanner from './features/UserBanner';
const MeHomePage = memo(() => {
return (
<>
<UserBanner />
<Category />
<Center padding={16}>
<BrandWatermark />
</Center>
</>
);
});
MeHomePage.displayName = 'MeHomePage';
export default MeHomePage;
@@ -1,18 +1,15 @@
import { PropsWithChildren, Suspense } from 'react';
import MobileContentLayout from "@/components/server/MobileNavLayout";
import Loading from "@/components/Loading/BrandTextLoading";
import { Outlet } from "react-router-dom";
import Header from "./features/Header";
import { Suspense } from "react";
import Loading from '@/components/Loading/BrandTextLoading';
import MobileContentLayout from '@/components/server/MobileNavLayout';
import Header from './features/Header';
const Layout = ({ children }: PropsWithChildren) => {
return (
<MobileContentLayout header={<Header />} withNav>
<Suspense fallback={<Loading />}>{children}</Suspense>
const Layout = () => {
return <MobileContentLayout header={<Header />} withNav>
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
</MobileContentLayout>
);
};
}
Layout.displayName = 'MeLayout';
export default Layout;
export default Layout;
@@ -1,38 +0,0 @@
'use client';
import { Skeleton } from 'antd';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import Divider from '@/components/Cell/Divider';
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
const Loading = memo(() => {
return (
<>
<Flexbox align={'center'} gap={12} horizontal paddingBlock={12} paddingInline={12}>
<Skeleton.Avatar active shape={'circle'} size={48} />
<Skeleton.Button active block />
</Flexbox>
<Flexbox gap={4} horizontal paddingBlock={12} paddingInline={16}>
<Skeleton.Button active block />
<Skeleton.Button active block />
<Skeleton.Button active block />
</Flexbox>
<Divider />
<SkeletonLoading
active
paragraph={{ rows: 6, style: { marginBottom: 0 }, width: '100%' }}
title={false}
/>
<Divider />
<SkeletonLoading
active
paragraph={{ rows: 3, style: { marginBottom: 0 }, width: '100%' }}
title={false}
/>
</>
);
});
export default Loading;
@@ -1,40 +0,0 @@
import { redirect } from 'next/navigation';
import { Center } from 'react-layout-kit';
import BrandWatermark from '@/components/BrandWatermark';
import { metadataModule } from '@/server/metadata';
import { translation } from '@/server/translation';
import { DynamicLayoutProps } from '@/types/next';
import { RouteVariants } from '@/utils/server/routeVariants';
import Category from './features/Category';
import UserBanner from './features/UserBanner';
export const generateMetadata = async (props: DynamicLayoutProps) => {
const locale = await RouteVariants.getLocale(props);
const { t } = await translation('common', locale);
return metadataModule.generate({
title: t('tab.me'),
url: '/me',
});
};
const Page = async (props: DynamicLayoutProps) => {
const isMobile = await RouteVariants.getIsMobile(props);
if (!isMobile) return redirect('/chat');
return (
<>
<UserBanner />
<Category />
<Center padding={16}>
<BrandWatermark />
</Center>
</>
);
};
Page.displayName = 'Me';
export default Page;
@@ -1,9 +1,9 @@
'use client';
import { ChartColumnBigIcon, LogOut, ShieldCheck, UserCircle } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import Cell, { CellProps } from '@/components/Cell';
import { ProfileTabs } from '@/store/global/initialState';
@@ -16,26 +16,26 @@ const Category = memo(() => {
authSelectors.isLoginWithClerk(s),
s.logout,
]);
const router = useRouter();
const navigate = useNavigate();
const { t } = useTranslation('auth');
const items: CellProps[] = [
{
icon: UserCircle,
key: ProfileTabs.Profile,
label: t('tab.profile'),
onClick: () => router.push('/profile'),
onClick: () => navigate('/profile'),
},
isLoginWithClerk && {
icon: ShieldCheck,
key: ProfileTabs.Security,
label: t('tab.security'),
onClick: () => router.push('/profile/security'),
onClick: () => navigate('/profile/security'),
},
{
icon: ChartColumnBigIcon,
key: ProfileTabs.Stats,
label: t('tab.stats'),
onClick: () => router.push('/profile/stats'),
onClick: () => navigate('/profile/stats'),
},
isLogin && {
type: 'divider',
@@ -46,7 +46,7 @@ const Category = memo(() => {
label: t('signout', { ns: 'auth' }),
onClick: () => {
signOut();
router.push('/login');
navigate('/login');
},
},
].filter(Boolean) as CellProps[];
@@ -1,9 +1,9 @@
'use client';
import { ChatHeader } from '@lobehub/ui/mobile';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Flexbox } from 'react-layout-kit';
import { mobileHeaderSticky } from '@/styles/mobileHeader';
@@ -11,7 +11,7 @@ import { mobileHeaderSticky } from '@/styles/mobileHeader';
const Header = memo(() => {
const { t } = useTranslation('common');
const router = useRouter();
const navigate = useNavigate();
return (
<ChatHeader
center={
@@ -23,7 +23,7 @@ const Header = memo(() => {
}
/>
}
onBackClick={() => router.push('/me')}
onBackClick={() => navigate('/me')}
showBackButton
style={mobileHeaderSticky}
/>
@@ -0,0 +1,16 @@
'use client';
import { memo } from 'react';
import Category from './features/Category';
const MeProfilePage = memo(() => {
return (
<Category />
);
});
MeProfilePage.displayName = 'MeProfilePage';
export default MeProfilePage;
@@ -1,13 +1,11 @@
import { PropsWithChildren } from 'react';
import MobileContentLayout from "@/components/server/MobileNavLayout";
import { Outlet } from "react-router-dom";
import Header from "./features/Header";
import MobileContentLayout from '@/components/server/MobileNavLayout';
const Layout = () => {
return <MobileContentLayout header={<Header />}>
<Outlet />
</MobileContentLayout>
}
import Header from './features/Header';
const Layout = ({ children }: PropsWithChildren) => {
return <MobileContentLayout header={<Header />}>{children}</MobileContentLayout>;
};
Layout.displayName = 'MeProfileLayout';
export default Layout;
export default Layout;
@@ -1,5 +0,0 @@
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
export default () => {
return <SkeletonLoading paragraph={{ rows: 8 }} />;
};
@@ -1,30 +0,0 @@
import { redirect } from 'next/navigation';
import { metadataModule } from '@/server/metadata';
import { translation } from '@/server/translation';
import { DynamicLayoutProps } from '@/types/next';
import { RouteVariants } from '@/utils/server/routeVariants';
import Category from './features/Category';
export const generateMetadata = async (props: DynamicLayoutProps) => {
const locale = await RouteVariants.getLocale(props);
const { t } = await translation('auth', locale);
return metadataModule.generate({
description: t('header.desc'),
title: t('header.title'),
url: '/me/profile',
});
};
const Page = async (props: DynamicLayoutProps) => {
const isMobile = await RouteVariants.getIsMobile(props);
if (!isMobile) return redirect('/profile');
return <Category />;
};
Page.displayName = 'MeProfile';
export default Page;
@@ -1,9 +1,9 @@
'use client';
import { ChatHeader } from '@lobehub/ui/mobile';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { Flexbox } from 'react-layout-kit';
import { mobileHeaderSticky } from '@/styles/mobileHeader';
@@ -11,7 +11,7 @@ import { mobileHeaderSticky } from '@/styles/mobileHeader';
const Header = memo(() => {
const { t } = useTranslation('common');
const router = useRouter();
const navigate = useNavigate();
return (
<ChatHeader
center={
@@ -23,7 +23,7 @@ const Header = memo(() => {
}
/>
}
onBackClick={() => router.push('/me')}
onBackClick={() => navigate('/me')}
showBackButton
style={mobileHeaderSticky}
/>
@@ -1,12 +1,11 @@
import { Bot, Brain, Info, Mic2, Settings2, Sparkles } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { CellProps } from '@/components/Cell';
import { SettingsTabs } from '@/store/global/initialState';
export const useCategory = () => {
const router = useRouter();
const navigate = useNavigate();
const { t } = useTranslation('setting');
const items: CellProps[] = [
@@ -40,6 +39,6 @@ export const useCategory = () => {
return items.map((item) => ({
...item,
onClick: () => router.push(`/settings?active=${item.key}`),
onClick: () => navigate(`/settings?active=${item.key}`),
}));
};
@@ -0,0 +1,16 @@
'use client';
import { memo } from 'react';
import Category from './features/Category';
const MeSettingsPage = memo(() => {
return (
<Category />
);
});
MeSettingsPage.displayName = 'MeSettingsPage';
export default MeSettingsPage;
@@ -1,13 +1,15 @@
import { PropsWithChildren } from 'react';
import MobileContentLayout from "@/components/server/MobileNavLayout";
import Loading from "@/components/Loading/BrandTextLoading";
import { Outlet } from "react-router-dom";
import Header from "./features/Header";
import { Suspense } from "react";
import MobileContentLayout from '@/components/server/MobileNavLayout';
const Layout = () => {
return <MobileContentLayout header={<Header />} withNav>
<Suspense fallback={<Loading />}>
<Outlet />
</Suspense>
</MobileContentLayout>
}
import Header from './features/Header';
const Layout = ({ children }: PropsWithChildren) => {
return <MobileContentLayout header={<Header />}>{children}</MobileContentLayout>;
};
Layout.displayName = 'MeSettingsLayout';
export default Layout;
export default Layout;
@@ -1,5 +0,0 @@
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
export default () => {
return <SkeletonLoading paragraph={{ rows: 8 }} />;
};
@@ -1,30 +0,0 @@
import { redirect } from 'next/navigation';
import { metadataModule } from '@/server/metadata';
import { translation } from '@/server/translation';
import { DynamicLayoutProps } from '@/types/next';
import { RouteVariants } from '@/utils/server/routeVariants';
import Category from './features/Category';
export const generateMetadata = async (props: DynamicLayoutProps) => {
const locale = await RouteVariants.getLocale(props);
const { t } = await translation('setting', locale);
return metadataModule.generate({
description: t('header.desc'),
title: t('header.title'),
url: '/me/settings',
});
};
const Page = async (props: DynamicLayoutProps) => {
const isMobile = await RouteVariants.getIsMobile(props);
if (!isMobile) return redirect('/settings');
return <Category />;
};
Page.displayName = 'MeSettings';
export default Page;
@@ -1,106 +0,0 @@
import { ActionIcon, ActionIconProps, Hotkey } from '@lobehub/ui';
import { Compass, FolderClosed, MessageSquare, Palette } from 'lucide-react';
import Link from 'next/link';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { useGlobalStore } from '@/store/global';
import { SidebarTabKey } from '@/store/global/initialState';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
import { useSessionStore } from '@/store/session';
import { useUserStore } from '@/store/user';
import { settingsSelectors } from '@/store/user/selectors';
import { HotkeyEnum } from '@/types/hotkey';
const ICON_SIZE: ActionIconProps['size'] = {
blockSize: 40,
size: 24,
strokeWidth: 2,
};
export interface TopActionProps {
isPinned?: boolean | null;
tab?: SidebarTabKey;
}
// TODO Change icons
const TopActions = memo<TopActionProps>(({ tab, isPinned }) => {
const { t } = useTranslation('common');
const switchBackToChat = useGlobalStore((s) => s.switchBackToChat);
const { showMarket, enableKnowledgeBase, showAiImage } =
useServerConfigStore(featureFlagsSelectors);
const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.NavigateToChat));
const isChatActive = tab === SidebarTabKey.Chat && !isPinned;
const isFilesActive = tab === SidebarTabKey.Files;
const isDiscoverActive = tab === SidebarTabKey.Discover;
const isImageActive = tab === SidebarTabKey.Image;
return (
<Flexbox gap={8}>
<Link
aria-label={t('tab.chat')}
href={'/chat'}
onClick={(e) => {
// If Cmd key is pressed, let the default link behavior happen (open in new tab)
if (e.metaKey || e.ctrlKey) {
return;
}
// Otherwise, prevent default and switch session within the current tab
e.preventDefault();
switchBackToChat(useSessionStore.getState().activeId);
}}
>
<ActionIcon
active={isChatActive}
icon={MessageSquare}
size={ICON_SIZE}
title={
<Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
<span>{t('tab.chat')}</span>
<Hotkey inverseTheme keys={hotkey} />
</Flexbox>
}
tooltipProps={{ placement: 'right' }}
/>
</Link>
{enableKnowledgeBase && (
<Link aria-label={t('tab.knowledgeBase')} href={'/knowledge'}>
<ActionIcon
active={isFilesActive}
icon={FolderClosed}
size={ICON_SIZE}
title={t('tab.knowledgeBase')}
tooltipProps={{ placement: 'right' }}
/>
</Link>
)}
{showAiImage && (
<Link aria-label={t('tab.aiImage')} href={'/image'}>
<ActionIcon
active={isImageActive}
icon={Palette}
size={ICON_SIZE}
title={t('tab.aiImage')}
tooltipProps={{ placement: 'right' }}
/>
</Link>
)}
{showMarket && (
<Link aria-label={t('tab.discover')} href={'/discover'}>
<ActionIcon
active={isDiscoverActive}
icon={Compass}
size={ICON_SIZE}
title={t('tab.discover')}
tooltipProps={{ placement: 'right' }}
/>
</Link>
)}
</Flexbox>
);
});
export default TopActions;
@@ -1,15 +1,16 @@
import { ReactNode } from 'react';
import { Outlet } from 'react-router-dom';
import { Locales } from '@/locales/resources';
import Hero from '../../features/Hero';
import Container from './Container';
type Props = { children: ReactNode };
const Layout = ({ children }: Props) => {
const Layout = (props: { locale: Locales }) => {
const { locale } = props;
return (
<Container>
<Hero />
{children}
<Outlet context={{ locale }} />
</Container>
);
};
@@ -1,17 +1,17 @@
'use client';
import { ChatHeader } from '@lobehub/ui/mobile';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import { useNavigate } from 'react-router-dom';
import { mobileHeaderSticky } from '@/styles/mobileHeader';
const Header = memo(() => {
const { t } = useTranslation('changelog');
const router = useRouter();
const navigate = useNavigate();
return (
<ChatHeader
center={
@@ -23,7 +23,7 @@ const Header = memo(() => {
}
/>
}
onBackClick={() => router.back()}
onBackClick={() => navigate(-1)}
showBackButton
style={mobileHeaderSticky}
/>
@@ -1,17 +1,17 @@
import { ReactNode } from 'react';
import { Outlet } from 'react-router-dom';
import MobileContentLayout from '@/components/server/MobileNavLayout';
import { Locales } from '@/locales/resources';
import Hero from '../../features/Hero';
import Header from './Header';
type Props = { children: ReactNode };
const Layout = ({ children }: Props) => {
const Layout = (props: { locale: Locales }) => {
const { locale } = props;
return (
<MobileContentLayout header={<Header />} padding={16}>
<Hero />
{children}
<Outlet context={{ locale }} />
</MobileContentLayout>
);
};
@@ -1,10 +1,11 @@
import { Typography } from '@lobehub/ui';
import { Image } from '@lobehub/ui/mdx';
import { Divider } from 'antd';
import Link from 'next/link';
import useSWR from 'swr';
import urlJoin from 'url-join';
import { CustomMDX } from '@/components/mdx';
import Image from '@/components/mdx/Image';
import { OFFICIAL_SITE } from '@/const/url';
import { Locales } from '@/locales/resources';
import { ChangelogService } from '@/server/services/changelog';
@@ -14,14 +15,16 @@ import GridLayout from './GridLayout';
import PublishedTime from './PublishedTime';
import VersionTag from './VersionTag';
const Post = async ({
const Post = ({
id,
mobile,
versionRange,
locale,
}: ChangelogIndexItem & { branch?: string; locale: Locales; mobile?: boolean }) => {
const changelogService = new ChangelogService();
const data = await changelogService.getPostById(id, { locale });
const { data } = useSWR([`changelog-post-${id}`, locale], async () => {
const changelogService = new ChangelogService();
return await changelogService.getPostById(id, { locale });
});
if (!data || !data.title) return null;
@@ -0,0 +1,55 @@
import { Fragment } from 'react';
import { Flexbox } from 'react-layout-kit';
import { useOutletContext } from 'react-router-dom';
import useSWR from 'swr';
import NotFound from '@/components/404';
import { Locales } from '@/locales/resources';
import { ChangelogService } from '@/server/services/changelog';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
import GridLayout from './features/GridLayout';
import Pagination from './features/Pagination';
import Post from './features/Post';
import UpdateChangelogStatus from './features/UpdateChangelogStatus';
const Page = (props: { isMobile: boolean }) => {
const { locale } = useOutletContext<{ locale: Locales }>();
const { isMobile } = props;
const { hideDocs } = useServerConfigStore(featureFlagsSelectors);
const { data } = useSWR('changelog-index', async () => {
const changelogService = new ChangelogService();
return await changelogService.getChangelogIndex();
});
if (hideDocs) return <NotFound />;
if (!data) return <NotFound />;
return (
<>
<Flexbox gap={isMobile ? 16 : 48}>
{data?.map((item) => (
<Fragment key={item.id}>
<Post locale={locale} mobile={isMobile} {...item} />
</Fragment>
))}
</Flexbox>
<GridLayout>
<Pagination />
</GridLayout>
<UpdateChangelogStatus currentId={data[0]?.id} />
</>
);
};
const DesktopPage = () => {
return <Page isMobile={false} />;
};
const MobilePage = () => {
return <Page isMobile={true} />;
};
export { DesktopPage, MobilePage };
@@ -1,10 +0,0 @@
import ServerLayout from '@/components/server/ServerLayout';
import Desktop from './_layout/Desktop';
import Mobile from './_layout/Mobile';
const MainLayout = ServerLayout({ Desktop, Mobile });
MainLayout.displayName = 'ChangelogLayout';
export default MainLayout;
@@ -1,23 +0,0 @@
'use client';
import { useEffect } from 'react';
import { useQueryRoute } from '@/hooks/useQueryRoute';
/**
* @description: Changelog Modal (intercepting routes fallback when hard refresh)
* @example: /changelog/modal => /changelog
* @refs: https://github.com/lobehub/lobe-chat/discussions/2295#discussioncomment-9290942
*/
const ChangelogModal = () => {
const router = useQueryRoute();
useEffect(() => {
router.replace('/changelog');
}, []);
return null;
};
export default ChangelogModal;
@@ -1,78 +0,0 @@
import { Divider, Skeleton } from 'antd';
import { notFound } from 'next/navigation';
import { Fragment, Suspense } from 'react';
import { Flexbox } from 'react-layout-kit';
import urlJoin from 'url-join';
import Pagination from '@/app/[variants]/@modal/(.)changelog/modal/features/Pagination';
import UpdateChangelogStatus from '@/app/[variants]/@modal/(.)changelog/modal/features/UpdateChangelogStatus';
import StructuredData from '@/components/StructuredData';
import { serverFeatureFlags } from '@/config/featureFlags';
import { BRANDING_NAME } from '@/const/branding';
import { OFFICIAL_SITE } from '@/const/url';
import { ldModule } from '@/server/ld';
import { metadataModule } from '@/server/metadata';
import { ChangelogService } from '@/server/services/changelog';
import { translation } from '@/server/translation';
import { DynamicLayoutProps } from '@/types/next';
import { RouteVariants } from '@/utils/server/routeVariants';
import GridLayout from './features/GridLayout';
import Post from './features/Post';
export const generateMetadata = async (props: DynamicLayoutProps) => {
const locale = await RouteVariants.getLocale(props);
const { t } = await translation('metadata', locale);
return metadataModule.generate({
canonical: urlJoin(OFFICIAL_SITE, 'changelog'),
description: t('changelog.description', { appName: BRANDING_NAME }),
title: t('changelog.title'),
url: '/changelog',
});
};
const Page = async (props: DynamicLayoutProps) => {
const hideDocs = serverFeatureFlags().hideDocs;
if (hideDocs) return notFound();
const { isMobile, locale } = await RouteVariants.getVariantsFromProps(props);
const { t } = await translation('metadata', locale);
const changelogService = new ChangelogService();
const data = await changelogService.getChangelogIndex();
if (!data) return notFound();
const ld = ldModule.generate({
description: t('changelog.description', { appName: BRANDING_NAME }),
title: t('changelog.title', { appName: BRANDING_NAME }),
url: '/changelog',
});
return (
<>
<StructuredData ld={ld} />
<Flexbox gap={isMobile ? 16 : 48}>
{data?.map((item) => (
<Fragment key={item.id}>
<Suspense
fallback={
<GridLayout>
<Divider />
<Skeleton active paragraph={{ rows: 5 }} />
</GridLayout>
}
>
<Post locale={locale} mobile={isMobile} {...item} />
</Suspense>
</Fragment>
))}
</Flexbox>
<GridLayout>
<Pagination />
</GridLayout>
<UpdateChangelogStatus currentId={data[0]?.id} />
</>
);
};
export default Page;
@@ -1,83 +0,0 @@
'use client';
import { memo, useEffect } from 'react';
import { useMediaQuery } from 'react-responsive';
import { MemoryRouter, Navigate, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
import MainChatPage from './components/MainChatPage';
import SettingsPage from './components/SettingsPage';
// Get initial path from URL
const getInitialPath = () => {
if (typeof window === 'undefined') return '/';
const fullPath = window.location.pathname;
const searchParams = window.location.search;
const chatIndex = fullPath.indexOf('/chat');
if (chatIndex !== -1) {
const pathAfterChat = fullPath.slice(chatIndex + '/chat'.length) || '/';
return pathAfterChat + searchParams;
}
return '/';
};
// Helper component to sync URL with MemoryRouter
const UrlSynchronizer = () => {
const location = useLocation();
const navigate = useNavigate();
// Sync initial URL
useEffect(() => {
const fullPath = window.location.pathname;
const searchParams = window.location.search;
const chatIndex = fullPath.indexOf('/chat');
if (chatIndex !== -1) {
const pathAfterChat = fullPath.slice(chatIndex + '/chat'.length) || '/';
const targetPath = pathAfterChat + searchParams;
if (location.pathname + location.search !== targetPath) {
navigate(targetPath, { replace: true });
}
}
}, []);
// Update browser URL when location changes
useEffect(() => {
const normalizedPath = location.pathname === '/' ? '' : location.pathname;
const newUrl = `/chat${normalizedPath}${location.search}`;
if (window.location.pathname + window.location.search !== newUrl) {
window.history.replaceState({}, '', newUrl);
}
}, [location.pathname, location.search]);
return null;
};
const ChatRouter = memo(() => {
const mobile = useMediaQuery({ maxWidth: 768 });
const routes = (
<Routes>
<Route element={<MainChatPage mobile={true} />} path="/" />
<Route element={<SettingsPage mobile={true} />} path="/settings" />
<Route element={<Navigate replace to="/" />} path="*" />
</Routes>
);
return (
<MemoryRouter initialEntries={[getInitialPath()]} initialIndex={0}>
<UrlSynchronizer />
{mobile ? (
// Mobile Layout
routes
) : (
// Desktop Layout
<MainChatPage mobile={false} />
)}
</MemoryRouter>
);
});
ChatRouter.displayName = 'ChatRouter';
export default ChatRouter;
@@ -1,22 +0,0 @@
'use client';
import { PropsWithChildren, memo } from 'react';
import Desktop from './Desktop';
import Mobile from './Mobile';
interface ChatLayoutProps extends PropsWithChildren {
mobile?: boolean;
}
const ChatLayout = memo<ChatLayoutProps>(({ children, mobile }) => {
if (mobile) {
return <Mobile>{children}</Mobile>;
}
return <Desktop>{children}</Desktop>;
});
ChatLayout.displayName = 'ChatLayout';
export default ChatLayout;
@@ -1,15 +1,14 @@
import { Suspense } from 'react';
import { Flexbox } from 'react-layout-kit';
import { Outlet } from 'react-router-dom';
import { isDesktop } from '@/const/version';
import ProtocolUrlHandler from '@/features/ProtocolUrlHandler';
import { LayoutProps } from '../type';
import RegisterHotkeys from './RegisterHotkeys';
import SessionPanel from './SessionPanel';
import Workspace from './Workspace';
const Layout = ({ children }: LayoutProps) => {
const Layout = () => {
return (
<>
<Flexbox
@@ -19,14 +18,14 @@ const Layout = ({ children }: LayoutProps) => {
width={'100%'}
>
<SessionPanel />
<Workspace>{children}</Workspace>
<Workspace>
<Outlet />
</Workspace>
</Flexbox>
{/* ↓ cloud slot ↓ */}
{/* ↑ cloud slot ↑ */}
<Suspense>
<RegisterHotkeys />
</Suspense>
<RegisterHotkeys />
{isDesktop && <ProtocolUrlHandler />}
</>
);
@@ -8,7 +8,7 @@ import { withSuspense } from '@/components/withSuspense';
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
import SessionPanelContent from '../components/SessionPanel';
import { LayoutProps } from './type';
import { Outlet } from 'react-router-dom';
const useStyles = createStyles(({ css, token }) => ({
main: css`
@@ -18,7 +18,7 @@ const useStyles = createStyles(({ css, token }) => ({
`,
}));
const Layout = memo<LayoutProps>(({ children }) => {
const Layout = memo(( ) => {
const showMobileWorkspace = useShowMobileWorkspace();
const { styles } = useStyles();
@@ -38,7 +38,7 @@ const Layout = memo<LayoutProps>(({ children }) => {
style={showMobileWorkspace ? undefined : { display: 'none' }}
width="100%"
>
{children}
<Outlet />
</Flexbox>
</>
);
@@ -1,25 +0,0 @@
'use client';
import { memo } from 'react';
import TelemetryNotification from '../components/features/TelemetryNotification';
import PageTitle from '../features/PageTitle';
import WorkspaceLayout from './WorkspaceLayout';
interface MainChatPageProps {
mobile?: boolean;
}
const MainChatPage = memo<MainChatPageProps>(({ mobile }) => {
return (
<>
<PageTitle />
<WorkspaceLayout mobile={mobile} />
<TelemetryNotification mobile={mobile} />
</>
);
});
MainChatPage.displayName = 'MainChatPage';
export default MainChatPage;
@@ -14,9 +14,6 @@ import ConversationArea from './ConversationArea';
import PortalPanel from './PortalPanel';
import TopicSidebar from './TopicSidebar';
interface WorkspaceLayoutProps {
mobile?: boolean;
}
const DesktopWorkspace = memo(() => (
<>
@@ -60,14 +57,4 @@ const MobileWorkspace = memo(() => (
MobileWorkspace.displayName = 'MobileWorkspace';
const WorkspaceLayout = memo<WorkspaceLayoutProps>(({ mobile }) => {
if (mobile) {
return <MobileWorkspace />;
}
return <DesktopWorkspace />;
});
WorkspaceLayout.displayName = 'WorkspaceLayout';
export default WorkspaceLayout;
export { DesktopWorkspace, MobileWorkspace };
@@ -1,9 +1,9 @@
'use client';
import { useQueryState } from 'nuqs';
import { memo, useLayoutEffect } from 'react';
import { createStoreUpdater } from 'zustand-utils';
import { useQueryState } from '@/hooks/useQueryParam';
import { useChatStore } from '@/store/chat';
// sync outside state to useChatStore
@@ -34,7 +34,7 @@ const ChatHydration = memo(() => {
unsubscribeTopic();
unsubscribeThread();
};
}, []);
}, [setTopic, setThread]); // ✅ 现在 setValue 是稳定的,可以安全地添加到依赖数组
return null;
});
@@ -1,7 +1,7 @@
'use client';
import { useSearchParams } from 'next/navigation';
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useChatStore } from '@/store/chat';
@@ -10,21 +10,19 @@ import { useSend } from '../useSend';
const MessageFromUrl = () => {
const updateMessageInput = useChatStore((s) => s.updateMessageInput);
const { send: sendMessage } = useSend();
const searchParams = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
useEffect(() => {
const message = searchParams.get('message');
if (message) {
// Remove message from URL
const params = new URLSearchParams(searchParams.toString());
params.delete('message');
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.replaceState({}, '', newUrl);
if (!message) return;
updateMessageInput(message);
sendMessage();
}
}, [searchParams, updateMessageInput, sendMessage]);
const params = new URLSearchParams(searchParams.toString());
params.delete('message');
setSearchParams(params, { replace: true });
updateMessageInput(message);
sendMessage();
}, [searchParams, setSearchParams, updateMessageInput, sendMessage]);
return null;
};
@@ -10,7 +10,6 @@ import GroupChatInput from './GroupChat';
const Desktop = memo((props: { targetMemberId?: string }) => {
const isGroupSession = useSessionStore(sessionSelectors.isCurrentSessionGroupSession);
return isGroupSession ? <GroupChatInput {...props} /> : <ClassicChatInput />;
});
@@ -1,10 +1,10 @@
'use client';
import { useQueryState } from 'nuqs';
import { memo, useEffect, useLayoutEffect } from 'react';
import { createStoreUpdater } from 'zustand-utils';
import { useFetchThreads } from '@/hooks/useFetchThreads';
import { useQueryState } from '@/hooks/useQueryParam';
import { useChatStore } from '@/store/chat';
// sync outside state to useChatStore
@@ -26,7 +26,7 @@ const ThreadHydration = memo(() => {
return () => {
unsubscribe();
};
}, []);
}, [setThread]); // ✅ 现在 setValue 是稳定的,可以安全地添加到依赖数组
// should open portal automatically when portalThread is set
useEffect(() => {
@@ -97,15 +97,15 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
},
...(isDesktop
? [
{
icon: <Icon icon={ExternalLink} />,
key: 'openInNewWindow',
label: t('actions.openInNewWindow'),
onClick: () => {
openTopicInNewWindow(activeId, id);
{
icon: <Icon icon={ExternalLink} />,
key: 'openInNewWindow',
label: t('actions.openInNewWindow'),
onClick: () => {
openTopicInNewWindow(activeId, id);
},
},
},
]
]
: []),
{
type: 'divider',
@@ -153,7 +153,16 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
},
},
],
[id, activeId, autoRenameTopicTitle, duplicateTopic, removeTopic, t, toggleEditing, openTopicInNewWindow],
[
id,
activeId,
autoRenameTopicTitle,
duplicateTopic,
removeTopic,
t,
toggleEditing,
openTopicInNewWindow,
],
);
return (
@@ -180,7 +189,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
spin={isLoading}
/>
{!editing ? (
title === LOADING_FLAT ? (
title === LOADING_FLAT || (isLoading && !title) ? (
<Flexbox flex={1} height={28} justify={'center'}>
<BubblesLoading />
</Flexbox>
@@ -190,7 +199,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
ellipsis={{ rows: 1, tooltip: { placement: 'left', title } }}
onDoubleClick={() => {
if (isDesktop) {
openTopicInNewWindow(activeId, id)
openTopicInNewWindow(activeId, id);
}
}}
style={{ margin: 0 }}
-3
View File
@@ -1,3 +0,0 @@
'use client';
export { default } from '@/components/Error';
@@ -3,7 +3,6 @@
import { memo } from 'react';
import PageTitle from '@/components/PageTitle';
import { withSuspense } from '@/components/withSuspense';
import { useChatStore } from '@/store/chat';
import { topicSelectors } from '@/store/chat/selectors';
import { useSessionStore } from '@/store/session';
@@ -16,4 +15,4 @@ const Title = memo(() => {
return <PageTitle title={[topicTitle, agentTitle].filter(Boolean).join(' · ')} />;
});
export default withSuspense(Title);
export default Title;
+29
View File
@@ -0,0 +1,29 @@
'use client';
import { memo } from 'react';
import { DesktopWorkspace, MobileWorkspace } from './components/WorkspaceLayout';
import TelemetryNotification from './components/features/TelemetryNotification';
import PageTitle from './features/PageTitle';
const MobileChatPage = memo(() => {
return (
<>
<PageTitle />
<MobileWorkspace />
<TelemetryNotification mobile={true} />
</>
);
});
const DesktopChatPage = memo(() => {
return (
<>
<PageTitle />
<DesktopWorkspace />
<TelemetryNotification mobile={false} />
</>
);
});
export { DesktopChatPage, MobileChatPage };
-10
View File
@@ -1,10 +0,0 @@
import ServerLayout from '@/components/server/ServerLayout';
import Desktop from './_layout/Desktop';
import Mobile from './_layout/Mobile';
import { LayoutProps } from './_layout/type';
const Layout = ServerLayout<LayoutProps>({ Desktop, Mobile });
Layout.displayName = 'ChatLayout';
export default Layout;
@@ -1,3 +0,0 @@
import Loading from '@/components/Loading/BrandTextLoading';
export default () => <Loading />;
@@ -1 +0,0 @@
export { default } from '@/components/404';
-12
View File
@@ -1,12 +0,0 @@
'use client';
import dynamic from 'next/dynamic';
import { BrandTextLoading } from '@/components/Loading';
const ChatRouter = dynamic(() => import('./ChatRouter'), {
loading: BrandTextLoading,
ssr: false,
});
export default ChatRouter;
@@ -1,14 +1,15 @@
'use client';
import { useQueryState } from 'nuqs';
import { parseAsString } from 'nuqs/server';
import { memo, useEffect } from 'react';
import { createStoreUpdater } from 'zustand-utils';
import { parseAsString, useQueryParam } from '@/hooks/useQueryParam';
import { useAgentStore } from '@/store/agent';
import { useChatStore } from '@/store/chat';
import { useSessionStore } from '@/store/session';
const THROTTLE_DELAY = 50;
// sync outside state to useSessionStore
const SessionHydration = memo(() => {
const useStoreUpdater = createStoreUpdater(useSessionStore);
@@ -17,10 +18,11 @@ const SessionHydration = memo(() => {
const [switchTopic] = useChatStore((s) => [s.switchTopic]);
// two-way bindings the url and session store
const [session, setSession] = useQueryState(
'session',
parseAsString.withDefault('inbox').withOptions({ history: 'replace', throttleMs: 50 }),
);
const [session, setSession] = useQueryParam('session', parseAsString.withDefault('inbox'), {
history: 'replace',
throttleMs: THROTTLE_DELAY,
});
useStoreUpdater('activeId', session);
useAgentStoreUpdater('activeId', session);
useChatStoreUpdater('activeId', session);
@@ -1,6 +1,6 @@
import Link from 'next/link';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
import { INBOX_SESSION_ID } from '@/const/session';
@@ -24,15 +24,12 @@ const Inbox = memo(() => {
return (
<Link
aria-label={t('inbox.title')}
href={SESSION_CHAT_URL(INBOX_SESSION_ID, mobile)}
onClick={async (e) => {
e.preventDefault();
if (activeId === INBOX_SESSION_ID && !mobile) {
// If user tap the inbox again, open a new topic.
// Only for desktop.
const inboxMessages = chatSelectors.inboxActiveTopicMessages(getChatStoreState());
if (inboxMessages.length > 0) {
await openNewTopicOrSaveTopic();
}
@@ -40,6 +37,7 @@ const Inbox = memo(() => {
switchSession(INBOX_SESSION_ID);
}
}}
to={SESSION_CHAT_URL(INBOX_SESSION_ID, mobile)}
>
<ListItem
active={activeId === INBOX_SESSION_ID}
@@ -9,8 +9,8 @@ import LazyLoad from 'react-lazy-load';
import { SESSION_CHAT_URL } from '@/const/url';
import { useSwitchSession } from '@/hooks/useSwitchSession';
import { useSessionStore, getSessionStoreState } from '@/store/session';
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
import { getSessionStoreState, useSessionStore } from '@/store/session';
import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors';
import { getUserStoreState } from '@/store/user';
import { userProfileSelectors } from '@/store/user/selectors';
@@ -3,8 +3,8 @@
import { ActionIcon } from '@lobehub/ui';
import { ChatHeader } from '@lobehub/ui/mobile';
import { MessageSquarePlus } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { useNavigate } from 'react-router-dom';
import { Flexbox } from 'react-layout-kit';
import { ProductLogo } from '@/components/Branding';
@@ -16,14 +16,14 @@ import { mobileHeaderSticky } from '@/styles/mobileHeader';
const Header = memo(() => {
const [createSession] = useSessionStore((s) => [s.createSession]);
const router = useRouter();
const navigate = useNavigate();
const { showCreateSession } = useServerConfigStore(featureFlagsSelectors);
return (
<ChatHeader
left={
<Flexbox align={'center'} gap={8} horizontal style={{ marginLeft: 8 }}>
<UserAvatar onClick={() => router.push('/me')} size={32} />
<UserAvatar onClick={() => navigate('/me')} size={32} />
<ProductLogo type={'text'} />
</Flexbox>
}
@@ -1,9 +1,9 @@
'use client';
import { ChatHeader, ChatHeaderTitle } from '@lobehub/ui/chat';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { pathString } from '@/utils/url';
@@ -11,12 +11,12 @@ import HeaderContent from '../../features/HeaderContent';
const Header = memo(() => {
const { t } = useTranslation('setting');
const router = useRouter();
const navigate = useNavigate();
return (
<ChatHeader
left={<ChatHeaderTitle title={t('header.session')} />}
onBackClick={() => router.push(pathString('/chat', { search: location.search }))}
onBackClick={() => navigate(pathString('/chat', { search: location.search }))}
right={<HeaderContent />}
showBackButton
/>
@@ -3,20 +3,20 @@
import { ChatHeader } from '@lobehub/ui/mobile';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
import { useQueryRoute } from '@/hooks/useQueryRoute';
import { mobileHeaderSticky } from '@/styles/mobileHeader';
import HeaderContent from '../../features/HeaderContent';
const Header = memo(() => {
const { t } = useTranslation('setting');
const router = useQueryRoute();
const navigate = useNavigate();
return (
<ChatHeader
center={<ChatHeader.Title title={t('header.session')} />}
onBackClick={() => router.push('/chat')}
onBackClick={() => navigate(-1)}
right={<HeaderContent />}
showBackButton
style={mobileHeaderSticky}
@@ -1,3 +0,0 @@
'use client';
export { default } from '@/components/Error';
@@ -2,9 +2,9 @@
import { CheckCircleOutlined } from '@ant-design/icons';
import { Button, Modal, Space } from 'antd';
import { useRouter } from 'next/navigation';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
interface PublishResultModalProps {
identifier?: string;
@@ -13,13 +13,13 @@ interface PublishResultModalProps {
}
const PublishResultModal = memo<PublishResultModalProps>(({ identifier, onCancel, open }) => {
const router = useRouter();
const navigate = useNavigate();
const { t } = useTranslation('setting');
const { t: tCommon } = useTranslation('common');
const handleGoToMarket = () => {
if (identifier) {
router.push(`/discover/assistant/${identifier}`);
navigate(`/discover/assistant/${identifier}`);
}
onCancel();
};
@@ -8,12 +8,9 @@ import { useTranslation } from 'react-i18next';
import MobileContentLayout from '@/components/server/MobileNavLayout';
import PageTitle from '@/components/PageTitle';
import SafeSpacing from '@/components/SafeSpacing';
import { HEADER_HEIGHT } from '@/const/layoutTokens';
import { useCategory } from '@/features/AgentSetting/AgentCategory/useCategory';
import AgentSettings from '@/features/AgentSetting/AgentSettings';
import Footer from '@/features/Setting/Footer';
import SettingContainer from '@/features/Setting/SettingContainer';
import { useInitAgentConfig } from '@/hooks/useInitAgentConfig';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
@@ -21,14 +18,9 @@ import { ChatSettingsTabs } from '@/store/global/initialState';
import { useSessionStore } from '@/store/session';
import { sessionMetaSelectors } from '@/store/session/selectors';
import DesktopHeader from '../settings/_layout/Desktop/Header';
import MobileHeader from '../settings/_layout/Mobile/Header';
import MobileHeader from './_layout/Mobile/Header';
interface SettingsPageProps {
mobile?: boolean;
}
const SettingsPage = memo<SettingsPageProps>(({ mobile = false }) => {
export default memo(() => {
const { t } = useTranslation('setting');
const [tab, setTab] = useState(ChatSettingsTabs.Prompt);
const theme = useTheme();
@@ -46,8 +38,8 @@ const SettingsPage = memo<SettingsPageProps>(({ mobile = false }) => {
const { isLoading } = useInitAgentConfig();
const content = (
<>
return (
<MobileContentLayout header={<MobileHeader />}>
<PageTitle title={t('header.sessionWithName', { name: title })} />
<Tabs
activeKey={tab}
@@ -67,28 +59,7 @@ const SettingsPage = memo<SettingsPageProps>(({ mobile = false }) => {
onMetaChange={updateAgentMeta}
tab={tab}
/>
</>
);
if (mobile) {
return (
<MobileContentLayout header={<MobileHeader />}>
{content}
<Footer />
</MobileContentLayout>
);
}
return (
<>
<DesktopHeader />
<SettingContainer addonAfter={<Footer />} addonBefore={<SafeSpacing height={HEADER_HEIGHT} />}>
{content}
</SettingContainer>
</>
<Footer />
</MobileContentLayout>
);
});
SettingsPage.displayName = 'SettingsPage';
export default SettingsPage;
@@ -1,3 +0,0 @@
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
export default () => <SkeletonLoading paragraph={{ rows: 8 }} />;
@@ -1 +0,0 @@
export { default } from '@/components/404';
@@ -0,0 +1,21 @@
import React, { memo } from 'react';
import { Link as ReactRouterLink, LinkProps as ReactRouterLinkProps } from 'react-router-dom';
interface LinkProps extends Omit<ReactRouterLinkProps, 'to'> {
children?: React.ReactNode;
href?: string;
to?: string;
}
/**
* Link component for React Router
* Provides a Next.js-like API (href prop) while using React Router internally
*/
const Link = memo<LinkProps>(({ href, to, ...props }) => {
const linkTo = href || to || '/';
return <ReactRouterLink {...props} to={linkTo} />;
});
Link.displayName = 'Link';
export default Link;
@@ -1,12 +1,19 @@
import { PropsWithChildren } from 'react';
'use client';
import { memo } from 'react';
import { Outlet } from 'react-router-dom';
import { Flexbox } from 'react-layout-kit';
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
import { SCROLL_PARENT_ID } from '../../../features/const';
import Footer from '@/features/Setting/Footer';
const MAX_WIDTH = 1440;
const Layout = ({ children }: PropsWithChildren) => {
/**
* Desktop Discover Detail Layout
* Layout for detail pages (assistant, model, provider, mcp details)
*/
const DesktopDiscoverDetailLayout = memo(() => {
return (
<Flexbox
align={'center'}
@@ -17,14 +24,15 @@ const Layout = ({ children }: PropsWithChildren) => {
width={'100%'}
>
<Flexbox gap={24} style={{ maxWidth: MAX_WIDTH, minHeight: '100%' }} width={'100%'}>
{children}
<Outlet />
<div />
<Footer />
</Flexbox>
</Flexbox>
);
};
});
Layout.displayName = 'DesktopDiscoverDetailLayout';
DesktopDiscoverDetailLayout.displayName = 'DesktopDiscoverDetailLayout';
export default Layout;
export default DesktopDiscoverDetailLayout;
@@ -1,22 +0,0 @@
'use client';
import { PropsWithChildren, memo } from 'react';
import Desktop from './Desktop';
import Mobile from './Mobile';
interface DetailLayoutProps extends PropsWithChildren {
mobile?: boolean;
}
const DetailLayout = memo<DetailLayoutProps>(({ children, mobile }) => {
if (mobile) {
return <Mobile>{children}</Mobile>;
}
return <Desktop>{children}</Desktop>;
});
DetailLayout.displayName = 'DetailLayout';
export default DetailLayout;
@@ -1,15 +1,15 @@
import { PropsWithChildren } from 'react';
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
import MobileContentLayout from '@/components/server/MobileNavLayout';
import Footer from '@/features/Setting/Footer';
import Header from './Header';
import { Outlet } from 'react-router-dom';
const Layout = ({ children }: PropsWithChildren) => {
const Layout = () => {
return (
<MobileContentLayout gap={16} header={<Header />} id={SCROLL_PARENT_ID} padding={16}>
{children}
<Outlet />
<div />
<Footer />
</MobileContentLayout>
@@ -1,51 +0,0 @@
'use client';
import { notFound } from 'next/navigation';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { withSuspense } from '@/components/withSuspense';
import { useQuery } from '@/hooks/useQuery';
import { useDiscoverStore } from '@/store/discover';
import { AssistantMarketSource } from '@/types/discover';
import { TocProvider } from '../../features/Toc/useToc';
import { DetailProvider } from './features/DetailProvider';
import Details from './features/Details';
import Header from './features/Header';
import StatusPage from './features/StatusPage';
import Loading from './loading';
interface ClientProps {
identifier: string;
mobile?: boolean;
}
const Client = memo<ClientProps>(({ identifier, mobile }) => {
const { version, source } = useQuery() as { source?: AssistantMarketSource; version?: string };
const marketSource = source as AssistantMarketSource | undefined;
const useAssistantDetail = useDiscoverStore((s) => s.useAssistantDetail);
const { data, isLoading } = useAssistantDetail({ identifier, source: marketSource, version });
if (isLoading) return <Loading />;
if (!data) return notFound();
// 检查助手状态
const status = (data as any)?.status;
if (status === 'unpublished' || status === 'archived' || status === 'deprecated') {
return <StatusPage status={status} />;
}
return (
<TocProvider>
<DetailProvider config={data}>
<Flexbox gap={16}>
<Header mobile={mobile} />
<Details mobile={mobile} />
</Flexbox>
</DetailProvider>
</TocProvider>
);
});
export default withSuspense(Client);
@@ -2,7 +2,7 @@ import { Tag } from '@lobehub/ui';
import { ReactNode, memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import Title from '../../../../../../features/Title';
import Title from '../../../../../features/Title';
interface BlockProps {
children?: ReactNode;
@@ -10,7 +10,7 @@ import { DEFAULT_USER_AVATAR_URL } from '@/const/meta';
import { useUserStore } from '@/store/user';
import { authSelectors, userProfileSelectors } from '@/store/user/selectors';
import Title from '../../../../../../features/Title';
import Title from '../../../../../features/Title';
import { useDetailContext } from '../../DetailProvider';
const Overview = memo(() => {
@@ -6,8 +6,8 @@ import { Flexbox } from 'react-layout-kit';
import { useQuery } from '@/hooks/useQuery';
import { AssistantMarketSource } from '@/types/discover';
import McpList from '../../../../../../(list)/assistant/features/List';
import Title from '../../../../../../features/Title';
import McpList from '../../../../../(list)/assistant/features/List';
import Title from '../../../../../features/Title';
import { useDetailContext } from '../../DetailProvider';
const Related = memo(() => {
@@ -5,9 +5,9 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import TokenTag from '../../../../../../(list)/assistant/features/List/TokenTag';
import Title from '../../../../../../features/Title';
import MarkdownRender from '../../../../../features/MakedownRender';
import TokenTag from '../../../../../(list)/assistant/features/List/TokenTag';
import Title from '../../../../../features/Title';
import MarkdownRender from '../../../../features/MakedownRender';
import { useDetailContext } from '../../DetailProvider';
import TagList from './TagList';
@@ -1,19 +1,19 @@
import { Block, Icon, Tag } from '@lobehub/ui';
import { useTheme } from 'antd-style';
import { CheckIcon, MinusIcon } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import qs from 'query-string';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import Link from '@/app/[variants]/(main)/components/Link';
import { usePathname } from '@/app/[variants]/(main)/hooks/usePathname';
import { useQuery } from '@/app/[variants]/(main)/hooks/useQuery';
import InlineTable from '@/components/InlineTable';
import PublishedTime from '@/components/PublishedTime';
import { useQuery } from '@/hooks/useQuery';
import { AssistantMarketSource, AssistantNavKey } from '@/types/discover';
import Title from '../../../../../../features/Title';
import Title from '../../../../../features/Title';
import { useDetailContext } from '../../DetailProvider';
const Versions = memo(() => {
@@ -1,10 +1,10 @@
'use client';
import { useResponsive } from 'antd-style';
import { useQueryState } from 'nuqs';
import { memo } from 'react';
import { Flexbox } from 'react-layout-kit';
import { useQueryState } from '@/hooks/useQueryParam';
import { AssistantNavKey } from '@/types/discover';
import Sidebar from '../Sidebar';

Some files were not shown because too many files have changed in this diff Show More