@lobehub/lobehub 2.0.0-next.361 → 2.0.0-next.362

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/Dockerfile +2 -1
  3. package/changelog/v1.json +9 -0
  4. package/locales/en-US/chat.json +3 -1
  5. package/locales/zh-CN/chat.json +2 -0
  6. package/package.json +1 -1
  7. package/packages/context-engine/src/base/BaseEveryUserContentProvider.ts +204 -0
  8. package/packages/context-engine/src/base/BaseLastUserContentProvider.ts +1 -8
  9. package/packages/context-engine/src/base/__tests__/BaseEveryUserContentProvider.test.ts +354 -0
  10. package/packages/context-engine/src/base/constants.ts +20 -0
  11. package/packages/context-engine/src/engine/messages/MessagesEngine.ts +27 -23
  12. package/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +364 -0
  13. package/packages/context-engine/src/providers/PageEditorContextInjector.ts +17 -13
  14. package/packages/context-engine/src/providers/PageSelectionsInjector.ts +65 -0
  15. package/packages/context-engine/src/providers/__tests__/PageSelectionsInjector.test.ts +333 -0
  16. package/packages/context-engine/src/providers/index.ts +3 -1
  17. package/packages/prompts/src/agents/index.ts +1 -0
  18. package/packages/prompts/src/agents/pageSelectionContext.ts +28 -0
  19. package/packages/types/src/aiChat.ts +4 -0
  20. package/packages/types/src/message/common/index.ts +1 -0
  21. package/packages/types/src/message/common/metadata.ts +8 -0
  22. package/packages/types/src/message/common/pageSelection.ts +36 -0
  23. package/packages/types/src/message/ui/params.ts +16 -0
  24. package/scripts/prebuild.mts +1 -0
  25. package/src/features/ChatInput/Desktop/ContextContainer/ContextList.tsx +1 -1
  26. package/src/features/Conversation/ChatInput/index.tsx +9 -1
  27. package/src/features/Conversation/Messages/User/components/MessageContent.tsx +7 -1
  28. package/src/features/Conversation/Messages/User/components/PageSelections.tsx +62 -0
  29. package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +5 -1
  30. package/src/locales/default/chat.ts +3 -2
  31. package/src/server/routers/lambda/aiChat.ts +7 -0
  32. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +5 -19
