💄 style: support streaming and display ui for group mode

This commit is contained in:
arvinxx
2025-12-30 01:08:34 +08:00
parent 30cb4dfb93
commit f708cdb901
16 changed files with 495 additions and 3 deletions
+2
View File
@@ -51,6 +51,8 @@
"builtins.lobe-group-management.apiName.speak": "Designated member speaks",
"builtins.lobe-group-management.apiName.summarize": "Summarize conversation",
"builtins.lobe-group-management.apiName.vote": "Start vote",
"builtins.lobe-group-management.inspector.broadcast.title": "Following Agents speak:",
"builtins.lobe-group-management.inspector.speak.title": "Designated Agent speaks:",
"builtins.lobe-group-management.title": "Group Coordinator",
"builtins.lobe-gtd.apiName.clearTodos": "Clear todos",
"builtins.lobe-gtd.apiName.clearTodos.modeAll": "all",
+2
View File
@@ -51,6 +51,8 @@
"builtins.lobe-group-management.apiName.speak": "指定成员发言",
"builtins.lobe-group-management.apiName.summarize": "总结对话",
"builtins.lobe-group-management.apiName.vote": "发起投票",
"builtins.lobe-group-management.inspector.broadcast.title": "以下 Agent 发言:",
"builtins.lobe-group-management.inspector.speak.title": "指定 Agent 发言:",
"builtins.lobe-group-management.title": "群组协调",
"builtins.lobe-gtd.apiName.clearTodos": "清除待办",
"builtins.lobe-gtd.apiName.clearTodos.modeAll": "全部",
@@ -0,0 +1,89 @@
'use client';
import { DEFAULT_AVATAR } from '@lobechat/const';
import type { AgentItem, BuiltinInspectorProps } from '@lobechat/types';
import { Avatar, Flexbox } from '@lobehub/ui';
import { createStaticStyles, cx, useTheme } from 'antd-style';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAgentGroupStore } from '@/store/agentGroup';
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
import { shinyTextStyles } from '@/styles';
import type { BroadcastParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
root: css`
overflow: hidden;
display: flex;
gap: 8px;
align-items: center;
`,
title: css`
flex-shrink: 0;
color: ${cssVar.colorTextSecondary};
white-space: nowrap;
`,
}));
export const BroadcastInspector = memo<BuiltinInspectorProps<BroadcastParams>>(
({ args, partialArgs, isArgumentsStreaming }) => {
const { t } = useTranslation('plugin');
const agentIds = args?.agentIds || partialArgs?.agentIds || [];
// Get active group ID and agents from store
const activeGroupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
const groupAgents = useAgentGroupStore((s) =>
activeGroupId ? agentGroupSelectors.getGroupAgents(activeGroupId)(s) : [],
);
const theme = useTheme();
// Get agent details for the broadcast targets
const agents = useMemo(() => {
if (!agentIds.length || !groupAgents.length) return [];
return agentIds
.map((id) => groupAgents.find((agent) => agent.id === id))
.filter((agent): agent is AgentItem => !!agent);
}, [agentIds, groupAgents]);
// Transform agents to Avatar.Group format
const avatarItems = useMemo(
() =>
agents.map((agent) => ({
avatar: agent.avatar || DEFAULT_AVATAR,
background: agent.backgroundColor || theme.colorBgContainer,
key: agent.id,
title: agent.title || undefined,
})),
[agents],
);
if (isArgumentsStreaming && agents.length === 0) {
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-group-management.apiName.broadcast')}</span>
</div>
);
}
return (
<Flexbox
align={'center'}
className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
gap={8}
horizontal
>
<span className={styles.title}>
{t('builtins.lobe-group-management.inspector.broadcast.title')}
</span>
{avatarItems.length > 0 && <Avatar.Group items={avatarItems} shape={'square'} size={24} />}
</Flexbox>
);
},
);
BroadcastInspector.displayName = 'BroadcastInspector';
export default BroadcastInspector;
@@ -0,0 +1,87 @@
'use client';
import { DEFAULT_AVATAR } from '@lobechat/const';
import type { BuiltinInspectorProps } from '@lobechat/types';
import { Avatar, Flexbox } from '@lobehub/ui';
import { createStaticStyles, cx, useTheme } from 'antd-style';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useAgentGroupStore } from '@/store/agentGroup';
import { agentGroupSelectors } from '@/store/agentGroup/selectors';
import { shinyTextStyles } from '@/styles';
import type { SpeakParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
agentName: css`
padding-block-end: 1px;
color: ${cssVar.colorText};
background: linear-gradient(to top, ${cssVar.colorPrimaryBg} 40%, transparent 40%);
`,
root: css`
overflow: hidden;
display: flex;
gap: 8px;
align-items: center;
`,
title: css`
flex-shrink: 0;
color: ${cssVar.colorTextSecondary};
white-space: nowrap;
`,
}));
export const SpeakInspector = memo<BuiltinInspectorProps<SpeakParams>>(
({ args, partialArgs, isArgumentsStreaming }) => {
const { t } = useTranslation('plugin');
const agentId = args?.agentId || partialArgs?.agentId;
// Get active group ID and agent from store
const activeGroupId = useAgentGroupStore(agentGroupSelectors.activeGroupId);
const agent = useAgentGroupStore((s) =>
activeGroupId && agentId
? agentGroupSelectors.getAgentByIdFromGroup(activeGroupId, agentId)(s)
: undefined,
);
const theme = useTheme();
if (isArgumentsStreaming && !agent) {
return (
<div className={cx(styles.root, shinyTextStyles.shinyText)}>
<span>{t('builtins.lobe-group-management.apiName.speak')}</span>
</div>
);
}
const agentName = agent?.title || agentId;
return (
<Flexbox
align={'center'}
className={cx(styles.root, isArgumentsStreaming && shinyTextStyles.shinyText)}
gap={8}
horizontal
>
<span className={styles.title}>
{t('builtins.lobe-group-management.inspector.speak.title')}
</span>
{agent && (
<Avatar
avatar={agent.avatar || DEFAULT_AVATAR}
background={agent.backgroundColor || theme.colorBgContainer}
shape={'square'}
size={24}
title={agent.title || undefined}
/>
)}
{agentName && <span className={styles.agentName}>{agentName}</span>}
</Flexbox>
);
},
);
SpeakInspector.displayName = 'SpeakInspector';
export default SpeakInspector;
@@ -0,0 +1,16 @@
import { type BuiltinInspector } from '@lobechat/types';
import { GroupManagementApiName } from '../../types';
import { BroadcastInspector } from './Broadcast';
import { SpeakInspector } from './Speak';
/**
* Group Management Inspector Components Registry
*
* Inspector components customize the title/header area
* of tool calls in the conversation UI.
*/
export const GroupManagementInspectors: Record<string, BuiltinInspector> = {
[GroupManagementApiName.broadcast]: BroadcastInspector as BuiltinInspector,
[GroupManagementApiName.speak]: SpeakInspector as BuiltinInspector,
};
@@ -0,0 +1,38 @@
'use client';
import type { BuiltinRenderProps } from '@lobechat/types';
import { Markdown } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import type { BroadcastParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
padding: 12px;
border-radius: 8px;
background: ${cssVar.colorFillQuaternary};
`,
instruction: css`
font-size: 13px;
color: ${cssVar.colorTextSecondary};
`,
}));
export const BroadcastRender = memo<BuiltinRenderProps<BroadcastParams>>(({ args }) => {
const { instruction } = args || {};
if (!instruction) return null;
return (
<div className={styles.container}>
<div className={styles.instruction}>
<Markdown variant={'chat'}>{instruction}</Markdown>
</div>
</div>
);
});
BroadcastRender.displayName = 'BroadcastRender';
export default BroadcastRender;
@@ -0,0 +1,38 @@
'use client';
import type { BuiltinRenderProps } from '@lobechat/types';
import { Markdown } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import type { SpeakParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
padding: 12px;
border-radius: 8px;
background: ${cssVar.colorFillQuaternary};
`,
instruction: css`
font-size: 13px;
color: ${cssVar.colorTextSecondary};
`,
}));
export const SpeakRender = memo<BuiltinRenderProps<SpeakParams>>(({ args }) => {
const { instruction } = args || {};
if (!instruction) return null;
return (
<div className={styles.container}>
<div className={styles.instruction}>
<Markdown variant={'chat'}>{instruction}</Markdown>
</div>
</div>
);
});
SpeakRender.displayName = 'SpeakRender';
export default SpeakRender;
@@ -1,11 +1,17 @@
import { GroupManagementApiName } from '../../types';
import BroadcastRender from './Broadcast';
import ExecuteTaskRender from './ExecuteTask';
import SpeakRender from './Speak';
/**
* Group Management Tool Render Components Registry
*/
export const GroupManagementRenders = {
[GroupManagementApiName.broadcast]: BroadcastRender,
[GroupManagementApiName.executeTask]: ExecuteTaskRender,
[GroupManagementApiName.speak]: SpeakRender,
};
export { default as BroadcastRender } from './Broadcast';
export { default as ExecuteTaskRender } from './ExecuteTask';
export { default as SpeakRender } from './Speak';
@@ -0,0 +1,40 @@
'use client';
import type { BuiltinStreamingProps } from '@lobechat/types';
import { Markdown } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import type { BroadcastParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
padding: 12px;
border-radius: 8px;
background: ${cssVar.colorFillQuaternary};
`,
instruction: css`
font-size: 13px;
color: ${cssVar.colorTextSecondary};
`,
}));
export const BroadcastStreaming = memo<BuiltinStreamingProps<BroadcastParams>>(({ args }) => {
const { instruction } = args || {};
if (!instruction) return null;
return (
<div className={styles.container}>
<div className={styles.instruction}>
<Markdown animated variant={'chat'}>
{instruction}
</Markdown>
</div>
</div>
);
});
BroadcastStreaming.displayName = 'BroadcastStreaming';
export default BroadcastStreaming;
@@ -0,0 +1,40 @@
'use client';
import type { BuiltinStreamingProps } from '@lobechat/types';
import { Markdown } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
import type { SpeakParams } from '../../../types';
const styles = createStaticStyles(({ css, cssVar }) => ({
container: css`
padding: 12px;
border-radius: 8px;
background: ${cssVar.colorFillQuaternary};
`,
instruction: css`
font-size: 13px;
color: ${cssVar.colorTextSecondary};
`,
}));
export const SpeakStreaming = memo<BuiltinStreamingProps<SpeakParams>>(({ args }) => {
const { instruction } = args || {};
if (!instruction) return null;
return (
<div className={styles.container}>
<div className={styles.instruction}>
<Markdown animated variant={'chat'}>
{instruction}
</Markdown>
</div>
</div>
);
});
SpeakStreaming.displayName = 'SpeakStreaming';
export default SpeakStreaming;
@@ -0,0 +1,16 @@
import type { BuiltinStreaming } from '@lobechat/types';
import { GroupManagementApiName } from '../../types';
import { BroadcastStreaming } from './Broadcast';
import { SpeakStreaming } from './Speak';
/**
* Group Management Streaming Components Registry
*
* Streaming components render tool calls while they are
* still executing, allowing real-time feedback to users.
*/
export const GroupManagementStreamings: Record<string, BuiltinStreaming> = {
[GroupManagementApiName.broadcast]: BroadcastStreaming as BuiltinStreaming,
[GroupManagementApiName.speak]: SpeakStreaming as BuiltinStreaming,
};
@@ -1,5 +1,11 @@
// Inspector components (title/header area)
export { GroupManagementInspectors } from './Inspector';
// Streaming components (real-time feedback)
export { GroupManagementStreamings } from './Streaming';
// Render components (read-only snapshots)
export { ExecuteTaskRender, GroupManagementRenders } from './Render';
export { BroadcastRender, ExecuteTaskRender, GroupManagementRenders, SpeakRender } from './Render';
// Intervention components (interactive editing)
export { ExecuteTaskIntervention, GroupManagementInterventions } from './Intervention';
@@ -150,7 +150,7 @@
"id": "msg-supervisor-summary",
"role": "supervisor",
"agentId": "supervisor",
"content": "## Summary\n\nBased on the discussion from our team:\n\n**Key Pros:**\n- Independent deployment and scaling\n- Technology flexibility\n- Clear service boundaries\n\n**Key Cons:**\n- Increased complexity in communication and infrastructure\n- Data consistency and monitoring challenges\n\nI recommend starting with a modular monolith and gradually extracting services as needed.",
"content": "",
"parentId": "msg-agent-architect-1",
"model": "gpt-4",
"provider": "openai",
@@ -162,7 +162,16 @@
},
"metadata": {
"isSupervisor": true
}
},
"children": [
{
"id": "msg-supervisor-summary",
"content": "## Summary\n\nBased on the discussion from our team:\n\n**Key Pros:**\n- Independent deployment and scaling\n- Technology flexibility\n- Clear service boundaries\n\n**Key Cons:**\n- Increased complexity in communication and infrastructure\n- Data consistency and monitoring challenges\n\nI recommend starting with a modular monolith and gradually extracting services as needed.",
"metadata": {
"isSupervisor": true
}
}
]
}
],
"messageMap": {
@@ -154,6 +154,22 @@ export class FlatListBuilder {
continue;
}
// Priority 2b: Supervisor message without tools (content-only)
// Transform to supervisor role with content in children array
if (
message.role === 'assistant' &&
message.metadata?.isSupervisor &&
(!message.tools || message.tools.length === 0)
) {
const supervisorMessage = this.createSupervisorContentMessage(message);
flatList.push(supervisorMessage);
processedIds.add(message.id);
// Continue with children
this.buildFlatListRecursive(message.id, flatList, processedIds, allMessages);
continue;
}
// Priority 3a: Compare mode from user message metadata
const childMessages = this.childrenMap.get(message.id) ?? [];
if (this.isCompareMode(message) && childMessages.length > 1) {
@@ -747,4 +763,88 @@ export class FlatListBuilder {
},
} as Message;
}
/**
* Create supervisor virtual message for content-only supervisor messages
* Moves content to children array similar to assistantGroup
*/
private createSupervisorContentMessage(message: Message): Message {
// Prefer top-level usage/performance fields, fall back to metadata
const { usage: metaUsage, performance: metaPerformance } =
this.messageTransformer.splitMetadata(message.metadata);
const msgUsage = message.usage || metaUsage;
const msgPerformance = message.performance || metaPerformance;
// Extract non-usage/performance metadata fields
const otherMetadata: Record<string, any> = {};
if (message.metadata) {
const usagePerformanceFields = new Set([
'acceptedPredictionTokens',
'cost',
'duration',
'inputAudioTokens',
'inputCacheMissTokens',
'inputCachedTokens',
'inputCitationTokens',
'inputImageTokens',
'inputTextTokens',
'inputWriteCacheTokens',
'latency',
'outputAudioTokens',
'outputImageTokens',
'outputReasoningTokens',
'outputTextTokens',
'rejectedPredictionTokens',
'totalInputTokens',
'totalOutputTokens',
'totalTokens',
'tps',
'ttft',
]);
Object.entries(message.metadata).forEach(([key, value]) => {
if (!usagePerformanceFields.has(key)) {
otherMetadata[key] = value;
}
});
}
// Create the child content block
const childBlock: any = {
content: message.content || '',
id: message.id,
};
if (message.error) childBlock.error = message.error;
if (message.fileList && message.fileList.length > 0) childBlock.fileList = message.fileList;
if (message.imageList && message.imageList.length > 0) childBlock.imageList = message.imageList;
if (msgPerformance) childBlock.performance = msgPerformance;
if (message.reasoning) childBlock.reasoning = message.reasoning;
if (msgUsage) childBlock.usage = msgUsage;
if (Object.keys(otherMetadata).length > 0) {
childBlock.metadata = otherMetadata;
}
const result: Message = {
...message,
children: [childBlock],
content: '',
role: 'supervisor' as any,
};
// Remove fields that should not be in supervisor message
delete result.imageList;
delete result.metadata;
delete result.reasoning;
delete result.tools;
// Add aggregated fields if they exist
if (msgPerformance) result.performance = msgPerformance;
if (msgUsage) result.usage = msgUsage;
// Preserve isSupervisor in metadata
result.metadata = { isSupervisor: true, ...otherMetadata };
return result;
}
}
@@ -106,6 +106,7 @@ const Tool = memo<GroupToolProps>(
allowExpand={hasCustomRender}
expand={isToolRenderExpand}
itemKey={id}
onExpandChange={setShowPluginRender}
paddingBlock={4}
paddingInline={4}
title={
+2
View File
@@ -51,6 +51,8 @@ export default {
'builtins.lobe-group-management.apiName.speak': 'Designated member speaks',
'builtins.lobe-group-management.apiName.summarize': 'Summarize conversation',
'builtins.lobe-group-management.apiName.vote': 'Start vote',
'builtins.lobe-group-management.inspector.broadcast.title': 'Following Agents speak:',
'builtins.lobe-group-management.inspector.speak.title': 'Designated Agent speaks:',
'builtins.lobe-group-management.title': 'Group Coordinator',
'builtins.lobe-gtd.apiName.clearTodos': 'Clear todos',
'builtins.lobe-gtd.apiName.clearTodos.modeAll': 'all',