mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-16 04:25:59 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7afd1318db | |||
| 6a374d2f32 | |||
| cec034721f | |||
| 2d70632d3e | |||
| 41c554d748 | |||
| 4e4933d861 | |||
| a5bb31b844 |
@@ -2,6 +2,48 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
[
|
||||
{
|
||||
"children": {},
|
||||
"date": "2025-11-17",
|
||||
"version": "2.0.0-next.70"
|
||||
},
|
||||
{
|
||||
"children": {
|
||||
"improvements": ["Remove language_model_settings and remove isDeprecatedEdition."]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -330,6 +330,11 @@
|
||||
"screenshot": "截图",
|
||||
"settings": "导出设置",
|
||||
"text": "文本",
|
||||
"widthMode": {
|
||||
"label": "宽度模式",
|
||||
"narrow": "窄屏模式",
|
||||
"wide": "宽屏模式"
|
||||
},
|
||||
"withBackground": "包含背景图片",
|
||||
"withFooter": "包含页脚",
|
||||
"withPluginInfo": "包含插件信息",
|
||||
|
||||
+5
-5
@@ -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,
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/lobehub",
|
||||
"version": "2.0.0-next.69",
|
||||
"version": "2.0.0-next.71",
|
||||
"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",
|
||||
@@ -397,4 +397,4 @@
|
||||
"@vercel/speed-insights"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -112,7 +112,7 @@ export const ChatToolPayloadSchema = z.object({
|
||||
});
|
||||
|
||||
/**
|
||||
* 聊天消息错误对象
|
||||
* Chat message error object
|
||||
*/
|
||||
export interface ChatMessagePluginError {
|
||||
body?: any;
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
import type { BaseDataModel } from '../meta';
|
||||
|
||||
// 类型定义
|
||||
// Type definitions
|
||||
export type TimeGroupId =
|
||||
| 'today'
|
||||
| 'yesterday'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ const partialBuildPages = [
|
||||
{
|
||||
name: 'changelog',
|
||||
disabled: isDesktop,
|
||||
paths: ['src/app/[variants]/(main)/changelog'],
|
||||
paths: ['src/app/[variants]/@modal/(.)changelog', '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,12 +6,10 @@ import { useUserStore } from '@/store/user';
|
||||
import UserBanner from '../features/UserBanner';
|
||||
|
||||
// Mock dependencies
|
||||
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('next/navigation', () => ({
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/features/User/UserInfo', () => ({
|
||||
@@ -47,7 +45,6 @@ vi.mock('@/const/auth', () => ({
|
||||
|
||||
afterEach(() => {
|
||||
enableAuth = true;
|
||||
mockNavigate.mockReset();
|
||||
});
|
||||
|
||||
describe('UserBanner', () => {
|
||||
|
||||
@@ -11,9 +11,10 @@ const wrapper: React.JSXElementConstructor<{ children: React.ReactNode }> = ({ c
|
||||
);
|
||||
|
||||
// Mock dependencies
|
||||
const mockNavigate = vi.fn();
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockNavigate,
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: vi.fn(() => ({
|
||||
push: vi.fn(),
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
@@ -47,7 +48,6 @@ vi.mock('@/const/version', async (importOriginal) => {
|
||||
afterEach(() => {
|
||||
enableAuth = true;
|
||||
enableClerk = true;
|
||||
mockNavigate.mockReset();
|
||||
});
|
||||
|
||||
describe('useCategory', () => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'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';
|
||||
@@ -12,7 +13,7 @@ import { useUserStore } from '@/store/user';
|
||||
import { authSelectors } from '@/store/user/selectors';
|
||||
|
||||
const UserBanner = memo(() => {
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const isLoginWithAuth = useUserStore(authSelectors.isLoginWithAuth);
|
||||
const [signIn] = useUserStore((s) => [s.openLogin]);
|
||||
|
||||
@@ -20,10 +21,10 @@ const UserBanner = memo(() => {
|
||||
<Flexbox gap={12} paddingBlock={8}>
|
||||
{!enableAuth || (enableAuth && isLoginWithAuth) ? (
|
||||
<>
|
||||
<Link style={{ color: 'inherit' }} to="/profile">
|
||||
<Link href={'/profile'} style={{ color: 'inherit' }}>
|
||||
<UserInfo />
|
||||
</Link>
|
||||
<Link style={{ color: 'inherit' }} to="/profile/stats">
|
||||
<Link href={'/profile/stats'} style={{ color: 'inherit' }}>
|
||||
<DataStatistics paddingInline={12} />
|
||||
</Link>
|
||||
</>
|
||||
@@ -35,7 +36,7 @@ const UserBanner = memo(() => {
|
||||
signIn();
|
||||
return;
|
||||
}
|
||||
navigate('/login');
|
||||
router.push('/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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
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: () => navigate('/me/profile'),
|
||||
onClick: () => router.push('/me/profile'),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useCategory = () => {
|
||||
icon: Settings2,
|
||||
key: 'setting',
|
||||
label: t('userPanel.setting'),
|
||||
onClick: () => navigate('/me/settings'),
|
||||
onClick: () => router.push('/me/settings'),
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
@@ -58,6 +58,9 @@ export const useCategory = () => {
|
||||
];
|
||||
|
||||
/* ↓ cloud slot ↓ */
|
||||
|
||||
/* ↑ cloud slot ↑ */
|
||||
|
||||
const helps: CellProps[] = [
|
||||
showCloudPromotion && {
|
||||
icon: Cloudy,
|
||||
@@ -81,7 +84,7 @@ export const useCategory = () => {
|
||||
icon: FileClockIcon,
|
||||
key: 'changelog',
|
||||
label: t('changelog'),
|
||||
onClick: () => navigate('/changelog'),
|
||||
onClick: () => router.push('/changelog'),
|
||||
},
|
||||
].filter(Boolean) as CellProps[];
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
'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,15 +1,18 @@
|
||||
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 { PropsWithChildren, Suspense } from 'react';
|
||||
|
||||
const Layout = () => {
|
||||
return <MobileContentLayout header={<Header />} withNav>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
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>
|
||||
</MobileContentLayout>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
Layout.displayName = 'MeLayout';
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
'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;
|
||||
@@ -0,0 +1,40 @@
|
||||
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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation('auth');
|
||||
const items: CellProps[] = [
|
||||
{
|
||||
icon: UserCircle,
|
||||
key: ProfileTabs.Profile,
|
||||
label: t('tab.profile'),
|
||||
onClick: () => navigate('/profile'),
|
||||
onClick: () => router.push('/profile'),
|
||||
},
|
||||
isLoginWithClerk && {
|
||||
icon: ShieldCheck,
|
||||
key: ProfileTabs.Security,
|
||||
label: t('tab.security'),
|
||||
onClick: () => navigate('/profile/security'),
|
||||
onClick: () => router.push('/profile/security'),
|
||||
},
|
||||
{
|
||||
icon: ChartColumnBigIcon,
|
||||
key: ProfileTabs.Stats,
|
||||
label: t('tab.stats'),
|
||||
onClick: () => navigate('/profile/stats'),
|
||||
onClick: () => router.push('/profile/stats'),
|
||||
},
|
||||
isLogin && {
|
||||
type: 'divider',
|
||||
@@ -46,7 +46,7 @@ const Category = memo(() => {
|
||||
label: t('signout', { ns: 'auth' }),
|
||||
onClick: () => {
|
||||
signOut();
|
||||
navigate('/login');
|
||||
router.push('/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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<ChatHeader
|
||||
center={
|
||||
@@ -23,7 +23,7 @@ const Header = memo(() => {
|
||||
}
|
||||
/>
|
||||
}
|
||||
onBackClick={() => navigate('/me')}
|
||||
onBackClick={() => router.push('/me')}
|
||||
showBackButton
|
||||
style={mobileHeaderSticky}
|
||||
/>
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
|
||||
|
||||
import Category from './features/Category';
|
||||
|
||||
const MeProfilePage = memo(() => {
|
||||
return (
|
||||
<Category />
|
||||
);
|
||||
});
|
||||
|
||||
MeProfilePage.displayName = 'MeProfilePage';
|
||||
|
||||
export default MeProfilePage;
|
||||
@@ -1,11 +1,13 @@
|
||||
import MobileContentLayout from "@/components/server/MobileNavLayout";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import Header from "./features/Header";
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
const Layout = () => {
|
||||
return <MobileContentLayout header={<Header />}>
|
||||
<Outlet />
|
||||
</MobileContentLayout>
|
||||
}
|
||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||
|
||||
export default Layout;
|
||||
import Header from './features/Header';
|
||||
|
||||
const Layout = ({ children }: PropsWithChildren) => {
|
||||
return <MobileContentLayout header={<Header />}>{children}</MobileContentLayout>;
|
||||
};
|
||||
|
||||
Layout.displayName = 'MeProfileLayout';
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
|
||||
|
||||
export default () => {
|
||||
return <SkeletonLoading paragraph={{ rows: 8 }} />;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<ChatHeader
|
||||
center={
|
||||
@@ -23,7 +23,7 @@ const Header = memo(() => {
|
||||
}
|
||||
/>
|
||||
}
|
||||
onBackClick={() => navigate('/me')}
|
||||
onBackClick={() => router.push('/me')}
|
||||
showBackButton
|
||||
style={mobileHeaderSticky}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation('setting');
|
||||
|
||||
const items: CellProps[] = [
|
||||
@@ -39,6 +40,6 @@ export const useCategory = () => {
|
||||
|
||||
return items.map((item) => ({
|
||||
...item,
|
||||
onClick: () => navigate(`/settings?active=${item.key}`),
|
||||
onClick: () => router.push(`/settings?active=${item.key}`),
|
||||
}));
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
|
||||
|
||||
import Category from './features/Category';
|
||||
|
||||
const MeSettingsPage = memo(() => {
|
||||
return (
|
||||
<Category />
|
||||
);
|
||||
});
|
||||
|
||||
MeSettingsPage.displayName = 'MeSettingsPage';
|
||||
|
||||
export default MeSettingsPage;
|
||||
@@ -1,15 +1,13 @@
|
||||
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 { PropsWithChildren } from 'react';
|
||||
|
||||
const Layout = () => {
|
||||
return <MobileContentLayout header={<Header />} withNav>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</MobileContentLayout>
|
||||
}
|
||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||
|
||||
export default Layout;
|
||||
import Header from './features/Header';
|
||||
|
||||
const Layout = ({ children }: PropsWithChildren) => {
|
||||
return <MobileContentLayout header={<Header />}>{children}</MobileContentLayout>;
|
||||
};
|
||||
|
||||
Layout.displayName = 'MeSettingsLayout';
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
|
||||
|
||||
export default () => {
|
||||
return <SkeletonLoading paragraph={{ rows: 8 }} />;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
+2
-3
@@ -1,14 +1,13 @@
|
||||
import { useTheme } from 'antd-style';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { PropsWithChildren, Suspense, memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import SideBar from './SideBar';
|
||||
|
||||
const DesktopLayoutContainer = memo<PropsWithChildren>(({ children }) => {
|
||||
const theme = useTheme();
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
const pathname = usePathname();
|
||||
const hideSideBar = pathname.startsWith('/settings');
|
||||
return (
|
||||
<>
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
import { ActionIcon, ActionIconProps } from '@lobehub/ui';
|
||||
import { FlaskConical, Github } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { memo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
@@ -22,16 +22,16 @@ const BottomActions = memo(() => {
|
||||
return (
|
||||
<Flexbox gap={8}>
|
||||
{!hideGitHub && (
|
||||
<a aria-label={'GitHub'} href={GITHUB} rel="noopener noreferrer" target={'_blank'}>
|
||||
<Link aria-label={'GitHub'} href={GITHUB} target={'_blank'}>
|
||||
<ActionIcon
|
||||
icon={Github}
|
||||
size={ICON_SIZE}
|
||||
title={'GitHub'}
|
||||
tooltipProps={{ placement: 'right' }}
|
||||
/>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link aria-label={t('labs')} to={'/labs'}>
|
||||
<Link aria-label={t('labs')} href={'/labs'}>
|
||||
<ActionIcon
|
||||
icon={FlaskConical}
|
||||
size={ICON_SIZE}
|
||||
+9
-9
@@ -38,16 +38,17 @@ afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
useNavigate: vi.fn(() => vi.fn()),
|
||||
vi.mock('next/link', () => ({
|
||||
default: vi.fn(({ children, ...rest }: { children: React.ReactNode; href: string }) => (
|
||||
<div {...rest}>
|
||||
{`Mocked Link ${rest.href}`}
|
||||
{children}
|
||||
</div>
|
||||
)),
|
||||
}));
|
||||
|
||||
vi.mock('@lobehub/ui', () => ({
|
||||
ActionIcon: vi.fn(({ title, onClick, icon }) => (
|
||||
<div data-testid={`action-icon-${icon?.name || 'unknown'}`} onClick={onClick}>
|
||||
{title}
|
||||
</div>
|
||||
)),
|
||||
ActionIcon: vi.fn(({ title }) => <div>{title}</div>),
|
||||
combineKeys: vi.fn((keys) => keys.join('+')),
|
||||
KeyMapEnum: { Alt: 'alt', Ctrl: 'ctrl', Shift: 'shift' },
|
||||
Hotkey: vi.fn(({ keys = [] }) => <div>{keys}</div>),
|
||||
@@ -133,8 +134,7 @@ describe('TopActions', () => {
|
||||
const switchBackToChat = vi.spyOn(store.current, 'switchBackToChat');
|
||||
|
||||
renderTopActions({ tab: SidebarTabKey.Discover });
|
||||
const chatIcon = screen.getByText('tab.chat');
|
||||
fireEvent.click(chatIcon);
|
||||
fireEvent.click(screen.getByText('Mocked Link /chat'));
|
||||
|
||||
expect(switchBackToChat).toBeCalledWith('1');
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
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;
|
||||
+4
-9
@@ -2,17 +2,15 @@
|
||||
|
||||
import { useTheme } from 'antd-style';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Suspense, memo } from 'react';
|
||||
import { PropsWithChildren, Suspense, memo } from 'react';
|
||||
import { HotkeysProvider } from 'react-hotkeys-hook';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { isDesktop } from '@/const/version';
|
||||
import { BANNER_HEIGHT } from '@/features/AlertBanner/CloudBanner';
|
||||
import TitleBar, { TITLE_BAR_HEIGHT } from '@/features/ElectronTitlebar';
|
||||
import HotkeyHelperPanel from '@/features/HotkeyHelperPanel';
|
||||
import { usePlatform } from '@/hooks/usePlatform';
|
||||
import { Locales } from '@/locales/resources';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
import { HotkeyScopeEnum } from '@/types/hotkey';
|
||||
|
||||
@@ -22,8 +20,7 @@ import SideBar from './SideBar';
|
||||
|
||||
const CloudBanner = dynamic(() => import('@/features/AlertBanner/CloudBanner'));
|
||||
|
||||
const Layout = memo((props: { locale: Locales }) => {
|
||||
const { locale } = props;
|
||||
const Layout = memo<PropsWithChildren>(({ children }) => {
|
||||
const { isPWA } = usePlatform();
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -48,15 +45,13 @@ const Layout = memo((props: { locale: Locales }) => {
|
||||
width={'100%'}
|
||||
>
|
||||
{isDesktop ? (
|
||||
<DesktopLayoutContainer>
|
||||
<Outlet context={{ locale: locale }} />
|
||||
</DesktopLayoutContainer>
|
||||
<DesktopLayoutContainer>{children}</DesktopLayoutContainer>
|
||||
) : (
|
||||
<>
|
||||
<Suspense>
|
||||
<SideBar />
|
||||
</Suspense>
|
||||
<Outlet context={{ locale: locale }} />
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</Flexbox>
|
||||
+5
-5
@@ -4,10 +4,10 @@ import { Icon } from '@lobehub/ui';
|
||||
import { TabBar, type TabBarProps } from '@lobehub/ui/mobile';
|
||||
import { createStyles } from 'antd-style';
|
||||
import { Compass, MessageSquare, User } from 'lucide-react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { rgba } from 'polished';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import { MOBILE_TABBAR_HEIGHT } from '@/const/layoutTokens';
|
||||
import { useActiveTabKey } from '@/hooks/useActiveTabKey';
|
||||
@@ -32,7 +32,7 @@ const NavBar = memo(() => {
|
||||
const { t } = useTranslation('common');
|
||||
const { styles } = useStyles();
|
||||
const activeKey = useActiveTabKey();
|
||||
const navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
|
||||
const { showMarket } = useServerConfigStore(featureFlagsSelectors);
|
||||
|
||||
@@ -45,7 +45,7 @@ const NavBar = memo(() => {
|
||||
),
|
||||
key: SidebarTabKey.Chat,
|
||||
onClick: () => {
|
||||
navigate('/chat');
|
||||
router.push('/chat');
|
||||
},
|
||||
title: t('tab.chat'),
|
||||
},
|
||||
@@ -55,7 +55,7 @@ const NavBar = memo(() => {
|
||||
),
|
||||
key: SidebarTabKey.Discover,
|
||||
onClick: () => {
|
||||
navigate('/discover');
|
||||
router.push('/discover');
|
||||
},
|
||||
title: t('tab.discover'),
|
||||
},
|
||||
@@ -65,7 +65,7 @@ const NavBar = memo(() => {
|
||||
),
|
||||
key: SidebarTabKey.Me,
|
||||
onClick: () => {
|
||||
navigate('/me');
|
||||
router.push('/me');
|
||||
},
|
||||
title: t('tab.me'),
|
||||
},
|
||||
+7
-10
@@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { PropsWithChildren, memo } from 'react';
|
||||
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
|
||||
import { Locales } from '@/locales/resources';
|
||||
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
||||
|
||||
import NavBar from './NavBar';
|
||||
@@ -23,11 +22,9 @@ const MOBILE_NAV_ROUTES = new Set([
|
||||
'/me',
|
||||
]);
|
||||
|
||||
export const MobileMainLayout = memo((props: { locale: Locales }) => {
|
||||
const { locale } = props;
|
||||
const Layout = memo(({ children }: PropsWithChildren) => {
|
||||
const showMobileWorkspace = useShowMobileWorkspace();
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
const pathname = usePathname();
|
||||
const showNav = !showMobileWorkspace && MOBILE_NAV_ROUTES.has(pathname);
|
||||
|
||||
const { showCloudPromotion } = useServerConfigStore(featureFlagsSelectors);
|
||||
@@ -35,12 +32,12 @@ export const MobileMainLayout = memo((props: { locale: Locales }) => {
|
||||
return (
|
||||
<>
|
||||
{showCloudPromotion && <CloudBanner mobile />}
|
||||
<Outlet context={{ locale: locale }} />
|
||||
{children}
|
||||
{showNav && <NavBar />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
MobileMainLayout.displayName = 'MobileMainLayout';
|
||||
Layout.displayName = 'MobileMainLayout';
|
||||
|
||||
export default withSuspense(MobileMainLayout);
|
||||
export default withSuspense(Layout);
|
||||
@@ -1,16 +1,15 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
|
||||
import { Locales } from '@/locales/resources';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import Hero from '../../features/Hero';
|
||||
import Container from './Container';
|
||||
|
||||
const Layout = (props: { locale: Locales }) => {
|
||||
const { locale } = props;
|
||||
type Props = { children: ReactNode };
|
||||
|
||||
const Layout = ({ children }: Props) => {
|
||||
return (
|
||||
<Container>
|
||||
<Hero />
|
||||
<Outlet context={{ locale }} />
|
||||
{children}
|
||||
</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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<ChatHeader
|
||||
center={
|
||||
@@ -23,7 +23,7 @@ const Header = memo(() => {
|
||||
}
|
||||
/>
|
||||
}
|
||||
onBackClick={() => navigate(-1)}
|
||||
onBackClick={() => router.back()}
|
||||
showBackButton
|
||||
style={mobileHeaderSticky}
|
||||
/>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import MobileContentLayout from '@/components/server/MobileNavLayout';
|
||||
import { Locales } from '@/locales/resources';
|
||||
|
||||
import Hero from '../../features/Hero';
|
||||
import Header from './Header';
|
||||
|
||||
const Layout = (props: { locale: Locales }) => {
|
||||
const { locale } = props;
|
||||
type Props = { children: ReactNode };
|
||||
|
||||
const Layout = ({ children }: Props) => {
|
||||
return (
|
||||
<MobileContentLayout header={<Header />} padding={16}>
|
||||
<Hero />
|
||||
<Outlet context={{ locale }} />
|
||||
{children}
|
||||
</MobileContentLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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';
|
||||
@@ -15,16 +14,14 @@ import GridLayout from './GridLayout';
|
||||
import PublishedTime from './PublishedTime';
|
||||
import VersionTag from './VersionTag';
|
||||
|
||||
const Post = ({
|
||||
const Post = async ({
|
||||
id,
|
||||
mobile,
|
||||
versionRange,
|
||||
locale,
|
||||
}: ChangelogIndexItem & { branch?: string; locale: Locales; mobile?: boolean }) => {
|
||||
const { data } = useSWR([`changelog-post-${id}`, locale], async () => {
|
||||
const changelogService = new ChangelogService();
|
||||
return await changelogService.getPostById(id, { locale });
|
||||
});
|
||||
const changelogService = new ChangelogService();
|
||||
const data = await changelogService.getPostById(id, { locale });
|
||||
|
||||
if (!data || !data.title) return null;
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
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 };
|
||||
@@ -0,0 +1,10 @@
|
||||
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;
|
||||
@@ -0,0 +1,23 @@
|
||||
'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;
|
||||
@@ -0,0 +1,78 @@
|
||||
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;
|
||||
@@ -0,0 +1,83 @@
|
||||
'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;
|
||||
@@ -0,0 +1,22 @@
|
||||
'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,14 +1,15 @@
|
||||
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 = () => {
|
||||
const Layout = ({ children }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<Flexbox
|
||||
@@ -18,14 +19,14 @@ const Layout = () => {
|
||||
width={'100%'}
|
||||
>
|
||||
<SessionPanel />
|
||||
<Workspace>
|
||||
<Outlet />
|
||||
</Workspace>
|
||||
<Workspace>{children}</Workspace>
|
||||
</Flexbox>
|
||||
{/* ↓ cloud slot ↓ */}
|
||||
|
||||
{/* ↑ cloud slot ↑ */}
|
||||
<RegisterHotkeys />
|
||||
<Suspense>
|
||||
<RegisterHotkeys />
|
||||
</Suspense>
|
||||
{isDesktop && <ProtocolUrlHandler />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { withSuspense } from '@/components/withSuspense';
|
||||
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
|
||||
|
||||
import SessionPanelContent from '../components/SessionPanel';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { LayoutProps } from './type';
|
||||
|
||||
const useStyles = createStyles(({ css, token }) => ({
|
||||
main: css`
|
||||
@@ -18,7 +18,7 @@ const useStyles = createStyles(({ css, token }) => ({
|
||||
`,
|
||||
}));
|
||||
|
||||
const Layout = memo(( ) => {
|
||||
const Layout = memo<LayoutProps>(({ children }) => {
|
||||
const showMobileWorkspace = useShowMobileWorkspace();
|
||||
const { styles } = useStyles();
|
||||
|
||||
@@ -38,7 +38,7 @@ const Layout = memo(( ) => {
|
||||
style={showMobileWorkspace ? undefined : { display: 'none' }}
|
||||
width="100%"
|
||||
>
|
||||
<Outlet />
|
||||
{children}
|
||||
</Flexbox>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
'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;
|
||||
+35
-6
@@ -8,9 +8,12 @@ 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';
|
||||
@@ -18,9 +21,14 @@ import { ChatSettingsTabs } from '@/store/global/initialState';
|
||||
import { useSessionStore } from '@/store/session';
|
||||
import { sessionMetaSelectors } from '@/store/session/selectors';
|
||||
|
||||
import MobileHeader from './_layout/Mobile/Header';
|
||||
import DesktopHeader from '../settings/_layout/Desktop/Header';
|
||||
import MobileHeader from '../settings/_layout/Mobile/Header';
|
||||
|
||||
export default memo(() => {
|
||||
interface SettingsPageProps {
|
||||
mobile?: boolean;
|
||||
}
|
||||
|
||||
const SettingsPage = memo<SettingsPageProps>(({ mobile = false }) => {
|
||||
const { t } = useTranslation('setting');
|
||||
const [tab, setTab] = useState(ChatSettingsTabs.Prompt);
|
||||
const theme = useTheme();
|
||||
@@ -38,8 +46,8 @@ export default memo(() => {
|
||||
|
||||
const { isLoading } = useInitAgentConfig();
|
||||
|
||||
return (
|
||||
<MobileContentLayout header={<MobileHeader />}>
|
||||
const content = (
|
||||
<>
|
||||
<PageTitle title={t('header.sessionWithName', { name: title })} />
|
||||
<Tabs
|
||||
activeKey={tab}
|
||||
@@ -59,7 +67,28 @@ export default memo(() => {
|
||||
onMetaChange={updateAgentMeta}
|
||||
tab={tab}
|
||||
/>
|
||||
<Footer />
|
||||
</MobileContentLayout>
|
||||
</>
|
||||
);
|
||||
|
||||
if (mobile) {
|
||||
return (
|
||||
<MobileContentLayout header={<MobileHeader />}>
|
||||
{content}
|
||||
<Footer />
|
||||
</MobileContentLayout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DesktopHeader />
|
||||
<SettingContainer addonAfter={<Footer />} addonBefore={<SafeSpacing height={HEADER_HEIGHT} />}>
|
||||
{content}
|
||||
</SettingContainer>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
SettingsPage.displayName = 'SettingsPage';
|
||||
|
||||
export default SettingsPage;
|
||||
@@ -14,6 +14,9 @@ import ConversationArea from './ConversationArea';
|
||||
import PortalPanel from './PortalPanel';
|
||||
import TopicSidebar from './TopicSidebar';
|
||||
|
||||
interface WorkspaceLayoutProps {
|
||||
mobile?: boolean;
|
||||
}
|
||||
|
||||
const DesktopWorkspace = memo(() => (
|
||||
<>
|
||||
@@ -57,4 +60,14 @@ const MobileWorkspace = memo(() => (
|
||||
|
||||
MobileWorkspace.displayName = 'MobileWorkspace';
|
||||
|
||||
export { DesktopWorkspace, MobileWorkspace };
|
||||
const WorkspaceLayout = memo<WorkspaceLayoutProps>(({ mobile }) => {
|
||||
if (mobile) {
|
||||
return <MobileWorkspace />;
|
||||
}
|
||||
|
||||
return <DesktopWorkspace />;
|
||||
});
|
||||
|
||||
WorkspaceLayout.displayName = 'WorkspaceLayout';
|
||||
|
||||
export default WorkspaceLayout;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
});
|
||||
|
||||
+12
-10
@@ -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,19 +10,21 @@ import { useSend } from '../useSend';
|
||||
const MessageFromUrl = () => {
|
||||
const updateMessageInput = useChatStore((s) => s.updateMessageInput);
|
||||
const { send: sendMessage } = useSend();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useEffect(() => {
|
||||
const message = searchParams.get('message');
|
||||
if (!message) return;
|
||||
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);
|
||||
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete('message');
|
||||
setSearchParams(params, { replace: true });
|
||||
|
||||
updateMessageInput(message);
|
||||
sendMessage();
|
||||
}, [searchParams, setSearchParams, updateMessageInput, sendMessage]);
|
||||
updateMessageInput(message);
|
||||
sendMessage();
|
||||
}
|
||||
}, [searchParams, updateMessageInput, sendMessage]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
+1
@@ -10,6 +10,7 @@ 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(() => {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
'use client';
|
||||
|
||||
export { default } from '@/components/Error';
|
||||
@@ -3,6 +3,7 @@
|
||||
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';
|
||||
@@ -15,4 +16,4 @@ const Title = memo(() => {
|
||||
return <PageTitle title={[topicTitle, agentTitle].filter(Boolean).join(' · ')} />;
|
||||
});
|
||||
|
||||
export default Title;
|
||||
export default withSuspense(Title);
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
'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 };
|
||||
@@ -0,0 +1,10 @@
|
||||
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;
|
||||
@@ -0,0 +1,3 @@
|
||||
import Loading from '@/components/Loading/BrandTextLoading';
|
||||
|
||||
export default () => <Loading />;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from '@/components/404';
|
||||
@@ -0,0 +1,12 @@
|
||||
'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,15 +1,14 @@
|
||||
'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);
|
||||
@@ -18,11 +17,10 @@ const SessionHydration = memo(() => {
|
||||
const [switchTopic] = useChatStore((s) => [s.switchTopic]);
|
||||
|
||||
// two-way bindings the url and session store
|
||||
const [session, setSession] = useQueryParam('session', parseAsString.withDefault('inbox'), {
|
||||
history: 'replace',
|
||||
throttleMs: THROTTLE_DELAY,
|
||||
});
|
||||
|
||||
const [session, setSession] = useQueryState(
|
||||
'session',
|
||||
parseAsString.withDefault('inbox').withOptions({ history: 'replace', throttleMs: 50 }),
|
||||
);
|
||||
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,12 +24,15 @@ 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();
|
||||
}
|
||||
@@ -37,7 +40,6 @@ 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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const { showCreateSession } = useServerConfigStore(featureFlagsSelectors);
|
||||
|
||||
return (
|
||||
<ChatHeader
|
||||
left={
|
||||
<Flexbox align={'center'} gap={8} horizontal style={{ marginLeft: 8 }}>
|
||||
<UserAvatar onClick={() => navigate('/me')} size={32} />
|
||||
<UserAvatar onClick={() => router.push('/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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<ChatHeader
|
||||
left={<ChatHeaderTitle title={t('header.session')} />}
|
||||
onBackClick={() => navigate(pathString('/chat', { search: location.search }))}
|
||||
onBackClick={() => router.push(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 navigate = useNavigate();
|
||||
const router = useQueryRoute();
|
||||
|
||||
return (
|
||||
<ChatHeader
|
||||
center={<ChatHeader.Title title={t('header.session')} />}
|
||||
onBackClick={() => navigate(-1)}
|
||||
onBackClick={() => router.push('/chat')}
|
||||
right={<HeaderContent />}
|
||||
showBackButton
|
||||
style={mobileHeaderSticky}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
'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 navigate = useNavigate();
|
||||
const router = useRouter();
|
||||
const { t } = useTranslation('setting');
|
||||
const { t: tCommon } = useTranslation('common');
|
||||
|
||||
const handleGoToMarket = () => {
|
||||
if (identifier) {
|
||||
navigate(`/discover/assistant/${identifier}`);
|
||||
router.push(`/discover/assistant/${identifier}`);
|
||||
}
|
||||
onCancel();
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import SkeletonLoading from '@/components/Loading/SkeletonLoading';
|
||||
|
||||
export default () => <SkeletonLoading paragraph={{ rows: 8 }} />;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from '@/components/404';
|
||||
@@ -1,21 +0,0 @@
|
||||
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;
|
||||
+7
-15
@@ -1,19 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { PropsWithChildren } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { SCROLL_PARENT_ID } from '../../../features/const';
|
||||
import { SCROLL_PARENT_ID } from '@/app/[variants]/(main)/discover/features/const';
|
||||
import Footer from '@/features/Setting/Footer';
|
||||
|
||||
const MAX_WIDTH = 1440;
|
||||
|
||||
/**
|
||||
* Desktop Discover Detail Layout
|
||||
* Layout for detail pages (assistant, model, provider, mcp details)
|
||||
*/
|
||||
const DesktopDiscoverDetailLayout = memo(() => {
|
||||
const Layout = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<Flexbox
|
||||
align={'center'}
|
||||
@@ -24,15 +17,14 @@ const DesktopDiscoverDetailLayout = memo(() => {
|
||||
width={'100%'}
|
||||
>
|
||||
<Flexbox gap={24} style={{ maxWidth: MAX_WIDTH, minHeight: '100%' }} width={'100%'}>
|
||||
<Outlet />
|
||||
{children}
|
||||
<div />
|
||||
<Footer />
|
||||
</Flexbox>
|
||||
</Flexbox>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
DesktopDiscoverDetailLayout.displayName = 'DesktopDiscoverDetailLayout';
|
||||
Layout.displayName = 'DesktopDiscoverDetailLayout';
|
||||
|
||||
|
||||
export default DesktopDiscoverDetailLayout;
|
||||
export default Layout;
|
||||
@@ -0,0 +1,22 @@
|
||||
'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 = () => {
|
||||
const Layout = ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<MobileContentLayout gap={16} header={<Header />} id={SCROLL_PARENT_ID} padding={16}>
|
||||
<Outlet />
|
||||
{children}
|
||||
<div />
|
||||
<Footer />
|
||||
</MobileContentLayout>
|
||||
|
||||
+11
-18
@@ -2,9 +2,9 @@
|
||||
|
||||
import { memo } from 'react';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
import { useLoaderData } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import type { SlugParams } from '@/app/[variants]/loaders/routeParams';
|
||||
import { withSuspense } from '@/components/withSuspense';
|
||||
import { useQuery } from '@/hooks/useQuery';
|
||||
import { useDiscoverStore } from '@/store/discover';
|
||||
import { AssistantMarketSource, DiscoverTab } from '@/types/discover';
|
||||
@@ -12,19 +12,20 @@ import { AssistantMarketSource, DiscoverTab } from '@/types/discover';
|
||||
import NotFound from '../components/NotFound';
|
||||
import Breadcrumb from '../features/Breadcrumb';
|
||||
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';
|
||||
import { DetailProvider } from './[...slugs]/features/DetailProvider';
|
||||
import Details from './[...slugs]/features/Details';
|
||||
import Header from './[...slugs]/features/Header';
|
||||
import StatusPage from './[...slugs]/features/StatusPage';
|
||||
import Loading from './[...slugs]/loading';
|
||||
|
||||
interface AssistantDetailPageProps {
|
||||
mobile?: boolean;
|
||||
}
|
||||
|
||||
const AssistantDetailPage = memo<AssistantDetailPageProps>(({ mobile }) => {
|
||||
const { slug } = useLoaderData() as SlugParams;
|
||||
const identifier = decodeURIComponent(slug);
|
||||
const params = useParams();
|
||||
const slugs = params['*']?.split('/') || [];
|
||||
const identifier = decodeURIComponent(slugs.join('/'));
|
||||
const { version, source } = useQuery() as { source?: AssistantMarketSource; version?: string };
|
||||
|
||||
const useAssistantDetail = useDiscoverStore((s) => s.useAssistantDetail);
|
||||
@@ -52,12 +53,4 @@ const AssistantDetailPage = memo<AssistantDetailPageProps>(({ mobile }) => {
|
||||
);
|
||||
});
|
||||
|
||||
const DesktopDiscoverAssistantDetailPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <AssistantDetailPage mobile={false} />;
|
||||
});
|
||||
|
||||
const MobileDiscoverAssistantDetailPage = memo<{ mobile?: boolean }>(() => {
|
||||
return <AssistantDetailPage mobile={true} />;
|
||||
});
|
||||
|
||||
export { DesktopDiscoverAssistantDetailPage, MobileDiscoverAssistantDetailPage };
|
||||
export default withSuspense(AssistantDetailPage);
|
||||
@@ -0,0 +1,51 @@
|
||||
'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);
|
||||
+1
-1
@@ -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;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user