@@ -0,0 +1,333 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { PipelineContext } from '../../types';
4
+ import { PageSelectionsInjector } from '../PageSelectionsInjector';
5
+
6
+ describe('PageSelectionsInjector', () => {
7
+ const createContext = (messages: any[] = []): PipelineContext => ({
8
+ initialState: {
9
+ messages: [],
10
+ model: 'test-model',
11
+ provider: 'test-provider',
12
+ },
13
+ isAborted: false,
14
+ messages,
15
+ metadata: {
16
+ maxTokens: 4000,
17
+ model: 'test-model',
18
+ },
19
+ });
20
+
21
+ const createPageSelection = (id: string, xmlContent: string, pageId = 'page-1') => ({
22
+ content: xmlContent, // preview content
23
+ id,
24
+ pageId,
25
+ xml: xmlContent, // actual content used by formatPageSelections
26
+ });
27
+
28
+ describe('enabled/disabled', () => {
29
+ it('should skip injection when disabled', async () => {
30
+ const injector = new PageSelectionsInjector({ enabled: false });
31
+
32
+ const context = createContext([
33
+ {
34
+ content: 'Question',
35
+ metadata: {
36
+ pageSelections: [createPageSelection('sel-1', 'Selected text')],
37
+ },
38
+ role: 'user',
39
+ },
40
+ ]);
41
+
42
+ const result = await injector.process(context);
43
+
44
+ expect(result.messages[0].content).toBe('Question');
45
+ expect(result.metadata.PageSelectionsInjectorInjectedCount).toBeUndefined();
46
+ });
47
+
48
+ it('should inject when enabled', async () => {
49
+ const injector = new PageSelectionsInjector({ enabled: true });
50
+
51
+ const context = createContext([
52
+ {
53
+ content: 'Question',
54
+ metadata: {
55
+ pageSelections: [createPageSelection('sel-1', 'Selected text')],
56
+ },
57
+ role: 'user',
58
+ },
59
+ ]);
60
+
61
+ const result = await injector.process(context);
62
+
63
+ expect(result.messages[0].content).toContain('Question');
64
+ expect(result.messages[0].content).toContain('<user_page_selections>');
65
+ expect(result.messages[0].content).toContain('Selected text');
66
+ });
67
+ });
68
+
69
+ describe('injection to every user message', () => {
70
+ it('should inject selections to each user message that has them', async () => {
71
+ const injector = new PageSelectionsInjector({ enabled: true });
72
+
73
+ const context = createContext([
74
+ {
75
+ content: 'First question',
76
+ metadata: {
77
+ pageSelections: [createPageSelection('sel-1', 'First selection')],
78
+ },
79
+ role: 'user',
80
+ },
81
+ { content: 'First answer', role: 'assistant' },
82
+ {
83
+ content: 'Second question',
84
+ metadata: {
85
+ pageSelections: [createPageSelection('sel-2', 'Second selection')],
86
+ },
87
+ role: 'user',
88
+ },
89
+ { content: 'Second answer', role: 'assistant' },
90
+ {
91
+ content: 'Third question without selection',
92
+ role: 'user',
93
+ },
94
+ ]);
95
+
96
+ const result = await injector.process(context);
97
+
98
+ // First user message should have first selection
99
+ expect(result.messages[0].content).toContain('First question');
100
+ expect(result.messages[0].content).toContain('First selection');
101
+ expect(result.messages[0].content).toContain('<user_page_selections>');
102
+
103
+ // Second user message should have second selection
104
+ expect(result.messages[2].content).toContain('Second question');
105
+ expect(result.messages[2].content).toContain('Second selection');
106
+ expect(result.messages[2].content).toContain('<user_page_selections>');
107
+
108
+ // Third user message should NOT have injection (no selections)
109
+ expect(result.messages[4].content).toBe('Third question without selection');
110
+
111
+ // Assistant messages should be unchanged
112
+ expect(result.messages[1].content).toBe('First answer');
113
+ expect(result.messages[3].content).toBe('Second answer');
114
+
115
+ // Metadata should show 2 injections
116
+ expect(result.metadata.PageSelectionsInjectorInjectedCount).toBe(2);
117
+ });
118
+
119
+ it('should skip user messages without pageSelections', async () => {
120
+ const injector = new PageSelectionsInjector({ enabled: true });
121
+
122
+ const context = createContext([
123
+ { content: 'No selections here', role: 'user' },
124
+ { content: 'Answer', role: 'assistant' },
125
+ {
126
+ content: 'With selections',
127
+ metadata: {
128
+ pageSelections: [createPageSelection('sel-1', 'Some text')],
129
+ },
130
+ role: 'user',
131
+ },
132
+ ]);
133
+
134
+ const result = await injector.process(context);
135
+
136
+ expect(result.messages[0].content).toBe('No selections here');
137
+ expect(result.messages[2].content).toContain('With selections');
138
+ expect(result.messages[2].content).toContain('Some text');
139
+ });
140
+
141
+ it('should skip user messages with empty pageSelections array', async () => {
142
+ const injector = new PageSelectionsInjector({ enabled: true });
143
+
144
+ const context = createContext([
145
+ {
146
+ content: 'Empty selections',
147
+ metadata: { pageSelections: [] },
148
+ role: 'user',
149
+ },
150
+ ]);
151
+
152
+ const result = await injector.process(context);
153
+
154
+ expect(result.messages[0].content).toBe('Empty selections');
155
+ });
156
+ });
157
+
158
+ describe('SYSTEM CONTEXT wrapper', () => {
159
+ it('should wrap selection content with SYSTEM CONTEXT markers', async () => {
160
+ const injector = new PageSelectionsInjector({ enabled: true });
161
+
162
+ const context = createContext([
163
+ {
164
+ content: 'Question',
165
+ metadata: {
166
+ pageSelections: [createPageSelection('sel-1', 'Selected text')],
167
+ },
168
+ role: 'user',
169
+ },
170
+ ]);
171
+
172
+ const result = await injector.process(context);
173
+ const content = result.messages[0].content as string;
174
+
175
+ expect(content).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
176
+ expect(content).toContain('<context.instruction>');
177
+ expect(content).toContain('<user_page_selections>');
178
+ expect(content).toContain('</user_page_selections>');
179
+ expect(content).toContain('<!-- END SYSTEM CONTEXT -->');
180
+ });
181
+
182
+ it('should have only one SYSTEM CONTEXT wrapper per message even with multiple selections', async () => {
183
+ const injector = new PageSelectionsInjector({ enabled: true });
184
+
185
+ const context = createContext([
186
+ {
187
+ content: 'Question',
188
+ metadata: {
189
+ pageSelections: [
190
+ createPageSelection('sel-1', 'First selection'),
191
+ createPageSelection('sel-2', 'Second selection'),
192
+ ],
193
+ },
194
+ role: 'user',
195
+ },
196
+ ]);
197
+
198
+ const result = await injector.process(context);
199
+ const content = result.messages[0].content as string;
200
+
201
+ const startCount = (content.match(/<!-- SYSTEM CONTEXT/g) || []).length;
202
+ const endCount = (content.match(/<!-- END SYSTEM CONTEXT/g) || []).length;
203
+
204
+ expect(startCount).toBe(1);
205
+ expect(endCount).toBe(1);
206
+ });
207
+
208
+ it('should create separate SYSTEM CONTEXT wrappers for each user message', async () => {
209
+ const injector = new PageSelectionsInjector({ enabled: true });
210
+
211
+ const context = createContext([
212
+ {
213
+ content: 'First question',
214
+ metadata: {
215
+ pageSelections: [createPageSelection('sel-1', 'First selection')],
216
+ },
217
+ role: 'user',
218
+ },
219
+ { content: 'Answer', role: 'assistant' },
220
+ {
221
+ content: 'Second question',
222
+ metadata: {
223
+ pageSelections: [createPageSelection('sel-2', 'Second selection')],
224
+ },
225
+ role: 'user',
226
+ },
227
+ ]);
228
+
229
+ const result = await injector.process(context);
230
+
231
+ // Each user message should have its own SYSTEM CONTEXT wrapper
232
+ const firstContent = result.messages[0].content as string;
233
+ const secondContent = result.messages[2].content as string;
234
+
235
+ expect(firstContent).toContain('<!-- SYSTEM CONTEXT');
236
+ expect(firstContent).toContain('First selection');
237
+
238
+ expect(secondContent).toContain('<!-- SYSTEM CONTEXT');
239
+ expect(secondContent).toContain('Second selection');
240
+ });
241
+ });
242
+
243
+ describe('multimodal messages', () => {
244
+ it('should handle array content with text parts', async () => {
245
+ const injector = new PageSelectionsInjector({ enabled: true });
246
+
247
+ const context = createContext([
248
+ {
249
+ content: [
250
+ { text: 'Question with image', type: 'text' },
251
+ { image_url: { url: 'http://example.com/img.png' }, type: 'image_url' },
252
+ ],
253
+ metadata: {
254
+ pageSelections: [createPageSelection('sel-1', 'Selected text')],
255
+ },
256
+ role: 'user',
257
+ },
258
+ ]);
259
+
260
+ const result = await injector.process(context);
261
+
262
+ expect(result.messages[0].content[0].text).toContain('Question with image');
263
+ expect(result.messages[0].content[0].text).toContain('Selected text');
264
+ expect(result.messages[0].content[0].text).toContain('<user_page_selections>');
265
+ expect(result.messages[0].content[1]).toEqual({
266
+ image_url: { url: 'http://example.com/img.png' },
267
+ type: 'image_url',
268
+ });
269
+ });
270
+ });
271
+
272
+ describe('integration with PageEditorContextInjector', () => {
273
+ it('should create wrapper that PageEditorContextInjector can reuse', async () => {
274
+ const injector = new PageSelectionsInjector({ enabled: true });
275
+
276
+ const context = createContext([
277
+ {
278
+ content: 'Question about the page',
279
+ metadata: {
280
+ pageSelections: [createPageSelection('sel-1', 'Selected paragraph')],
281
+ },
282
+ role: 'user',
283
+ },
284
+ ]);
285
+
286
+ const result = await injector.process(context);
287
+ const content = result.messages[0].content as string;
288
+
289
+ // Verify the wrapper structure is correct for reuse
290
+ expect(content).toContain('<!-- SYSTEM CONTEXT (NOT PART OF USER QUERY) -->');
291
+ expect(content).toContain('<context.instruction>');
292
+ expect(content).toContain('<!-- END SYSTEM CONTEXT -->');
293
+
294
+ // Verify the content is in the right position (between instruction and end marker)
295
+ const instructionIndex = content.indexOf('</context.instruction>');
296
+ const selectionsIndex = content.indexOf('<user_page_selections>');
297
+ const endIndex = content.indexOf('<!-- END SYSTEM CONTEXT -->');
298
+
299
+ expect(instructionIndex).toBeLessThan(selectionsIndex);
300
+ expect(selectionsIndex).toBeLessThan(endIndex);
301
+ });
302
+ });
303
+
304
+ describe('metadata', () => {
305
+ it('should set metadata when injections are made', async () => {
306
+ const injector = new PageSelectionsInjector({ enabled: true });
307
+
308
+ const context = createContext([
309
+ {
310
+ content: 'Question',
311
+ metadata: {
312
+ pageSelections: [createPageSelection('sel-1', 'Text')],
313
+ },
314
+ role: 'user',
315
+ },
316
+ ]);
317
+
318
+ const result = await injector.process(context);
319
+
320
+ expect(result.metadata.PageSelectionsInjectorInjectedCount).toBe(1);
321
+ });
322
+
323
+ it('should not set metadata when no injections are made', async () => {
324
+ const injector = new PageSelectionsInjector({ enabled: true });
325
+
326
+ const context = createContext([{ content: 'No selections', role: 'user' }]);
327
+
328
+ const result = await injector.process(context);
329
+
330
+ expect(result.metadata.PageSelectionsInjectorInjectedCount).toBeUndefined();
331
+ });
332
+ });
333
+ });
@@ -7,6 +7,7 @@ export { GTDTodoInjector } from './GTDTodoInjector';
7
7
  export { HistorySummaryProvider } from './HistorySummary';
