@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.
- package/CHANGELOG.md +25 -0
- package/Dockerfile +2 -1
- package/changelog/v1.json +9 -0
- package/locales/en-US/chat.json +3 -1
- package/locales/zh-CN/chat.json +2 -0
- package/package.json +1 -1
- package/packages/context-engine/src/base/BaseEveryUserContentProvider.ts +204 -0
- package/packages/context-engine/src/base/BaseLastUserContentProvider.ts +1 -8
- package/packages/context-engine/src/base/__tests__/BaseEveryUserContentProvider.test.ts +354 -0
- package/packages/context-engine/src/base/constants.ts +20 -0
- package/packages/context-engine/src/engine/messages/MessagesEngine.ts +27 -23
- package/packages/context-engine/src/engine/messages/__tests__/MessagesEngine.test.ts +364 -0
- package/packages/context-engine/src/providers/PageEditorContextInjector.ts +17 -13
- package/packages/context-engine/src/providers/PageSelectionsInjector.ts +65 -0
- package/packages/context-engine/src/providers/__tests__/PageSelectionsInjector.test.ts +333 -0
- package/packages/context-engine/src/providers/index.ts +3 -1
- package/packages/prompts/src/agents/index.ts +1 -0
- package/packages/prompts/src/agents/pageSelectionContext.ts +28 -0
- package/packages/types/src/aiChat.ts +4 -0
- package/packages/types/src/message/common/index.ts +1 -0
- package/packages/types/src/message/common/metadata.ts +8 -0
- package/packages/types/src/message/common/pageSelection.ts +36 -0
- package/packages/types/src/message/ui/params.ts +16 -0
- package/scripts/prebuild.mts +1 -0
- package/src/features/ChatInput/Desktop/ContextContainer/ContextList.tsx +1 -1
- package/src/features/Conversation/ChatInput/index.tsx +9 -1
- package/src/features/Conversation/Messages/User/components/MessageContent.tsx +7 -1
- package/src/features/Conversation/Messages/User/components/PageSelections.tsx +62 -0
- package/src/features/PageEditor/EditorCanvas/useAskCopilotItem.tsx +5 -1
- package/src/locales/default/chat.ts +3 -2
- package/src/server/routers/lambda/aiChat.ts +7 -0
- 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';
|
|
@@ -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(),
|
|
@@ -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 {
|
package/scripts/prebuild.mts
CHANGED
|
@@ -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({
|
|
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
|
};
|