@lobehub/lobehub 2.0.0-next.302 → 2.0.0-next.303
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/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/src/features/Conversation/Messages/Assistant/index.tsx +1 -1
- package/src/features/Conversation/Messages/AssistantGroup/components/MessageContent.tsx +3 -3
- package/src/features/Conversation/Messages/Supervisor/components/MessageContent.tsx +2 -2
- package/src/features/Conversation/Messages/components/ContentLoading.tsx +5 -3
- package/src/store/chat/slices/operation/__tests__/selectors.test.ts +165 -0
- package/src/store/chat/slices/operation/selectors.ts +23 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
## [Version 2.0.0-next.303](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.302...v2.0.0-next.303)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-01-18**</sup>
|
|
8
|
+
|
|
9
|
+
#### 💄 Styles
|
|
10
|
+
|
|
11
|
+
- **misc**: Improve operation hint and fix scroll issue.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### Styles
|
|
19
|
+
|
|
20
|
+
- **misc**: Improve operation hint and fix scroll issue, closes [#11573](https://github.com/lobehub/lobe-chat/issues/11573) ([8505d14](https://github.com/lobehub/lobe-chat/commit/8505d14))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
5
30
|
## [Version 2.0.0-next.302](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.301...v2.0.0-next.302)
|
|
6
31
|
|
|
7
32
|
<sup>Released on **2026-01-17**</sup>
|
package/changelog/v1.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lobehub/lobehub",
|
|
3
|
-
"version": "2.0.0-next.
|
|
3
|
+
"version": "2.0.0-next.303",
|
|
4
4
|
"description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"framework",
|
|
@@ -58,7 +58,7 @@ const AssistantMessage = memo<AssistantMessageProps>(
|
|
|
58
58
|
const editing = useConversationStore(messageStateSelectors.isMessageEditing(id));
|
|
59
59
|
const generating = useConversationStore(messageStateSelectors.isMessageGenerating(id));
|
|
60
60
|
const creating = useConversationStore(messageStateSelectors.isMessageCreating(id));
|
|
61
|
-
const newScreen = useNewScreen({ creating, isLatestItem });
|
|
61
|
+
const newScreen = useNewScreen({ creating: creating || generating, isLatestItem });
|
|
62
62
|
|
|
63
63
|
const errorContent = useErrorContent(error);
|
|
64
64
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createStaticStyles, cx } from 'antd-style';
|
|
2
2
|
import { memo } from 'react';
|
|
3
3
|
|
|
4
|
-
import BubblesLoading from '@/components/BubblesLoading';
|
|
5
4
|
import { LOADING_FLAT } from '@/const/message';
|
|
6
5
|
import MarkdownMessage from '@/features/Conversation/Markdown';
|
|
6
|
+
import ContentLoading from '@/features/Conversation/Messages/components/ContentLoading';
|
|
7
7
|
|
|
8
8
|
import { normalizeThinkTags, processWithArtifact } from '../../../utils/markdown';
|
|
9
9
|
import { useMarkdown } from '../useMarkdown';
|
|
@@ -25,12 +25,12 @@ const MessageContent = memo<ContentBlockProps>(({ content, hasTools, id }) => {
|
|
|
25
25
|
const message = normalizeThinkTags(processWithArtifact(content));
|
|
26
26
|
const markdownProps = useMarkdown(id);
|
|
27
27
|
|
|
28
|
-
if (!content && !hasTools) return <
|
|
28
|
+
if (!content && !hasTools) return <ContentLoading id={id} />;
|
|
29
29
|
|
|
30
30
|
if (content === LOADING_FLAT) {
|
|
31
31
|
if (hasTools) return null;
|
|
32
32
|
|
|
33
|
-
return <
|
|
33
|
+
return <ContentLoading id={id} />;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
return (
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { memo } from 'react';
|
|
2
2
|
|
|
3
|
-
import BubblesLoading from '@/components/BubblesLoading';
|
|
4
3
|
import { LOADING_FLAT } from '@/const/message';
|
|
5
4
|
import MarkdownMessage from '@/features/Conversation/Markdown';
|
|
6
5
|
|
|
7
6
|
import { normalizeThinkTags, processWithArtifact } from '../../../utils/markdown';
|
|
8
7
|
import { useMarkdown } from '../../AssistantGroup/useMarkdown';
|
|
8
|
+
import ContentLoading from '../../components/ContentLoading';
|
|
9
9
|
|
|
10
10
|
interface ContentBlockProps {
|
|
11
11
|
content: string;
|
|
@@ -20,7 +20,7 @@ const MessageContent = memo<ContentBlockProps>(({ content, id, hasTools }) => {
|
|
|
20
20
|
if (!content || content === LOADING_FLAT) {
|
|
21
21
|
if (hasTools) return null;
|
|
22
22
|
|
|
23
|
-
return <
|
|
23
|
+
return <ContentLoading id={id} />;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
return content && <MarkdownMessage {...markdownProps}>{message}</MarkdownMessage>;
|
|
@@ -9,17 +9,17 @@ import type { OperationType } from '@/store/chat/slices/operation/types';
|
|
|
9
9
|
|
|
10
10
|
const ELAPSED_TIME_THRESHOLD = 2100; // Show elapsed time after 2 seconds
|
|
11
11
|
|
|
12
|
+
const NO_NEED_SHOW_DOT_OP_TYPES = new Set<OperationType>(['reasoning']);
|
|
13
|
+
|
|
12
14
|
interface ContentLoadingProps {
|
|
13
15
|
id: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
|
|
17
19
|
const { t } = useTranslation('chat');
|
|
18
|
-
const
|
|
20
|
+
const runningOp = useChatStore(operationSelectors.getDeepestRunningOperationByMessage(id));
|
|
19
21
|
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
|
20
22
|
|
|
21
|
-
// Get the running operation
|
|
22
|
-
const runningOp = operations.find((op) => op.status === 'running');
|
|
23
23
|
const operationType = runningOp?.type as OperationType | undefined;
|
|
24
24
|
const startTime = runningOp?.metadata?.startTime;
|
|
25
25
|
|
|
@@ -48,6 +48,8 @@ const ContentLoading = memo<ContentLoadingProps>(({ id }) => {
|
|
|
48
48
|
|
|
49
49
|
const showElapsedTime = elapsedSeconds >= ELAPSED_TIME_THRESHOLD / 1000;
|
|
50
50
|
|
|
51
|
+
if (operationType && NO_NEED_SHOW_DOT_OP_TYPES.has(operationType)) return null;
|
|
52
|
+
|
|
51
53
|
return (
|
|
52
54
|
<Flexbox align={'center'} horizontal>
|
|
53
55
|
<BubblesLoading />
|
|
@@ -188,6 +188,171 @@ describe('Operation Selectors', () => {
|
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
describe('getDeepestRunningOperationByMessage', () => {
|
|
192
|
+
it('should return undefined when no operations exist', () => {
|
|
193
|
+
const { result } = renderHook(() => useChatStore());
|
|
194
|
+
|
|
195
|
+
const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
|
|
196
|
+
result.current,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
expect(deepestOp).toBeUndefined();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should return undefined when no running operations exist', () => {
|
|
203
|
+
const { result } = renderHook(() => useChatStore());
|
|
204
|
+
|
|
205
|
+
let opId: string;
|
|
206
|
+
act(() => {
|
|
207
|
+
opId = result.current.startOperation({
|
|
208
|
+
type: 'execAgentRuntime',
|
|
209
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
210
|
+
}).operationId;
|
|
211
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
212
|
+
result.current.completeOperation(opId);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
|
|
216
|
+
result.current,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
expect(deepestOp).toBeUndefined();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should return the only running operation when there is one', () => {
|
|
223
|
+
const { result } = renderHook(() => useChatStore());
|
|
224
|
+
|
|
225
|
+
let opId: string;
|
|
226
|
+
act(() => {
|
|
227
|
+
opId = result.current.startOperation({
|
|
228
|
+
type: 'execAgentRuntime',
|
|
229
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
230
|
+
}).operationId;
|
|
231
|
+
result.current.associateMessageWithOperation('msg1', opId);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
|
|
235
|
+
result.current,
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(deepestOp).toBeDefined();
|
|
239
|
+
expect(deepestOp?.type).toBe('execAgentRuntime');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should return the leaf operation in a parent-child tree', () => {
|
|
243
|
+
const { result } = renderHook(() => useChatStore());
|
|
244
|
+
|
|
245
|
+
let parentOpId: string;
|
|
246
|
+
let childOpId: string;
|
|
247
|
+
|
|
248
|
+
act(() => {
|
|
249
|
+
// Start parent operation
|
|
250
|
+
parentOpId = result.current.startOperation({
|
|
251
|
+
type: 'execAgentRuntime',
|
|
252
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
253
|
+
}).operationId;
|
|
254
|
+
result.current.associateMessageWithOperation('msg1', parentOpId);
|
|
255
|
+
|
|
256
|
+
// Start child operation
|
|
257
|
+
childOpId = result.current.startOperation({
|
|
258
|
+
type: 'reasoning',
|
|
259
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
260
|
+
parentOperationId: parentOpId,
|
|
261
|
+
}).operationId;
|
|
262
|
+
result.current.associateMessageWithOperation('msg1', childOpId);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
|
|
266
|
+
result.current,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Should return the child (reasoning) not the parent (execAgentRuntime)
|
|
270
|
+
expect(deepestOp).toBeDefined();
|
|
271
|
+
expect(deepestOp?.type).toBe('reasoning');
|
|
272
|
+
expect(deepestOp?.id).toBe(childOpId!);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should return the deepest leaf in a multi-level tree', () => {
|
|
276
|
+
const { result } = renderHook(() => useChatStore());
|
|
277
|
+
|
|
278
|
+
let rootOpId: string;
|
|
279
|
+
let level1OpId: string;
|
|
280
|
+
let level2OpId: string;
|
|
281
|
+
|
|
282
|
+
act(() => {
|
|
283
|
+
// Level 0: root operation
|
|
284
|
+
rootOpId = result.current.startOperation({
|
|
285
|
+
type: 'execAgentRuntime',
|
|
286
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
287
|
+
}).operationId;
|
|
288
|
+
result.current.associateMessageWithOperation('msg1', rootOpId);
|
|
289
|
+
|
|
290
|
+
// Level 1: child of root
|
|
291
|
+
level1OpId = result.current.startOperation({
|
|
292
|
+
type: 'callLLM',
|
|
293
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
294
|
+
parentOperationId: rootOpId,
|
|
295
|
+
}).operationId;
|
|
296
|
+
result.current.associateMessageWithOperation('msg1', level1OpId);
|
|
297
|
+
|
|
298
|
+
// Level 2: grandchild (deepest)
|
|
299
|
+
level2OpId = result.current.startOperation({
|
|
300
|
+
type: 'reasoning',
|
|
301
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
302
|
+
parentOperationId: level1OpId,
|
|
303
|
+
}).operationId;
|
|
304
|
+
result.current.associateMessageWithOperation('msg1', level2OpId);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
|
|
308
|
+
result.current,
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
// Should return the deepest leaf (reasoning at level 2)
|
|
312
|
+
expect(deepestOp).toBeDefined();
|
|
313
|
+
expect(deepestOp?.type).toBe('reasoning');
|
|
314
|
+
expect(deepestOp?.id).toBe(level2OpId!);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should return parent when child operation completes', () => {
|
|
318
|
+
const { result } = renderHook(() => useChatStore());
|
|
319
|
+
|
|
320
|
+
let parentOpId: string;
|
|
321
|
+
let childOpId: string;
|
|
322
|
+
|
|
323
|
+
act(() => {
|
|
324
|
+
parentOpId = result.current.startOperation({
|
|
325
|
+
type: 'execAgentRuntime',
|
|
326
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
327
|
+
}).operationId;
|
|
328
|
+
result.current.associateMessageWithOperation('msg1', parentOpId);
|
|
329
|
+
|
|
330
|
+
childOpId = result.current.startOperation({
|
|
331
|
+
type: 'reasoning',
|
|
332
|
+
context: { agentId: 'session1', messageId: 'msg1' },
|
|
333
|
+
parentOperationId: parentOpId,
|
|
334
|
+
}).operationId;
|
|
335
|
+
result.current.associateMessageWithOperation('msg1', childOpId);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// Before completing child
|
|
339
|
+
let deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(
|
|
340
|
+
result.current,
|
|
341
|
+
);
|
|
342
|
+
expect(deepestOp?.type).toBe('reasoning');
|
|
343
|
+
|
|
344
|
+
// Complete child operation
|
|
345
|
+
act(() => {
|
|
346
|
+
result.current.completeOperation(childOpId);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// After completing child, parent should be the deepest running
|
|
350
|
+
deepestOp = operationSelectors.getDeepestRunningOperationByMessage('msg1')(result.current);
|
|
351
|
+
expect(deepestOp?.type).toBe('execAgentRuntime');
|
|
352
|
+
expect(deepestOp?.id).toBe(parentOpId!);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
191
356
|
describe('isMessageProcessing', () => {
|
|
192
357
|
it('should return true if message has running operations', () => {
|
|
193
358
|
const { result } = renderHook(() => useChatStore());
|
|
@@ -355,6 +355,28 @@ const isAnyMessageLoading =
|
|
|
355
355
|
return messageIds.some((id) => isMessageProcessing(id)(s));
|
|
356
356
|
};
|
|
357
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Get the deepest running operation for a message (leaf node in operation tree)
|
|
360
|
+
* Operations form a tree structure via parentOperationId/childOperationIds
|
|
361
|
+
* This returns the most specific (deepest) running operation for UI display
|
|
362
|
+
*/
|
|
363
|
+
const getDeepestRunningOperationByMessage =
|
|
364
|
+
(messageId: string) =>
|
|
365
|
+
(s: ChatStoreState): Operation | undefined => {
|
|
366
|
+
const operations = getOperationsByMessage(messageId)(s);
|
|
367
|
+
const runningOps = operations.filter((op) => op.status === 'running');
|
|
368
|
+
|
|
369
|
+
if (runningOps.length === 0) return undefined;
|
|
370
|
+
|
|
371
|
+
const runningOpIds = new Set(runningOps.map((op) => op.id));
|
|
372
|
+
|
|
373
|
+
// A leaf running operation has no running children
|
|
374
|
+
return runningOps.find((op) => {
|
|
375
|
+
const childIds = op.childOperationIds || [];
|
|
376
|
+
return !childIds.some((childId) => runningOpIds.has(childId));
|
|
377
|
+
});
|
|
378
|
+
};
|
|
379
|
+
|
|
358
380
|
/**
|
|
359
381
|
* Check if a specific message is being regenerated
|
|
360
382
|
*/
|
|
@@ -440,6 +462,7 @@ export const operationSelectors = {
|
|
|
440
462
|
getCurrentContextOperations,
|
|
441
463
|
getCurrentOperationLabel,
|
|
442
464
|
getCurrentOperationProgress,
|
|
465
|
+
getDeepestRunningOperationByMessage,
|
|
443
466
|
getOperationById,
|
|
444
467
|
getOperationContextFromMessage,
|
|
445
468
|
getOperationsByContext,
|