8
8
  export { KnowledgeInjector } from './KnowledgeInjector';
9
9
  export { PageEditorContextInjector } from './PageEditorContextInjector';
10
+ export { PageSelectionsInjector } from './PageSelectionsInjector';
10
11
  export { SystemRoleInjector } from './SystemRoleInjector';
11
12
  export { ToolSystemRoleProvider } from './ToolSystemRole';
12
13
  export { UserMemoryInjector } from './UserMemoryInjector';
@@ -27,11 +28,12 @@ export type {
27
28
  GroupContextInjectorConfig,
28
29
  GroupMemberInfo as GroupContextMemberInfo,
29
30
  } from './GroupContextInjector';
30
- export type { HistorySummaryConfig } from './HistorySummary';
31
31
  export type { GTDPlan, GTDPlanInjectorConfig } from './GTDPlanInjector';
32
32
  export type { GTDTodoInjectorConfig, GTDTodoItem, GTDTodoList } from './GTDTodoInjector';
33
+ export type { HistorySummaryConfig } from './HistorySummary';
33
34
  export type { KnowledgeInjectorConfig } from './KnowledgeInjector';
34
35
  export type { PageEditorContextInjectorConfig } from './PageEditorContextInjector';
36
+ export type { PageSelectionsInjectorConfig } from './PageSelectionsInjector';
35
37
  export type { SystemRoleInjectorConfig } from './SystemRoleInjector';
