@lobehub/lobehub 2.0.0-next.216 → 2.0.0-next.218
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/changelog/v1.json +17 -0
- package/package.json +1 -1
- package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +18 -31
- package/packages/builtin-tool-cloud-sandbox/src/types.ts +3 -3
- package/packages/utils/src/server/index.ts +0 -1
- package/src/app/[variants]/(main)/chat/profile/features/EditorCanvas/TypoBar.tsx +1 -11
- package/src/app/[variants]/(main)/group/profile/features/EditorCanvas/TypoBar.tsx +1 -11
- package/src/app/[variants]/(main)/image/features/PromptInput/index.tsx +44 -0
- package/src/app/[variants]/(main)/memory/features/EditableModal/index.tsx +8 -101
- package/src/features/ChatInput/InputEditor/index.tsx +1 -0
- package/src/features/ChatInput/TypoBar/index.tsx +0 -11
- package/src/features/CommandMenu/AskAIMenu.tsx +47 -14
- package/src/features/Conversation/ChatItem/components/MessageContent/index.tsx +11 -12
- package/src/features/EditorModal/EditorCanvas.tsx +81 -0
- package/src/features/EditorModal/TextareCanvas.tsx +28 -0
- package/src/features/{Conversation/ChatItem/components/MessageContent → EditorModal}/Typobar.tsx +0 -11
- package/src/features/EditorModal/index.tsx +51 -0
- package/src/features/ModelSwitchPanel/index.tsx +21 -1
- package/src/features/PageEditor/EditorCanvas/InlineToolbar.tsx +1 -17
- package/src/server/routers/tools/market.ts +118 -102
- package/src/server/services/discover/index.ts +10 -5
- package/src/services/codeInterpreter.ts +12 -20
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +13 -86
- package/packages/utils/src/server/__tests__/geo.test.ts +0 -116
- package/packages/utils/src/server/geo.ts +0 -60
- package/src/app/[variants]/(main)/memory/features/EditableModal/Typobar.tsx +0 -150
- package/src/features/Conversation/ChatItem/components/MessageContent/EditableModal.tsx +0 -119
|
@@ -9,63 +9,20 @@ import { type StateCreator } from 'zustand/vanilla';
|
|
|
9
9
|
|
|
10
10
|
import { type MCPToolCallResult } from '@/libs/mcp';
|
|
11
11
|
import { chatService } from '@/services/chat';
|
|
12
|
-
import { codeInterpreterService } from '@/services/codeInterpreter';
|
|
13
|
-
import { fileService } from '@/services/file';
|
|
14
12
|
import { mcpService } from '@/services/mcp';
|
|
15
13
|
import { messageService } from '@/services/message';
|
|
16
14
|
import { AI_RUNTIME_OPERATION_TYPES } from '@/store/chat/slices/operation';
|
|
17
15
|
import { type ChatStore } from '@/store/chat/store';
|
|
18
16
|
import { useToolStore } from '@/store/tool';
|
|
19
17
|
import { hasExecutor } from '@/store/tool/slices/builtin/executors';
|
|
18
|
+
import { useUserStore } from '@/store/user';
|
|
19
|
+
import { userProfileSelectors } from '@/store/user/slices/auth/selectors';
|
|
20
20
|
import { safeParseJSON } from '@/utils/safeParseJSON';
|
|
21
21
|
|
|
22
22
|
import { dbMessageSelectors } from '../../message/selectors';
|
|
23
23
|
|
|
24
24
|
const log = debug('lobe-store:plugin-types');
|
|
25
25
|
|
|
26
|
-
/**
|
|
27
|
-
* Get MIME type from filename extension
|
|
28
|
-
*/
|
|
29
|
-
const getMimeTypeFromFilename = (filename: string): string => {
|
|
30
|
-
const ext = filename.split('.').pop()?.toLowerCase() || '';
|
|
31
|
-
const mimeTypes: Record<string, string> = {
|
|
32
|
-
// Images
|
|
33
|
-
bmp: 'image/bmp',
|
|
34
|
-
gif: 'image/gif',
|
|
35
|
-
jpeg: 'image/jpeg',
|
|
36
|
-
jpg: 'image/jpeg',
|
|
37
|
-
png: 'image/png',
|
|
38
|
-
svg: 'image/svg+xml',
|
|
39
|
-
webp: 'image/webp',
|
|
40
|
-
// Videos
|
|
41
|
-
mp4: 'video/mp4',
|
|
42
|
-
webm: 'video/webm',
|
|
43
|
-
mov: 'video/quicktime',
|
|
44
|
-
avi: 'video/x-msvideo',
|
|
45
|
-
// Documents
|
|
46
|
-
csv: 'text/csv',
|
|
47
|
-
doc: 'application/msword',
|
|
48
|
-
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
49
|
-
html: 'text/html',
|
|
50
|
-
json: 'application/json',
|
|
51
|
-
md: 'text/markdown',
|
|
52
|
-
pdf: 'application/pdf',
|
|
53
|
-
ppt: 'application/vnd.ms-powerpoint',
|
|
54
|
-
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
55
|
-
rtf: 'application/rtf',
|
|
56
|
-
txt: 'text/plain',
|
|
57
|
-
xls: 'application/vnd.ms-excel',
|
|
58
|
-
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
59
|
-
xml: 'application/xml',
|
|
60
|
-
// Code
|
|
61
|
-
css: 'text/css',
|
|
62
|
-
js: 'text/javascript',
|
|
63
|
-
py: 'text/x-python',
|
|
64
|
-
ts: 'text/typescript',
|
|
65
|
-
};
|
|
66
|
-
return mimeTypes[ext] || 'application/octet-stream';
|
|
67
|
-
};
|
|
68
|
-
|
|
69
26
|
/**
|
|
70
27
|
* Plugin type-specific implementations
|
|
71
28
|
* Each method handles a specific type of plugin invocation
|
|
@@ -284,10 +241,13 @@ export const pluginTypes: StateCreator<
|
|
|
284
241
|
const { CloudSandboxExecutionRuntime } =
|
|
285
242
|
await import('@lobechat/builtin-tool-cloud-sandbox/executionRuntime');
|
|
286
243
|
|
|
244
|
+
// Get userId from user store
|
|
245
|
+
const userId = userProfileSelectors.userId(useUserStore.getState()) || 'anonymous';
|
|
246
|
+
|
|
287
247
|
// Create runtime with context
|
|
288
248
|
const runtime = new CloudSandboxExecutionRuntime({
|
|
289
249
|
topicId: message?.topicId || 'default',
|
|
290
|
-
userId
|
|
250
|
+
userId,
|
|
291
251
|
});
|
|
292
252
|
|
|
293
253
|
// Parse arguments
|
|
@@ -341,58 +301,25 @@ export const pluginTypes: StateCreator<
|
|
|
341
301
|
context,
|
|
342
302
|
);
|
|
343
303
|
|
|
344
|
-
// Handle exportFile:
|
|
304
|
+
// Handle exportFile: associate the file (already created by server) with assistant message (parent)
|
|
345
305
|
if (payload.apiName === 'exportFile' && data.success && data.state) {
|
|
346
306
|
const exportState = data.state as ExportFileState;
|
|
347
|
-
|
|
307
|
+
// Server now creates the file record and returns fileId in the response
|
|
308
|
+
if (exportState.fileId && exportState.filename) {
|
|
348
309
|
try {
|
|
349
|
-
//
|
|
350
|
-
// Extract the path before query params: .../code-interpreter-exports/tpc_xxx/filename.ext
|
|
351
|
-
const urlPath = exportState.downloadUrl.split('?')[0];
|
|
352
|
-
const hash = `ci-export-${btoa(urlPath).slice(0, 32)}`;
|
|
353
|
-
|
|
354
|
-
// Use mimeType from state if available, otherwise infer from filename
|
|
355
|
-
const mimeType = exportState.mimeType || getMimeTypeFromFilename(exportState.filename);
|
|
356
|
-
|
|
357
|
-
// 1. Create file record in database
|
|
358
|
-
const fileResult = await fileService.createFile({
|
|
359
|
-
fileType: mimeType,
|
|
360
|
-
hash,
|
|
361
|
-
name: exportState.filename,
|
|
362
|
-
size: exportState.size || 0,
|
|
363
|
-
source: 'code-interpreter',
|
|
364
|
-
url: exportState.downloadUrl,
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// 2. If there's text content, save it to documents table for retrieval
|
|
368
|
-
if (exportState.content) {
|
|
369
|
-
await codeInterpreterService.saveExportedFileContent({
|
|
370
|
-
content: exportState.content,
|
|
371
|
-
fileId: fileResult.id,
|
|
372
|
-
fileType: mimeType,
|
|
373
|
-
filename: exportState.filename,
|
|
374
|
-
url: exportState.downloadUrl,
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
log(
|
|
378
|
-
'[invokeCloudCodeInterpreterTool] Saved file content to document: fileId=%s',
|
|
379
|
-
fileResult.id,
|
|
380
|
-
);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// 3. Associate file with the assistant message (parent of tool message)
|
|
310
|
+
// Associate file with the assistant message (parent of tool message)
|
|
384
311
|
// The current message (id) is the tool message, we need to attach to its parent
|
|
385
312
|
const targetMessageId = message?.parentId || id;
|
|
386
313
|
|
|
387
|
-
await messageService.addFilesToMessage(targetMessageId, [
|
|
314
|
+
await messageService.addFilesToMessage(targetMessageId, [exportState.fileId], {
|
|
388
315
|
agentId: message?.agentId,
|
|
389
316
|
topicId: message?.topicId,
|
|
390
317
|
});
|
|
391
318
|
|
|
392
319
|
log(
|
|
393
|
-
'[invokeCloudCodeInterpreterTool]
|
|
320
|
+
'[invokeCloudCodeInterpreterTool] Associated exported file with message: targetMessageId=%s, fileId=%s, filename=%s',
|
|
394
321
|
targetMessageId,
|
|
395
|
-
|
|
322
|
+
exportState.fileId,
|
|
396
323
|
exportState.filename,
|
|
397
324
|
);
|
|
398
325
|
} catch (error) {
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { geolocation } from '@vercel/functions';
|
|
2
|
-
import { getCountry } from 'countries-and-timezones';
|
|
3
|
-
import { NextRequest } from 'next/server';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
|
|
6
|
-
import { parseDefaultThemeFromCountry } from '../geo';
|
|
7
|
-
|
|
8
|
-
vi.mock('@vercel/functions', () => ({
|
|
9
|
-
geolocation: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock('countries-and-timezones', () => ({
|
|
13
|
-
getCountry: vi.fn(),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
describe('parseDefaultThemeFromCountry', () => {
|
|
17
|
-
const mockRequest = (headers: Record<string, string> = {}) => {
|
|
18
|
-
return {
|
|
19
|
-
headers: {
|
|
20
|
-
get: (key: string) => headers[key],
|
|
21
|
-
},
|
|
22
|
-
} as NextRequest;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
it('should return light theme when no country code is found', () => {
|
|
26
|
-
vi.mocked(geolocation).mockReturnValue({});
|
|
27
|
-
const request = mockRequest();
|
|
28
|
-
expect(parseDefaultThemeFromCountry(request)).toBe('light');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('should return light theme when country has no timezone', () => {
|
|
32
|
-
vi.mocked(geolocation).mockReturnValue({ country: 'US' });
|
|
33
|
-
vi.mocked(getCountry).mockReturnValue({
|
|
34
|
-
id: 'US',
|
|
35
|
-
name: 'United States',
|
|
36
|
-
timezones: [],
|
|
37
|
-
});
|
|
38
|
-
const request = mockRequest();
|
|
39
|
-
expect(parseDefaultThemeFromCountry(request)).toBe('light');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should return light theme when country has invalid timezone', () => {
|
|
43
|
-
vi.mocked(geolocation).mockReturnValue({ country: 'US' });
|
|
44
|
-
vi.mocked(getCountry).mockReturnValue({
|
|
45
|
-
id: 'US',
|
|
46
|
-
name: 'United States',
|
|
47
|
-
// @ts-ignore
|
|
48
|
-
timezones: ['America/Invalid'],
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
const mockDate = new Date('2025-04-01T12:00:00');
|
|
52
|
-
vi.setSystemTime(mockDate);
|
|
53
|
-
|
|
54
|
-
const request = mockRequest();
|
|
55
|
-
expect(parseDefaultThemeFromCountry(request)).toBe('light');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should return light theme during daytime hours', () => {
|
|
59
|
-
vi.mocked(geolocation).mockReturnValue({ country: 'US' });
|
|
60
|
-
vi.mocked(getCountry).mockReturnValue({
|
|
61
|
-
id: 'US',
|
|
62
|
-
name: 'United States',
|
|
63
|
-
timezones: ['America/New_York'],
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// 设置UTC时间16:00,这样在纽约时区(EDT,UTC-4)就是12:00
|
|
67
|
-
const mockDate = new Date('2025-04-01T16:00:00.000Z');
|
|
68
|
-
vi.setSystemTime(mockDate);
|
|
69
|
-
|
|
70
|
-
const request = mockRequest();
|
|
71
|
-
const result = parseDefaultThemeFromCountry(request);
|
|
72
|
-
expect(result).toBe('light');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should return dark theme during night hours', () => {
|
|
76
|
-
vi.mocked(geolocation).mockReturnValue({ country: 'US' });
|
|
77
|
-
vi.mocked(getCountry).mockReturnValue({
|
|
78
|
-
id: 'US',
|
|
79
|
-
name: 'United States',
|
|
80
|
-
timezones: ['America/New_York'],
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// 设置UTC时间02:00,这样在纽约时区(EDT,UTC-4)就是22:00
|
|
84
|
-
const mockDate = new Date('2025-04-01T02:00:00.000Z');
|
|
85
|
-
vi.setSystemTime(mockDate);
|
|
86
|
-
|
|
87
|
-
const request = mockRequest();
|
|
88
|
-
expect(parseDefaultThemeFromCountry(request)).toBe('dark');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should try different header sources for country code', () => {
|
|
92
|
-
vi.mocked(geolocation).mockReturnValue({});
|
|
93
|
-
vi.mocked(getCountry).mockReturnValue({
|
|
94
|
-
id: 'US',
|
|
95
|
-
name: 'United States',
|
|
96
|
-
timezones: ['America/New_York'],
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const headers = {
|
|
100
|
-
'x-vercel-ip-country': 'US',
|
|
101
|
-
'cf-ipcountry': 'CA',
|
|
102
|
-
'x-zeabur-ip-country': 'UK',
|
|
103
|
-
'x-country-code': 'FR',
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const request = mockRequest(headers);
|
|
107
|
-
parseDefaultThemeFromCountry(request);
|
|
108
|
-
|
|
109
|
-
expect(getCountry).toHaveBeenCalledWith('US');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
afterEach(() => {
|
|
113
|
-
vi.useRealTimers();
|
|
114
|
-
vi.clearAllMocks();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { geolocation } from '@vercel/functions';
|
|
2
|
-
import { getCountry } from 'countries-and-timezones';
|
|
3
|
-
import { NextRequest } from 'next/server';
|
|
4
|
-
|
|
5
|
-
const getLocalTime = (timeZone: string) => {
|
|
6
|
-
return new Date().toLocaleString('en-US', {
|
|
7
|
-
hour: 'numeric',
|
|
8
|
-
hour12: false,
|
|
9
|
-
timeZone,
|
|
10
|
-
});
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
const isValidTimeZone = (timeZone: string) => {
|
|
14
|
-
try {
|
|
15
|
-
getLocalTime(timeZone);
|
|
16
|
-
return true; // If no exception is thrown, the timezone is valid
|
|
17
|
-
} catch (e) {
|
|
18
|
-
// If a RangeError is caught, the timezone is invalid
|
|
19
|
-
if (e instanceof RangeError) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
// If it's another error, better to re-throw it
|
|
23
|
-
throw e;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export const parseDefaultThemeFromCountry = (request: NextRequest) => {
|
|
28
|
-
// 1. Get country code from request headers
|
|
29
|
-
const geo = geolocation(request);
|
|
30
|
-
|
|
31
|
-
const countryCode =
|
|
32
|
-
geo?.country ||
|
|
33
|
-
request.headers.get('x-vercel-ip-country') || // Vercel
|
|
34
|
-
request.headers.get('cf-ipcountry') || // Cloudflare
|
|
35
|
-
request.headers.get('x-zeabur-ip-country') || // Zeabur
|
|
36
|
-
request.headers.get('x-country-code'); // Netlify
|
|
37
|
-
|
|
38
|
-
// If no country code is obtained, return light theme directly
|
|
39
|
-
if (!countryCode) return 'light';
|
|
40
|
-
|
|
41
|
-
// 2. Get timezone information for the country
|
|
42
|
-
const country = getCountry(countryCode);
|
|
43
|
-
|
|
44
|
-
// If country information is not found or the country has no timezone information, return light theme
|
|
45
|
-
if (!country?.timezones?.length) return 'light';
|
|
46
|
-
|
|
47
|
-
const timeZone = country.timezones.find((tz) => isValidTimeZone(tz));
|
|
48
|
-
if (!timeZone) return 'light';
|
|
49
|
-
|
|
50
|
-
// 3. Get the current time in the country's first timezone
|
|
51
|
-
const localTime = getLocalTime(timeZone);
|
|
52
|
-
|
|
53
|
-
// 4. Parse the hour and determine the theme
|
|
54
|
-
const localHour = parseInt(localTime);
|
|
55
|
-
// console.log(
|
|
56
|
-
// `[theme] Country: ${countryCode}, Timezone: ${country.timezones[0]}, LocalHour: ${localHour}`,
|
|
57
|
-
// );
|
|
58
|
-
|
|
59
|
-
return localHour >= 6 && localHour < 18 ? 'light' : 'dark';
|
|
60
|
-
};
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { HotkeyEnum, type IEditor, getHotkeyById } from '@lobehub/editor';
|
|
2
|
-
import { useEditorState } from '@lobehub/editor/react';
|
|
3
|
-
import {
|
|
4
|
-
ChatInputActionBar,
|
|
5
|
-
ChatInputActions,
|
|
6
|
-
type ChatInputActionsProps,
|
|
7
|
-
CodeLanguageSelect,
|
|
8
|
-
} from '@lobehub/editor/react';
|
|
9
|
-
import { cssVar } from 'antd-style';
|
|
10
|
-
import {
|
|
11
|
-
BoldIcon,
|
|
12
|
-
CodeXmlIcon,
|
|
13
|
-
ItalicIcon,
|
|
14
|
-
ListIcon,
|
|
15
|
-
ListOrderedIcon,
|
|
16
|
-
ListTodoIcon,
|
|
17
|
-
MessageSquareQuote,
|
|
18
|
-
SigmaIcon,
|
|
19
|
-
SquareDashedBottomCodeIcon,
|
|
20
|
-
StrikethroughIcon,
|
|
21
|
-
UnderlineIcon,
|
|
22
|
-
} from 'lucide-react';
|
|
23
|
-
import { memo, useMemo } from 'react';
|
|
24
|
-
import { useTranslation } from 'react-i18next';
|
|
25
|
-
|
|
26
|
-
const TypoBar = memo<{ editor?: IEditor }>(({ editor }) => {
|
|
27
|
-
const { t } = useTranslation('editor');
|
|
28
|
-
const editorState = useEditorState(editor);
|
|
29
|
-
|
|
30
|
-
const items: ChatInputActionsProps['items'] = useMemo(
|
|
31
|
-
() =>
|
|
32
|
-
[
|
|
33
|
-
{
|
|
34
|
-
active: editorState.isBold,
|
|
35
|
-
icon: BoldIcon,
|
|
36
|
-
key: 'bold',
|
|
37
|
-
label: t('typobar.bold'),
|
|
38
|
-
onClick: editorState.bold,
|
|
39
|
-
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Bold).keys },
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
active: editorState.isItalic,
|
|
43
|
-
icon: ItalicIcon,
|
|
44
|
-
key: 'italic',
|
|
45
|
-
label: t('typobar.italic'),
|
|
46
|
-
onClick: editorState.italic,
|
|
47
|
-
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Italic).keys },
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
active: editorState.isUnderline,
|
|
51
|
-
icon: UnderlineIcon,
|
|
52
|
-
key: 'underline',
|
|
53
|
-
label: t('typobar.underline'),
|
|
54
|
-
onClick: editorState.underline,
|
|
55
|
-
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Underline).keys },
|
|
56
|
-
},
|
|
57
|
-
{
|
|
58
|
-
active: editorState.isStrikethrough,
|
|
59
|
-
icon: StrikethroughIcon,
|
|
60
|
-
key: 'strikethrough',
|
|
61
|
-
label: t('typobar.strikethrough'),
|
|
62
|
-
onClick: editorState.strikethrough,
|
|
63
|
-
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.Strikethrough).keys },
|
|
64
|
-
},
|
|
65
|
-
{
|
|
66
|
-
type: 'divider',
|
|
67
|
-
},
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
icon: ListIcon,
|
|
71
|
-
key: 'bulletList',
|
|
72
|
-
label: t('typobar.bulletList'),
|
|
73
|
-
onClick: editorState.bulletList,
|
|
74
|
-
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.BulletList).keys },
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
icon: ListOrderedIcon,
|
|
78
|
-
key: 'numberlist',
|
|
79
|
-
label: t('typobar.numberList'),
|
|
80
|
-
onClick: editorState.numberList,
|
|
81
|
-
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.NumberList).keys },
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
icon: ListTodoIcon,
|
|
85
|
-
key: 'tasklist',
|
|
86
|
-
label: t('typobar.taskList'),
|
|
87
|
-
onClick: editorState.checkList,
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
type: 'divider',
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
active: editorState.isBlockquote,
|
|
94
|
-
icon: MessageSquareQuote,
|
|
95
|
-
key: 'blockquote',
|
|
96
|
-
label: t('typobar.blockquote'),
|
|
97
|
-
onClick: editorState.blockquote,
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
type: 'divider',
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
icon: SigmaIcon,
|
|
104
|
-
key: 'math',
|
|
105
|
-
label: t('typobar.tex'),
|
|
106
|
-
onClick: editorState.insertMath,
|
|
107
|
-
},
|
|
108
|
-
{
|
|
109
|
-
active: editorState.isCode,
|
|
110
|
-
icon: CodeXmlIcon,
|
|
111
|
-
key: 'code',
|
|
112
|
-
label: t('typobar.code'),
|
|
113
|
-
onClick: editorState.code,
|
|
114
|
-
tooltipProps: { hotkey: getHotkeyById(HotkeyEnum.CodeInline).keys },
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
icon: SquareDashedBottomCodeIcon,
|
|
118
|
-
key: 'codeblock',
|
|
119
|
-
label: t('typobar.codeblock'),
|
|
120
|
-
onClick: editorState.codeblock,
|
|
121
|
-
},
|
|
122
|
-
editorState.isCodeblock && {
|
|
123
|
-
children: (
|
|
124
|
-
<CodeLanguageSelect
|
|
125
|
-
onSelect={(value) => editorState.updateCodeblockLang(value)}
|
|
126
|
-
value={editorState.codeblockLang}
|
|
127
|
-
/>
|
|
128
|
-
),
|
|
129
|
-
disabled: !editorState.isCodeblock,
|
|
130
|
-
key: 'codeblockLang',
|
|
131
|
-
},
|
|
132
|
-
].filter(Boolean) as ChatInputActionsProps['items'],
|
|
133
|
-
[editorState],
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
return (
|
|
137
|
-
<ChatInputActionBar
|
|
138
|
-
left={<ChatInputActions items={items} />}
|
|
139
|
-
style={{
|
|
140
|
-
background: cssVar.colorFillQuaternary,
|
|
141
|
-
borderTopLeftRadius: 8,
|
|
142
|
-
borderTopRightRadius: 8,
|
|
143
|
-
}}
|
|
144
|
-
/>
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
TypoBar.displayName = 'TypoBar';
|
|
149
|
-
|
|
150
|
-
export default TypoBar;
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ReactCodePlugin,
|
|
3
|
-
ReactCodemirrorPlugin,
|
|
4
|
-
ReactHRPlugin,
|
|
5
|
-
ReactLinkHighlightPlugin,
|
|
6
|
-
ReactListPlugin,
|
|
7
|
-
ReactMathPlugin,
|
|
8
|
-
ReactTablePlugin,
|
|
9
|
-
} from '@lobehub/editor';
|
|
10
|
-
import { Editor, useEditor } from '@lobehub/editor/react';
|
|
11
|
-
import { Flexbox, Modal } from '@lobehub/ui';
|
|
12
|
-
import { memo, useMemo } from 'react';
|
|
13
|
-
import { useTranslation } from 'react-i18next';
|
|
14
|
-
|
|
15
|
-
import { useUserStore } from '@/store/user';
|
|
16
|
-
import { labPreferSelectors } from '@/store/user/slices/preference/selectors';
|
|
17
|
-
|
|
18
|
-
import TypoBar from './Typobar';
|
|
19
|
-
|
|
20
|
-
interface EditableMessageProps {
|
|
21
|
-
editing?: boolean;
|
|
22
|
-
onChange?: (value: string) => void;
|
|
23
|
-
onEditingChange?: (editing: boolean) => void;
|
|
24
|
-
value?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const EditableMessage = memo<EditableMessageProps>(
|
|
28
|
-
({ editing, onEditingChange, onChange, value }) => {
|
|
29
|
-
const { t } = useTranslation('common');
|
|
30
|
-
const editor = useEditor();
|
|
31
|
-
|
|
32
|
-
const enableRichRender = useUserStore(labPreferSelectors.enableInputMarkdown);
|
|
33
|
-
|
|
34
|
-
const richRenderProps = useMemo(
|
|
35
|
-
() =>
|
|
36
|
-
!enableRichRender
|
|
37
|
-
? {
|
|
38
|
-
enablePasteMarkdown: false,
|
|
39
|
-
markdownOption: {
|
|
40
|
-
bold: false,
|
|
41
|
-
code: false,
|
|
42
|
-
header: false,
|
|
43
|
-
italic: false,
|
|
44
|
-
quote: false,
|
|
45
|
-
strikethrough: false,
|
|
46
|
-
underline: false,
|
|
47
|
-
underlineStrikethrough: false,
|
|
48
|
-
},
|
|
49
|
-
}
|
|
50
|
-
: {
|
|
51
|
-
plugins: [
|
|
52
|
-
ReactListPlugin,
|
|
53
|
-
ReactCodePlugin,
|
|
54
|
-
ReactCodemirrorPlugin,
|
|
55
|
-
ReactHRPlugin,
|
|
56
|
-
ReactLinkHighlightPlugin,
|
|
57
|
-
ReactTablePlugin,
|
|
58
|
-
ReactMathPlugin,
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
[enableRichRender],
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<Modal
|
|
66
|
-
cancelText={t('cancel')}
|
|
67
|
-
closable={false}
|
|
68
|
-
destroyOnHidden
|
|
69
|
-
okText={t('ok')}
|
|
70
|
-
onCancel={() => onEditingChange?.(false)}
|
|
71
|
-
onOk={() => {
|
|
72
|
-
if (!editor) return;
|
|
73
|
-
const newValue = editor.getDocument('markdown') as unknown as string;
|
|
74
|
-
onChange?.(newValue);
|
|
75
|
-
onEditingChange?.(false);
|
|
76
|
-
}}
|
|
77
|
-
open={editing}
|
|
78
|
-
styles={{
|
|
79
|
-
body: {
|
|
80
|
-
overflow: 'hidden',
|
|
81
|
-
padding: 0,
|
|
82
|
-
},
|
|
83
|
-
}}
|
|
84
|
-
title={null}
|
|
85
|
-
width={'min(90vw, 960px)'}
|
|
86
|
-
>
|
|
87
|
-
<TypoBar editor={editor} />
|
|
88
|
-
<Flexbox
|
|
89
|
-
onClick={() => {
|
|
90
|
-
editor.focus();
|
|
91
|
-
}}
|
|
92
|
-
paddingBlock={16}
|
|
93
|
-
paddingInline={48}
|
|
94
|
-
style={{ cursor: 'text', maxHeight: '80vh', minHeight: '50vh', overflowY: 'auto' }}
|
|
95
|
-
>
|
|
96
|
-
<Editor
|
|
97
|
-
autoFocus
|
|
98
|
-
content={''}
|
|
99
|
-
editor={editor}
|
|
100
|
-
onInit={(editor) => {
|
|
101
|
-
if (!editor) return;
|
|
102
|
-
try {
|
|
103
|
-
editor?.setDocument('markdown', value);
|
|
104
|
-
} catch {}
|
|
105
|
-
}}
|
|
106
|
-
style={{
|
|
107
|
-
paddingBottom: 120,
|
|
108
|
-
}}
|
|
109
|
-
type={'text'}
|
|
110
|
-
variant={'chat'}
|
|
111
|
-
{...richRenderProps}
|
|
112
|
-
/>
|
|
113
|
-
</Flexbox>
|
|
114
|
-
</Modal>
|
|
115
|
-
);
|
|
116
|
-
},
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
export default EditableMessage;
|