mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-16 12:36:07 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b5fc3656b | |||
| 06af7939e4 | |||
| e12965c7df | |||
| 7afd1318db | |||
| 6a374d2f32 | |||
| cec034721f | |||
| 2d70632d3e | |||
| 41c554d748 | |||
| 4e4933d861 | |||
| a5bb31b844 |
@@ -2,6 +2,73 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [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">
|
||||
|
||||
[](#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">
|
||||
|
||||
[](#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,16 @@
|
||||
[
|
||||
{
|
||||
"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."]
|
||||
|
||||
@@ -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": "包含插件信息",
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lobehub/lobehub",
|
||||
"version": "2.0.0-next.69",
|
||||
"version": "2.0.0-next.72",
|
||||
"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",
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { copyImageToClipboard, sanitizeSVGContent } from '@lobechat/utils/client';
|
||||
import { Button, Dropdown, Tooltip } from '@lobehub/ui';
|
||||
import { snapdom } from '@zumer/snapdom';
|
||||
import { App, Space } from 'antd';
|
||||
import { css, cx } from 'antd-style';
|
||||
import { CopyIcon, DownloadIcon } from 'lucide-react';
|
||||
import { domToPng } from 'modern-screenshot';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
@@ -41,12 +41,29 @@ const SVGRenderer = ({ content }: SVGRendererProps) => {
|
||||
const sanitizedContent = useMemo(() => sanitizeSVGContent(content), [content]);
|
||||
|
||||
const generatePng = async () => {
|
||||
return domToPng(document.querySelector(`#${DOM_ID}`) as HTMLDivElement, {
|
||||
features: {
|
||||
// 不启用移除控制符,否则会导致 safari emoji 报错
|
||||
removeControlCharacter: false,
|
||||
},
|
||||
const blob = await snapdom.toBlob(document.querySelector(`#${DOM_ID}`) as HTMLDivElement, {
|
||||
scale: 2,
|
||||
type: 'png',
|
||||
});
|
||||
|
||||
if (!blob) {
|
||||
throw new Error('Failed to generate PNG blob');
|
||||
}
|
||||
|
||||
// Convert blob to data URL
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error('FileReader result is not a string'));
|
||||
}
|
||||
});
|
||||
reader.addEventListener('error', () =>
|
||||
reject(reader.error || new Error('Failed to read blob as data URL')),
|
||||
);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useStyles } from './style';
|
||||
import { FieldType } from './type';
|
||||
|
||||
const Preview = memo<FieldType & { title?: string }>(
|
||||
({ title, withSystemRole, withBackground, withFooter }) => {
|
||||
({ title, withSystemRole, withBackground, withFooter, widthMode }) => {
|
||||
const [model, plugins, systemRole] = useAgentStore((s) => [
|
||||
agentSelectors.currentAgentModel(s),
|
||||
agentSelectors.displayableAgentPlugins(s),
|
||||
@@ -34,7 +34,7 @@ const Preview = memo<FieldType & { title?: string }>(
|
||||
|
||||
const { t } = useTranslation('chat');
|
||||
const { styles } = useStyles(withBackground);
|
||||
const { styles: containerStyles } = useContainerStyles();
|
||||
const { styles: containerStyles } = useContainerStyles(widthMode);
|
||||
|
||||
const displayTitle = isInbox ? t('inbox.title') : title;
|
||||
const displayDesc = isInbox ? t('inbox.desc') : description;
|
||||
|
||||
@@ -14,10 +14,11 @@ import { sessionMetaSelectors } from '@/store/session/selectors';
|
||||
|
||||
import { useStyles } from '../style';
|
||||
import Preview from './Preview';
|
||||
import { FieldType } from './type';
|
||||
import { FieldType, WidthMode } from './type';
|
||||
|
||||
const DEFAULT_FIELD_VALUE: FieldType = {
|
||||
imageType: ImageType.JPG,
|
||||
widthMode: WidthMode.Wide,
|
||||
withBackground: true,
|
||||
withFooter: true,
|
||||
withPluginInfo: false,
|
||||
@@ -34,7 +35,20 @@ const ShareImage = memo<{ mobile?: boolean }>(() => {
|
||||
title: currentAgentTitle,
|
||||
});
|
||||
const { loading: copyLoading, onCopy } = useImgToClipboard();
|
||||
|
||||
const widthModeOptions = [
|
||||
{ label: t('shareModal.widthMode.wide'), value: WidthMode.Wide },
|
||||
{ label: t('shareModal.widthMode.narrow'), value: WidthMode.Narrow },
|
||||
];
|
||||
|
||||
const settings: FormItemProps[] = [
|
||||
{
|
||||
children: <Segmented options={widthModeOptions} />,
|
||||
label: t('shareModal.widthMode.label'),
|
||||
layout: 'horizontal',
|
||||
minWidth: undefined,
|
||||
name: 'widthMode',
|
||||
},
|
||||
{
|
||||
children: <Switch />,
|
||||
label: t('shareModal.withSystemRole'),
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { ImageType } from '@/hooks/useScreenshot';
|
||||
|
||||
export enum WidthMode {
|
||||
Narrow = 'narrow',
|
||||
Wide = 'wide'
|
||||
}
|
||||
|
||||
export type FieldType = {
|
||||
imageType: ImageType;
|
||||
widthMode: WidthMode;
|
||||
withBackground: boolean;
|
||||
withFooter: boolean;
|
||||
withPluginInfo: boolean;
|
||||
|
||||
@@ -1,36 +1,46 @@
|
||||
import { createStyles } from 'antd-style';
|
||||
|
||||
export const useContainerStyles = createStyles(({ css, token, stylish, cx, responsive }) => ({
|
||||
preview: cx(
|
||||
stylish.noScrollbar,
|
||||
css`
|
||||
overflow: hidden scroll;
|
||||
import { WidthMode } from './ShareImage/type';
|
||||
|
||||
width: 100%;
|
||||
max-height: 70dvh;
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
export const useContainerStyles = createStyles(
|
||||
({ css, token, stylish, cx, responsive }, widthMode?: WidthMode) => {
|
||||
const isNarrow = widthMode === WidthMode.Narrow;
|
||||
|
||||
background: ${token.colorBgLayout};
|
||||
return {
|
||||
preview: cx(
|
||||
stylish.noScrollbar,
|
||||
css`
|
||||
overflow: hidden scroll;
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.react-pdf__Document *,
|
||||
.react-pdf__Page * {
|
||||
pointer-events: none;
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
width: 100%;
|
||||
max-width: ${isNarrow ? '480px' : 'none'};
|
||||
max-height: 70dvh;
|
||||
margin: ${isNarrow ? '0 auto' : '0'};
|
||||
border: 1px solid ${token.colorBorder};
|
||||
border-radius: ${token.borderRadiusLG}px;
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
background: ${token.colorBgLayout};
|
||||
|
||||
${responsive.mobile} {
|
||||
max-height: 40dvh;
|
||||
}
|
||||
`,
|
||||
),
|
||||
}));
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.react-pdf__Document *,
|
||||
.react-pdf__Page * {
|
||||
pointer-events: none;
|
||||
}
|
||||
/* stylelint-enable selector-class-pattern */
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
${responsive.mobile} {
|
||||
max-height: 40dvh;
|
||||
}
|
||||
`,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const useStyles = createStyles(({ responsive, token, css }) => ({
|
||||
body: css`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { enableNextAuth } from '@lobechat/const';
|
||||
import { enableNextAuth, isDesktop } from '@lobechat/const';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { memo } from 'react';
|
||||
@@ -37,7 +37,7 @@ const PanelContent = memo<{ closePopover: () => void }>(({ closePopover }) => {
|
||||
|
||||
return (
|
||||
<Flexbox gap={2} style={{ minWidth: 300 }}>
|
||||
{isLoginWithAuth ? (
|
||||
{isDesktop || isLoginWithAuth ? (
|
||||
<>
|
||||
<UserInfo avatarProps={{ clickable: false }} />
|
||||
|
||||
|
||||
+44
-28
@@ -1,6 +1,6 @@
|
||||
import type { SegmentedProps } from '@lobehub/ui';
|
||||
import { snapdom } from '@zumer/snapdom';
|
||||
import dayjs from 'dayjs';
|
||||
import { domToJpeg, domToPng, domToSvg, domToWebp } from 'modern-screenshot';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { BRANDING_NAME } from '@/const/branding';
|
||||
@@ -40,26 +40,6 @@ export const getImageUrl = async ({
|
||||
imageType: ImageType;
|
||||
width?: number;
|
||||
}) => {
|
||||
let screenshotFn: any;
|
||||
switch (imageType) {
|
||||
case ImageType.JPG: {
|
||||
screenshotFn = domToJpeg;
|
||||
break;
|
||||
}
|
||||
case ImageType.PNG: {
|
||||
screenshotFn = domToPng;
|
||||
break;
|
||||
}
|
||||
case ImageType.SVG: {
|
||||
screenshotFn = domToSvg;
|
||||
break;
|
||||
}
|
||||
case ImageType.WEBP: {
|
||||
screenshotFn = domToWebp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const dom: HTMLDivElement = document.querySelector(id) as HTMLDivElement;
|
||||
let copy: HTMLDivElement = dom;
|
||||
|
||||
@@ -69,18 +49,54 @@ export const getImageUrl = async ({
|
||||
document.body.append(copy);
|
||||
}
|
||||
|
||||
const dataUrl = await screenshotFn(width ? copy : dom, {
|
||||
features: {
|
||||
// 不启用移除控制符,否则会导致 safari emoji 报错
|
||||
removeControlCharacter: false,
|
||||
},
|
||||
const baseOptions = {
|
||||
scale: 2,
|
||||
width,
|
||||
});
|
||||
};
|
||||
|
||||
let blob: Blob;
|
||||
|
||||
if (imageType === ImageType.SVG) {
|
||||
// For SVG, we need to use the full snapdom API to get the raw SVG string
|
||||
const result = await snapdom(width ? copy : dom, baseOptions);
|
||||
const svgString = result.toRaw();
|
||||
blob = new Blob([svgString], { type: 'image/svg+xml' });
|
||||
} else {
|
||||
// For raster formats, use toBlob directly with type option
|
||||
const blobType = (imageType === ImageType.JPG ? 'jpg' : imageType) as 'png' | 'jpg' | 'webp';
|
||||
const blobResult = await snapdom.toBlob(width ? copy : dom, {
|
||||
type: blobType,
|
||||
useProxy: 'https://proxy.corsfix.com/?',
|
||||
});
|
||||
|
||||
if (!blobResult) {
|
||||
throw new Error('Failed to generate blob from snapdom');
|
||||
}
|
||||
|
||||
blob = blobResult;
|
||||
}
|
||||
|
||||
if (width && copy) copy?.remove();
|
||||
|
||||
return dataUrl;
|
||||
if (!blob) {
|
||||
throw new Error('Blob is undefined');
|
||||
}
|
||||
|
||||
// Convert blob to data URL using FileReader
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
if (typeof reader.result === 'string') {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error('FileReader result is not a string'));
|
||||
}
|
||||
});
|
||||
reader.addEventListener('error', () =>
|
||||
reject(reader.error || new Error('Failed to read blob as data URL')),
|
||||
);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
export const useScreenshot = ({
|
||||
|
||||
@@ -361,6 +361,11 @@ export default {
|
||||
screenshot: '截图',
|
||||
settings: '导出设置',
|
||||
text: '文本',
|
||||
widthMode: {
|
||||
label: '宽度模式',
|
||||
narrow: '窄屏模式',
|
||||
wide: '宽屏模式',
|
||||
},
|
||||
withBackground: '包含背景图片',
|
||||
withFooter: '包含页脚',
|
||||
withPluginInfo: '包含插件信息',
|
||||
|
||||
Reference in New Issue
Block a user