36
38
  export type { ToolSystemRoleConfig } from './ToolSystemRole';
37
39
  export type { UserMemoryInjectorConfig } from './UserMemoryInjector';
@@ -1 +1,2 @@
1
1
  export * from './pageContentContext';
2
+ export * from './pageSelectionContext';
@@ -0,0 +1,28 @@
1
+ import type { PageSelection } from '@lobechat/types';
2
+
3
+ /**
4
+ * Format page selections into a system prompt context
5
+ * Each selection is wrapped in a <selection> tag with metadata
6
+ */
7
+ export const formatPageSelections = (selections: PageSelection[]): string => {
8
+ if (!selections || selections.length === 0) {
9
+ return '';
10
+ }
11
+
12
+ const formattedSelections = selections
13
+ .map((sel) => {
14
+ const lineInfo =
15
+ sel.startLine !== undefined
16
+ ? ` lines="${sel.startLine}-${sel.endLine ?? sel.startLine}"`
17
+ : '';
18
+
19
+ return `<selection ${lineInfo}>
20
+ ${sel.xml}
21
+ </selection>`;
22
+ })
23
+ .join('\n');
24
+
25
+ return `<user_selections count="${selections.length}">
26
+ ${formattedSelections}
27
+ </user_selections>`;
28
+ };
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
 
