@lobehub/lobehub 2.0.0-next.217 → 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.
@@ -1,14 +1,13 @@
1
1
  import { type CodeInterpreterToolName, MarketSDK } from '@lobehub/market-sdk';
2
2
  import { TRPCError } from '@trpc/server';
3
3
  import debug from 'debug';
4
+ import { sha256 } from 'js-sha256';
4
5
  import { z } from 'zod';
5
6
 
6
- import { DocumentModel } from '@/database/models/document';
7
- import { FileModel } from '@/database/models/file';
8
7
  import { type ToolCallContent } from '@/libs/mcp';
9
8
  import { authedProcedure, router } from '@/libs/trpc/lambda';
10
9
  import { marketUserInfo, serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
11
- import { generateTrustedClientToken } from '@/libs/trusted-client';
10
+ import { generateTrustedClientToken, isTrustedClientEnabled } from '@/libs/trusted-client';
12
11
  import { FileS3 } from '@/server/modules/S3';
13
12
  import { DiscoverService } from '@/server/services/discover';
14
13
  import { FileService } from '@/server/services/file';
@@ -69,21 +68,13 @@ const callCodeInterpreterToolSchema = z.object({
69
68
  userId: z.string(),
70
69
  });
71
70
 
72
- // Schema for getting export file upload URL
73
- const getExportFileUploadUrlSchema = z.object({
71
+ // Schema for export and upload file (combined operation)
72
+ const exportAndUploadFileSchema = z.object({
74
73
  filename: z.string(),
74
+ path: z.string(),
75
75
  topicId: z.string(),
76
76
  });
77
77
 
78
- // Schema for saving exported file content to document
79
- const saveExportedFileContentSchema = z.object({
80
- content: z.string(),
81
- fileId: z.string(),
82
- fileType: z.string(),
83
- filename: z.string(),
84
- url: z.string(),
85
- });
86
-
87
78
  // Schema for cloud MCP endpoint call
88
79
  const callCloudMcpEndpointSchema = z.object({
89
80
  apiParams: z.record(z.any()),
@@ -94,8 +85,7 @@ const callCloudMcpEndpointSchema = z.object({
94
85
 
95
86
  // ============================== Type Exports ==============================
96
87
  export type CallCodeInterpreterToolInput = z.infer<typeof callCodeInterpreterToolSchema>;
97
- export type GetExportFileUploadUrlInput = z.infer<typeof getExportFileUploadUrlSchema>;
98
- export type SaveExportedFileContentInput = z.infer<typeof saveExportedFileContentSchema>;
88
+ export type ExportAndUploadFileInput = z.infer<typeof exportAndUploadFileSchema>;
99
89
 
100
90
  export interface CallToolResult {
101
91
  error?: {
@@ -107,22 +97,16 @@ export interface CallToolResult {
107
97
  success: boolean;
108
98
  }
109
99
 
110
- export interface GetExportFileUploadUrlResult {
111
- downloadUrl: string;
112
- error?: {
113
- message: string;
114
- };
115
- key: string;
116
- success: boolean;
117
- uploadUrl: string;
118
- }
119
-
120
- export interface SaveExportedFileContentResult {
121
- documentId?: string;
100
+ export interface ExportAndUploadFileResult {
122
101
  error?: {
123
102
  message: string;
124
103
  };
104
+ fileId?: string;
105
+ filename: string;
106
+ mimeType?: string;
107
+ size?: number;
125
108
  success: boolean;
109
+ url?: string;
126
110
  }
127
111
 
128
112
  // ============================== Router ==============================
@@ -140,17 +124,26 @@ export const marketRouter = router({
140
124
  let result: { content: string; state: any; success: boolean } | undefined;
141
125
 
142
126
  try {
143
- // Query user_settings to get market.accessToken
144
- const userState = await ctx.userModel.getUserState(async () => ({}));
145
- const userAccessToken = userState.settings?.market?.accessToken;
146
-
147
- log('callCloudMcpEndpoint: userAccessToken exists=%s', !!userAccessToken);
148
-
149
- if (!userAccessToken) {
150
- throw new TRPCError({
151
- code: 'UNAUTHORIZED',
152
- message: 'User access token not found. Please sign in to Market first.',
153
- });
127
+ // Check if trusted client is enabled - if so, we don't need user's accessToken
128
+ const trustedClientEnabled = isTrustedClientEnabled();
129
+
130
+ let userAccessToken: string | undefined;
131
+
132
+ if (!trustedClientEnabled) {
133
+ // Query user_settings to get market.accessToken only if trusted client is not enabled
134
+ const userState = await ctx.userModel.getUserState(async () => ({}));
135
+ userAccessToken = userState.settings?.market?.accessToken;
136
+
137
+ log('callCloudMcpEndpoint: userAccessToken exists=%s', !!userAccessToken);
138
+
139
+ if (!userAccessToken) {
140
+ throw new TRPCError({
141
+ code: 'UNAUTHORIZED',
142
+ message: 'User access token not found. Please sign in to Market first.',
143
+ });
144
+ }
145
+ } else {
146
+ log('callCloudMcpEndpoint: using trusted client authentication');
154
147
  }
155
148
 
156
149
  const cloudResult = await ctx.discoverService.callCloudMcpEndpoint({
@@ -277,99 +270,122 @@ export const marketRouter = router({
277
270
  }),
278
271
 
279
272
  /**
280
- * Generate a pre-signed upload URL for exporting files from sandbox
273
+ * Export a file from sandbox and upload to S3, then create a persistent file record
274
+ * This combines the previous getExportFileUploadUrl + callCodeInterpreterTool + createFileRecord flow
275
+ * Returns a permanent /f/:id URL instead of a temporary pre-signed URL
281
276
  */
282
- getExportFileUploadUrl: marketToolProcedure
283
- .input(getExportFileUploadUrlSchema)
284
- .mutation(async ({ input }) => {
285
- const { filename, topicId } = input;
277
+ exportAndUploadFile: marketToolProcedure
278
+ .input(exportAndUploadFileSchema)
279
+ .mutation(async ({ input, ctx }) => {
280
+ const { path, filename, topicId } = input;
286
281
 
287
- log('Generating export file upload URL for: %s in topic: %s', filename, topicId);
282
+ log('Exporting and uploading file: %s from path: %s in topic: %s', filename, path, topicId);
288
283
 
289
284
  try {
290
285
  const s3 = new FileS3();
291
286
 
287
+ // Use date-based sharding for privacy compliance (GDPR, CCPA)
288
+ const today = new Date().toISOString().split('T')[0];
289
+
292
290
  // Generate a unique key for the exported file
293
- const key = `code-interpreter-exports/${topicId}/${filename}`;
291
+ const key = `code-interpreter-exports/${today}/${topicId}/${filename}`;
294
292
 
295
- // Generate pre-signed upload URL
293
+ // Step 1: Generate pre-signed upload URL
296
294
  const uploadUrl = await s3.createPreSignedUrl(key);
297
-
298
- // Generate download URL (pre-signed for preview)
299
- const downloadUrl = await s3.createPreSignedUrlForPreview(key);
300
-
301
295
  log('Generated upload URL for key: %s', key);
302
296
 
303
- return {
304
- downloadUrl,
305
- key,
306
- success: true,
307
- uploadUrl,
308
- } as GetExportFileUploadUrlResult;
309
- } catch (error) {
310
- log('Error generating export file upload URL: %O', error);
297
+ // Step 2: Generate trusted client token if user info is available
298
+ const trustedClientToken = ctx.marketUserInfo
299
+ ? generateTrustedClientToken(ctx.marketUserInfo)
300
+ : undefined;
301
+
302
+ // Only require user accessToken if trusted client is not available
303
+ let userAccessToken: string | undefined;
304
+ if (!trustedClientToken) {
305
+ const userState = await ctx.userModel.getUserState(async () => ({}));
306
+ userAccessToken = userState.settings?.market?.accessToken;
307
+
308
+ if (!userAccessToken) {
309
+ return {
310
+ error: { message: 'User access token not found. Please sign in to Market first.' },
311
+ filename,
312
+ success: false,
313
+ } as ExportAndUploadFileResult;
314
+ }
315
+ } else {
316
+ log('Using trusted client authentication for exportAndUploadFile');
317
+ }
311
318
 
312
- return {
313
- downloadUrl: '',
314
- error: {
315
- message: (error as Error).message,
316
- },
317
- key: '',
318
- success: false,
319
- uploadUrl: '',
320
- } as GetExportFileUploadUrlResult;
321
- }
322
- }),
319
+ // Initialize MarketSDK
320
+ const market = new MarketSDK({
321
+ accessToken: userAccessToken,
322
+ baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
323
+ trustedClientToken,
324
+ });
323
325
 
324
- /**
325
- * Save exported file content to documents table
326
- */
327
- saveExportedFileContent: marketToolProcedure
328
- .input(saveExportedFileContentSchema)
329
- .mutation(async ({ ctx, input }) => {
330
- const { content, fileId, fileType, filename, url } = input;
326
+ // Step 3: Call sandbox's exportFile tool with the upload URL
327
+ const response = await market.plugins.runBuildInTool(
328
+ 'exportFile',
329
+ { path, uploadUrl },
330
+ { topicId, userId: ctx.userId },
331
+ );
331
332
 
332
- log('Saving exported file content: fileId=%s, filename=%s', fileId, filename);
333
+ log('Sandbox exportFile response: %O', response);
333
334
 
334
- try {
335
- const documentModel = new DocumentModel(ctx.serverDB, ctx.userId);
336
- const fileModel = new FileModel(ctx.serverDB, ctx.userId);
335
+ if (!response.success) {
336
+ return {
337
+ error: { message: response.error?.message || 'Failed to export file from sandbox' },
338
+ filename,
339
+ success: false,
340
+ } as ExportAndUploadFileResult;
341
+ }
342
+
343
+ const result = response.data?.result;
344
+ const uploadSuccess = result?.success !== false;
337
345
 
338
- // Verify the file exists
339
- const file = await fileModel.findById(fileId);
340
- if (!file) {
346
+ if (!uploadSuccess) {
341
347
  return {
342
- error: { message: 'File not found' },
348
+ error: { message: result?.error || 'Failed to upload file from sandbox' },
349
+ filename,
343
350
  success: false,
344
- } as SaveExportedFileContentResult;
351
+ } as ExportAndUploadFileResult;
345
352
  }
346
353
 
347
- // Create document record with the file content
348
- const document = await documentModel.create({
349
- content,
350
- fileId,
351
- fileType,
352
- filename,
353
- source: url,
354
- sourceType: 'file',
355
- title: filename,
356
- totalCharCount: content.length,
357
- totalLineCount: content.split('\n').length,
354
+ // Step 4: Get file metadata from S3 to verify upload and get actual size
355
+ const metadata = await s3.getFileMetadata(key);
356
+ const fileSize = metadata.contentLength;
357
+ const mimeType = metadata.contentType || result?.mimeType || 'application/octet-stream';
358
+
359
+ // Step 5: Create persistent file record using FileService
360
+ // Generate a simple hash from the key (since we don't have the actual file content)
361
+ const fileHash = sha256(key + Date.now().toString());
362
+
363
+ const { fileId, url } = await ctx.fileService.createFileRecord({
364
+ fileHash,
365
+ fileType: mimeType,
366
+ name: filename,
367
+ size: fileSize,
368
+ url: key, // Store S3 key
358
369
  });
359
370
 
360
- log('Created document for exported file: documentId=%s, fileId=%s', document.id, fileId);
371
+ log('Created file record: fileId=%s, url=%s', fileId, url);
361
372
 
362
373
  return {
363
- documentId: document.id,
374
+ fileId,
375
+ filename,
376
+ mimeType,
377
+ size: fileSize,
364
378
  success: true,
365
- } as SaveExportedFileContentResult;
379
+ url, // This is the permanent /f/:id URL
380
+ } as ExportAndUploadFileResult;
366
381
  } catch (error) {
367
- log('Error saving exported file content: %O', error);
382
+ log('Error in exportAndUploadFile: %O', error);
368
383
 
369
384
  return {
370
385
  error: { message: (error as Error).message },
386
+ filename,
371
387
  success: false,
372
- } as SaveExportedFileContentResult;
388
+ } as ExportAndUploadFileResult;
373
389
  }
374
390
  }),
375
391
  });
@@ -149,7 +149,7 @@ export class DiscoverService {
149
149
  apiParams: Record<string, any>;
150
150
  identifier: string;
151
151
  toolName: string;
152
- userAccessToken: string;
152
+ userAccessToken?: string;
153
153
  }) {
154
154
  log('callCloudMcpEndpoint: params=%O', {
155
155
  apiParams: params.apiParams,
@@ -159,7 +159,14 @@ export class DiscoverService {
159
159
  });
160
160
 
161
161
  try {
162
- // Call cloud gateway with user access token in Authorization header
162
+ // Build headers - only include Authorization if userAccessToken is provided
163
+ // When userAccessToken is not provided, MarketSDK will use trustedClientToken for authentication
164
+ const headers: Record<string, string> = {};
165
+ if (params.userAccessToken) {
166
+ headers.Authorization = `Bearer ${params.userAccessToken}`;
167
+ }
168
+
169
+ // Call cloud gateway with optional user access token in Authorization header
163
170
  const result = await this.market.plugins.callCloudGateway(
164
171
  {
165
172
  apiParams: params.apiParams,
@@ -167,9 +174,7 @@ export class DiscoverService {
167
174
  toolName: params.toolName,
168
175
  },
169
176
  {
170
- headers: {
171
- Authorization: `Bearer ${params.userAccessToken}`,
172
- },
177
+ headers,
173
178
  },
174
179
  );
175
180
 
@@ -2,10 +2,8 @@ import { toolsClient } from '@/libs/trpc/client';
2
2
  import type {
3
3
  CallCodeInterpreterToolInput,
4
4
  CallToolResult,
5
- GetExportFileUploadUrlInput,
6
- GetExportFileUploadUrlResult,
7
- SaveExportedFileContentInput,
8
- SaveExportedFileContentResult,
5
+ ExportAndUploadFileInput,
6
+ ExportAndUploadFileResult,
9
7
  } from '@/server/routers/tools/market';
10
8
  import { useUserStore } from '@/store/user';
11
9
  import { settingsSelectors } from '@/store/user/slices/settings/selectors/settings';
@@ -44,31 +42,25 @@ class CodeInterpreterService {
44
42
  }
45
43
 
46
44
  /**
47
- * Get a pre-signed upload URL for exporting a file from the sandbox
45
+ * Export a file from sandbox and upload to S3, then create a persistent file record
46
+ * This is a single call that combines: getUploadUrl + callTool(exportFile) + createFileRecord
47
+ * Returns a permanent /f/:id URL instead of a temporary pre-signed URL
48
+ * @param path - The file path in the sandbox
48
49
  * @param filename - The name of the file to export
49
50
  * @param topicId - The topic ID for organizing files
50
51
  */
51
- async getExportFileUploadUrl(
52
+ async exportAndUploadFile(
53
+ path: string,
52
54
  filename: string,
53
55
  topicId: string,
54
- ): Promise<GetExportFileUploadUrlResult> {
55
- const input: GetExportFileUploadUrlInput = {
56
+ ): Promise<ExportAndUploadFileResult> {
57
+ const input: ExportAndUploadFileInput = {
56
58
  filename,
59
+ path,
57
60
  topicId,
58
61
  };
59
62
 
60
- return toolsClient.market.getExportFileUploadUrl.mutate(input);
61
- }
62
-
63
- /**
64
- * Save exported file content to documents table
65
- * This creates a document record linked to the file for content retrieval
66
- * @param params - File content and metadata
67
- */
68
- async saveExportedFileContent(
69
- params: SaveExportedFileContentInput,
70
- ): Promise<SaveExportedFileContentResult> {
71
- return toolsClient.market.saveExportedFileContent.mutate(params);
63
+ return toolsClient.market.exportAndUploadFile.mutate(input);
72
64
  }
73
65
  }
74
66
 
@@ -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: 'current-user', // TODO: Get actual userId from auth context
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: save exported file and associate with assistant message (parent)
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
- if (exportState.downloadUrl && exportState.filename) {
307
+ // Server now creates the file record and returns fileId in the response
308
+ if (exportState.fileId && exportState.filename) {
348
309
  try {
349
- // Generate a hash from the URL path (without query params) for deduplication
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, [fileResult.id], {
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] Saved exported file: targetMessageId=%s, fileId=%s, filename=%s',
320
+ '[invokeCloudCodeInterpreterTool] Associated exported file with message: targetMessageId=%s, fileId=%s, filename=%s',
394
321
  targetMessageId,
395
- fileResult.id,
322
+ exportState.fileId,
396
323
  exportState.filename,
397
324
  );
398
325
  } catch (error) {
@@ -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;