3
3
  import { UIChatMessage } from './message';
4
+ import { PageSelection, PageSelectionSchema } from './message/ui/params';
4
5
  import { OpenAIChatMessage } from './openai/chat';
5
6
  import { LobeUniformTool, LobeUniformToolSchema } from './tool';
6
7
  import { ChatTopic } from './topic';
@@ -10,6 +11,8 @@ export interface SendNewMessage {
10
11
  content: string;
11
12
  // if message has attached with files, then add files to message and the agent
12
13
  files?: string[];
14
+ /** Page selections attached to this message (for Ask AI functionality) */
15
+ pageSelections?: PageSelection[];
13
16
  parentId?: string;
14
17
  }
15
18
 
@@ -83,6 +86,7 @@ export const AiSendMessageServerSchema = z.object({
83
86
  newUserMessage: z.object({
84
87
  content: z.string(),
85
88
  files: z.array(z.string()).optional(),
89
+ pageSelections: z.array(PageSelectionSchema).optional(),
86
90
  parentId: z.string().optional(),
87
91
  }),
88
92
  sessionId: z.string().optional(),
@@ -2,5 +2,6 @@ export * from './base';
2
2
  export * from './image';
3
3
  export * from './messageGroup';
4
4
  export * from './metadata';
5
+ export * from './pageSelection';
5
6
  export * from './tools';
6
7
  export * from './translate';
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
2
2
  import { z } from 'zod';
3
3
 
4
+ import { PageSelection, PageSelectionSchema } from './pageSelection';
5
+
4
6
  export interface ModelTokensUsage {
5
7
  // Input tokens breakdown
6
8
  /**
@@ -80,6 +82,7 @@ export const MessageMetadataSchema = ModelUsageSchema.merge(ModelPerformanceSche
80
82
  inspectExpanded: z.boolean().optional(),
81
83
  isMultimodal: z.boolean().optional(),
82
84
  isSupervisor: z.boolean().optional(),
85
+ pageSelections: z.array(PageSelectionSchema).optional(),
83
86
  });
84
87
 
85
88
  export interface ModelUsage extends ModelTokensUsage {
@@ -147,4 +150,9 @@ export interface MessageMetadata extends ModelUsage, ModelPerformance {
147
150
  */
148
151
  instruction?: string;
149
152
  taskTitle?: string;
153
+ /**
154
+ * Page selections attached to user message
155
+ * Used for Ask AI functionality to persist selection context
156
+ */
157
+ pageSelections?: PageSelection[];
150
158
  }
@@ -0,0 +1,36 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * Page selection represents a user-selected text region in a page/document.
6
+ * Used for Ask AI functionality to persist selection context with user messages.
7
+ */
8
+ export interface PageSelection {
9
+ /** Selection unique identifier */
10
+ id: string;
11
+ anchor?: {
12
+ startNodeId: string;
13
+ endNodeId: string;
14
+ startOffset: number;
15
+ endOffset: number;
16
+ };
17
+ /** Selected content (plain text or markdown) */
18
+ content: string;
19
+ /** XML structure of the selected content (for positioning edits) */
20
+ xml?: string;
21
+ /** Page ID the selection belongs to */
22
+ pageId: string;
23
+ /** Start line number */
24
+ startLine?: number;
25
+ /** End line number */
26
+ endLine?: number;
27
+ }
28
+
29
+ export const PageSelectionSchema = z.object({
30
+ id: z.string(),
31
+ content: z.string(),
32
+ xml: z.string().optional(),
33
+ pageId: z.string(),
34
+ startLine: z.number().optional(),
35
+ endLine: z.number().optional(),
36
+ });
@@ -5,6 +5,8 @@ import { ConversationContext } from '../../conversation';
5
5
  import { UploadFileItem } from '../../files';
6
6
  import { MessageSemanticSearchChunk } from '../../rag';
7
7
  import { ChatMessageError, ChatMessageErrorSchema } from '../common/base';
8
+ // Import for local use
9
+ import type { PageSelection } from '../common/pageSelection';
8
10
  import { ChatPluginPayload, ToolInterventionSchema } from '../common/tools';
9
11
  import { UIChatMessage } from './chat';
10
12
  import { SemanticSearchChunkSchema } from './rag';
@@ -78,6 +80,10 @@ export interface ChatContextContent {
78
80
  */
79
81
  format?: 'xml' | 'text' | 'markdown';
80
82
  id: string;
83
+ /**
84
+ * Page ID the selection belongs to (for page editor selections)
85
+ */
86
+ pageId?: string;
81
87
  /**
82
88
  * Optional short preview for displaying in UI.
83
89
  */
@@ -86,6 +92,10 @@ export interface ChatContextContent {
86
92
  type: 'text';
87
93
  }
88
94
 
95
+ // Re-export PageSelection from common for backwards compatibility
96
+ export type { PageSelection } from '../common/pageSelection';
97
+ export { PageSelectionSchema } from '../common/pageSelection';
98
+
89
99
  export interface SendMessageParams {
90
100
  /**
91
101
  * create a thread
@@ -119,8 +129,14 @@ export interface SendMessageParams {
119
129
  parentId?: string;
120
130
  /**
121
131
  * Additional contextual snippets (e.g., text selections) attached to the request.
132
+ * @deprecated Use pageSelections instead for page editor selections
122
133
  */
123
134
  contexts?: ChatContextContent[];
135
+ /**
136
+ * Page selections attached to the message (for Ask AI functionality)
137
+ * These will be persisted to the database and injected via context-engine
138
+ */
139
+ pageSelections?: PageSelection[];
124
140
  }
125
141
 
126
142
  export interface SendGroupMessageParams {
@@ -51,6 +51,7 @@ const checkRequiredEnvVars = () => {
51
51
  console.error(` 📖 Documentation: ${docUrl}\n`);
52
52
  }
53
53
  console.error('Please configure these environment variables and redeploy.');
54
+ console.error('\n💡 TIP: If you previously used NEXT_AUTH_SECRET, simply rename it to AUTH_SECRET.');
54
55
  console.error('═'.repeat(70) + '\n');
55
56
  process.exit(1);
56
57
  }
@@ -30,7 +30,7 @@ const ContextList = memo(() => {
30
30
  const rawSelectionList = useFileStore(fileChatSelectors.chatContextSelections);
31
31
  const showSelectionList = useFileStore(fileChatSelectors.chatContextSelectionHasItem);
32
32
  const clearChatContextSelections = useFileStore((s) => s.clearChatContextSelections);
33
-
33
+ console.log(rawSelectionList);
34
34
  // Clear selections only when agentId changes (not on initial mount)
35
35
  useEffect(() => {
36
36
  if (prevAgentIdRef.current !== undefined && prevAgentIdRef.current !== agentId) {
@@ -110,8 +110,16 @@ const ChatInput = memo<ChatInputProps>(
110
110
  fileStore.clearChatUploadFileList();
111
111
  fileStore.clearChatContextSelections();
112
112
 
113
+ // Convert ChatContextContent to PageSelection for persistence
114
+ const pageSelections = currentContextList.map((ctx) => ({
115
+ content: ctx.preview || '',
116
+ id: ctx.id,
117
+ pageId: ctx.pageId || '',
118
+ xml: ctx.content,
119
+ }));
120
+
113
121
  // Fire and forget - send with captured message
114
- await sendMessage({ contexts: currentContextList, files: currentFileList, message });
122
+ await sendMessage({ files: currentFileList, message, pageSelections });
115
123
  },
116
124
  [isAIGenerating, sendMessage],
117
125
  );
@@ -7,13 +7,19 @@ import { type UIChatMessage } from '@/types/index';
7
7
  import { useMarkdown } from '../useMarkdown';
8
8
  import FileListViewer from './FileListViewer';
9
9
  import ImageFileListViewer from './ImageFileListViewer';
10
+ import PageSelections from './PageSelections';
10
11
  import VideoFileListViewer from './VideoFileListViewer';
11
12
 
12
13
  const UserMessageContent = memo<UIChatMessage>(
13
- ({ id, content, imageList, videoList, fileList }) => {
14
+ ({ id, content, imageList, videoList, fileList, metadata }) => {
14
15
  const markdownProps = useMarkdown(id);
16
+ const pageSelections = metadata?.pageSelections;
17
+
15
18
  return (
16
19
  <Flexbox gap={8} id={id}>
20
+ {pageSelections && pageSelections.length > 0 && (
21
+ <PageSelections selections={pageSelections} />
22
+ )}
17
23
  {content && <MarkdownMessage {...markdownProps}>{content}</MarkdownMessage>}
18
24
  {imageList && imageList?.length > 0 && <ImageFileListViewer items={imageList} />}
19
25
  {videoList && videoList?.length > 0 && <VideoFileListViewer items={videoList} />}
@@ -0,0 +1,62 @@
1
+ import { Flexbox } from '@lobehub/ui';
2
+ import { createStaticStyles } from 'antd-style';
3
+ import { memo } from 'react';
4
+
5
+ import type { PageSelection } from '@/types/index';
6
+
7
+ const styles = createStaticStyles(({ css, cssVar }) => ({
8
+ container: css`
9
+ cursor: pointer;
10
+ position: relative;
11
+ border-radius: 8px;
12
+
13
+ :hover {
14
+ background: ${cssVar.colorFillQuaternary};
15
+ }
16
+ `,
17
+ content: css`
18
+ overflow: hidden;
19
+ display: -webkit-box;
20
+ -webkit-box-orient: vertical;
21
+ -webkit-line-clamp: 3;
22
+
23
+ font-size: 12px;
24
+ line-height: 1.5;
25
+ color: ${cssVar.colorTextSecondary};
26
+ white-space: pre-wrap;
27
+ `,
28
+ quote: css`
29
+ inset-block-start: 2px;
30
+ inset-inline-start: 0;
31
+
32
+ font-family: Georgia, serif;
33
+ font-size: 28px;
34
+ line-height: 1;
35
+ color: ${cssVar.colorTextQuaternary};
36
+ `,
37
+ wrapper: css``,
38
+ }));
39
+
40
+ interface PageSelectionsProps {
41
+ selections: PageSelection[];
42
+ }
43
+
44
+ const PageSelections = memo<PageSelectionsProps>(({ selections }) => {
45
+ if (!selections || selections.length === 0) return null;
46
+
47
+ return (
48
+ <Flexbox gap={8}>
49
+ {selections.map((selection) => (
50
+ <Flexbox className={styles.container} key={selection.id}>
51
+ <Flexbox className={styles.wrapper} gap={4} horizontal padding={4}>
52
+ {/* eslint-disable-next-line react/no-unescaped-entities */}
53
+ <span className={styles.quote}>"</span>
54
+ <div className={styles.content}>{selection.content}</div>
55
+ </Flexbox>
56
+ </Flexbox>
57
+ ))}
58
+ </Flexbox>
59
+ );
60
+ });
61
+
62
+ export default PageSelections;
@@ -12,6 +12,8 @@ import { useTranslation } from 'react-i18next';
12
12
  import { useFileStore } from '@/store/file';
13
13
  import { useGlobalStore } from '@/store/global';
14
14
 
15
+ import { usePageEditorStore } from '../store';
16
+
15
17
  const styles = createStaticStyles(({ css }) => ({
16
18
  askCopilot: css`
17
19
  border-radius: 6px;
@@ -26,6 +28,7 @@ const styles = createStaticStyles(({ css }) => ({
26
28
  export const useAskCopilotItem = (editor: IEditor | undefined): ChatInputActionsProps['items'] => {
27
29
  const { t } = useTranslation('common');
28
30
  const addSelectionContext = useFileStore((s) => s.addChatContextSelection);
31
+ const pageId = usePageEditorStore((s) => s.documentId);
29
32
 
30
33
  return useMemo(() => {
31
34
  if (!editor) return [];
@@ -60,6 +63,7 @@ export const useAskCopilotItem = (editor: IEditor | undefined): ChatInputActions
60
63
  content,
61
64
  format,
62
65
  id: `selection-${nanoid(6)}`,
66
+ pageId,
63
67
  preview,
64
68
  title: 'Selection',
65
69
  type: 'text',
@@ -95,5 +99,5 @@ export const useAskCopilotItem = (editor: IEditor | undefined): ChatInputActions
95
99
  onClick: () => {},
96
100
  },
97
101
  ];
98
- }, [addSelectionContext, editor, t]);
102
+ }, [addSelectionContext, editor, pageId, t]);
99
103
  };