Compare commits

...

1 Commits

Author SHA1 Message Date
rdmclin2 3be2635478 feat: implement AgentHome as standalone page
Redesign AgentHome from a Conversation welcome component into an
independent page with its own ChatInput, RecentTopics, and TaskList.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:13:28 +08:00
10 changed files with 376 additions and 3 deletions
+3
View File
@@ -383,6 +383,8 @@
"task.status.fetchingDetails": "Fetching details...",
"task.status.initializing": "Initializing task...",
"task.subtask": "Subtask",
"task.title": "Tasks",
"task.viewAll": "View All Tasks",
"thread.divider": "Subtopic",
"thread.threadMessageCount": "{{messageCount}} messages",
"thread.title": "Subtopic",
@@ -433,6 +435,7 @@
"topic.openNewTopic": "Open New Topic",
"topic.recent": "Recent Topics",
"topic.saveCurrentMessages": "Save current session as topic",
"topic.viewAll": "View All Topics",
"translate.action": "Translate",
"translate.clear": "Clear Translation",
"tts.action": "Text-to-Speech",
+3
View File
@@ -383,6 +383,8 @@
"task.status.fetchingDetails": "正在获取详情...",
"task.status.initializing": "任务启动中…",
"task.subtask": "子任务",
"task.title": "任务",
"task.viewAll": "查看全部任务",
"thread.divider": "子话题",
"thread.threadMessageCount": "{{messageCount}} 条消息",
"thread.title": "子话题",
@@ -433,6 +435,7 @@
"topic.openNewTopic": "开启新话题",
"topic.recent": "最近话题",
"topic.saveCurrentMessages": "保存为话题",
"topic.viewAll": "查看全部话题",
"translate.action": "翻译",
"translate.clear": "删除翻译",
"tts.action": "语音朗读",
+54
View File
@@ -0,0 +1,54 @@
'use client';
import { Avatar, Flexbox, Markdown, Text } from '@lobehub/ui';
import isEqual from 'fast-deep-equal';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { DEFAULT_AVATAR, DEFAULT_INBOX_AVATAR } from '@/const/meta';
import { useAgentStore } from '@/store/agent';
import { agentSelectors, builtinAgentSelectors } from '@/store/agent/selectors';
import { useUserStore } from '@/store/user';
import { userGeneralSettingsSelectors } from '@/store/user/slices/settings/selectors';
const AgentInfo = memo(() => {
const { t } = useTranslation(['chat', 'welcome']);
const isInbox = useAgentStore(builtinAgentSelectors.isInboxAgent);
const meta = useAgentStore(agentSelectors.currentAgentMeta, isEqual);
const openingMessage = useAgentStore(agentSelectors.openingMessage);
const fontSize = useUserStore(userGeneralSettingsSelectors.fontSize);
const displayTitle = isInbox
? meta.title || 'Lobe AI'
: meta.title || t('defaultSession', { ns: 'common' });
const message = useMemo(() => {
if (openingMessage) return openingMessage;
return t('agentDefaultMessageWithSystemRole', {
name: displayTitle,
});
}, [openingMessage, displayTitle, t]);
return (
<Flexbox gap={16}>
<Flexbox horizontal align={'center'} gap={12}>
<Avatar
avatar={isInbox ? meta.avatar || DEFAULT_INBOX_AVATAR : meta.avatar || DEFAULT_AVATAR}
background={meta.backgroundColor}
shape={'square'}
size={36}
/>
<Text fontSize={18} weight={'bold'}>
{displayTitle}
</Text>
</Flexbox>
<Flexbox width={'min(100%, 640px)'}>
<Markdown fontSize={fontSize} variant={'chat'}>
{message}
</Markdown>
</Flexbox>
</Flexbox>
);
});
export default AgentInfo;
+99
View File
@@ -0,0 +1,99 @@
'use client';
import { Flexbox } from '@lobehub/ui';
import { memo, useCallback } from 'react';
import { useParams } from 'react-router-dom';
import DragUploadZone, { useUploadFiles } from '@/components/DragUploadZone';
import { type ActionKeys, ChatInputProvider, DesktopChatInput } from '@/features/ChatInput';
import type { SendButtonHandler } from '@/features/ChatInput/store/initialState';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { useChatStore } from '@/store/chat';
import { fileChatSelectors, useFileStore } from '@/store/file';
const leftActions: ActionKeys[] = ['model', 'search', 'fileUpload', 'tools'];
const inputContainerProps = {
minHeight: 88,
resize: false,
style: {
borderRadius: 20,
boxShadow: '0 12px 32px rgba(0,0,0,.04)',
},
};
const InputArea = memo(() => {
const { aid } = useParams<{ aid: string }>();
const sendMessage = useChatStore((s) => s.sendMessage);
const clearChatUploadFileList = useFileStore((s) => s.clearChatUploadFileList);
const clearChatContextSelections = useFileStore((s) => s.clearChatContextSelections);
const model = useAgentStore(agentSelectors.currentAgentModel);
const provider = useAgentStore(agentSelectors.currentAgentModelProvider);
const { handleUploadFiles } = useUploadFiles({ model, provider });
const send = useCallback<SendButtonHandler>(
async ({ getEditorData }) => {
if (!aid) return;
const { inputMessage, mainInputEditor } = useChatStore.getState();
const editorData = getEditorData?.() ?? mainInputEditor?.getJSONState();
const fileList = fileChatSelectors.chatUploadFileList(useFileStore.getState());
const contextList = fileChatSelectors.chatContextSelections(useFileStore.getState());
if (!inputMessage && fileList.length === 0 && contextList.length === 0) return;
try {
sendMessage({
context: { agentId: aid },
contexts: contextList,
editorData,
files: fileList,
message: inputMessage,
});
} finally {
clearChatUploadFileList();
clearChatContextSelections();
mainInputEditor?.clearContent();
}
},
[aid, sendMessage, clearChatUploadFileList, clearChatContextSelections],
);
const agentId = aid || '';
return (
<Flexbox style={{ position: 'relative' }}>
<DragUploadZone style={{ position: 'relative', zIndex: 1 }} onUploadFiles={handleUploadFiles}>
<ChatInputProvider
agentId={agentId}
allowExpand={false}
leftActions={leftActions}
slashPlacement="bottom"
chatInputEditorRef={(instance) => {
if (!instance) return;
useChatStore.setState({ mainInputEditor: instance });
}}
sendButtonProps={{
generating: false,
onStop: () => {},
shape: 'round',
}}
onSend={send}
onMarkdownContentChange={(content) => {
useChatStore.setState({ inputMessage: content });
}}
>
<DesktopChatInput
dropdownPlacement="bottomLeft"
inputContainerProps={inputContainerProps}
showRuntimeConfig={false}
/>
</ChatInputProvider>
</DragUploadZone>
</Flexbox>
);
});
export default InputArea;
+78
View File
@@ -0,0 +1,78 @@
'use client';
import { BotMessageSquareIcon } from 'lucide-react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useParams } from 'react-router-dom';
import useSWR from 'swr';
import GroupBlock from '@/routes/(main)/home/features/components/GroupBlock';
import GroupSkeleton from '@/routes/(main)/home/features/components/GroupSkeleton';
import ScrollShadowWithButton from '@/routes/(main)/home/features/components/ScrollShadowWithButton';
import { RECENT_BLOCK_SIZE } from '@/routes/(main)/home/features/const';
import ReactTopicItem from '@/routes/(main)/home/features/RecentTopic/Item';
import { topicService } from '@/services/topic';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { type RecentTopic } from '@/types/topic';
const AgentRecentTopics = memo(() => {
const { t } = useTranslation('chat');
const { aid } = useParams<{ aid: string }>();
const meta = useAgentStore(agentSelectors.currentAgentMeta);
const { data: result, isLoading } = useSWR(aid ? ['agentHome.topics', aid] : null, () =>
topicService.getTopics({ agentId: aid!, current: 0, pageSize: 10 }),
);
const topics: RecentTopic[] = useMemo(() => {
if (!result?.items) return [];
return result.items.map((topic) => ({
agent: {
avatar: meta.avatar || null,
backgroundColor: meta.backgroundColor || null,
id: aid!,
title: meta.title || null,
},
group: null,
id: topic.id,
title: topic.title || null,
type: 'agent' as const,
updatedAt: new Date(topic.updatedAt),
}));
}, [result?.items, meta, aid]);
if (isLoading) {
return (
<GroupBlock icon={BotMessageSquareIcon} title={t('topic.recent')}>
<ScrollShadowWithButton>
<GroupSkeleton
height={RECENT_BLOCK_SIZE.TOPIC.HEIGHT}
rows={6}
width={RECENT_BLOCK_SIZE.TOPIC.WIDTH}
/>
</ScrollShadowWithButton>
</GroupBlock>
);
}
if (!topics || topics.length === 0) return null;
return (
<GroupBlock icon={BotMessageSquareIcon} title={t('topic.recent')}>
<ScrollShadowWithButton>
{topics.map((topic) => (
<Link
key={topic.id}
style={{ color: 'inherit', flexShrink: 0, textDecoration: 'none' }}
to={`/agent/${aid}?topic=${topic.id}`}
>
<ReactTopicItem {...topic} />
</Link>
))}
</ScrollShadowWithButton>
</GroupBlock>
);
});
export default AgentRecentTopics;
+78
View File
@@ -0,0 +1,78 @@
'use client';
import { Block, Flexbox, Tag, Text } from '@lobehub/ui';
import { CheckSquareIcon } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, useParams } from 'react-router-dom';
import useSWR from 'swr';
import GroupBlock from '@/routes/(main)/home/features/components/GroupBlock';
import { taskService } from '@/services/task';
const STATUS_COLORS: Record<string, string> = {
completed: 'green',
failed: 'red',
in_progress: 'blue',
pending: 'default',
running: 'blue',
};
const AgentTaskList = memo(() => {
const { t } = useTranslation('chat');
const { aid } = useParams<{ aid: string }>();
const { data: result, isLoading } = useSWR(aid ? ['agentHome.tasks', aid] : null, () =>
taskService.list({ assigneeAgentId: aid!, limit: 10 }),
);
const tasks = result?.data;
if (isLoading || !tasks || tasks.length === 0) return null;
return (
<GroupBlock
actionAlwaysVisible
icon={CheckSquareIcon}
title={t('task.title')}
action={
<Link style={{ color: 'inherit', textDecoration: 'none' }} to={`/agent/${aid}`}>
<Text fontSize={12} type={'secondary'}>
{t('task.viewAll')}
</Text>
</Link>
}
>
<Flexbox gap={2}>
{tasks.map((task: any) => (
<Block clickable key={task.id} variant={'borderless'}>
<Flexbox
horizontal
align={'center'}
gap={12}
height={48}
justify={'space-between'}
paddingInline={12}
>
<Flexbox flex={1} gap={2} style={{ overflow: 'hidden' }}>
<Text ellipsis weight={500}>
{task.name || task.instruction}
</Text>
{task.description && (
<Text ellipsis fontSize={12} type={'secondary'}>
{task.description}
</Text>
)}
</Flexbox>
<Tag color={STATUS_COLORS[task.status] || 'default'} style={{ flexShrink: 0 }}>
{task.status}
</Tag>
</Flexbox>
</Block>
))}
</Flexbox>
</GroupBlock>
);
});
export default AgentTaskList;
+37
View File
@@ -0,0 +1,37 @@
'use client';
import { Flexbox } from '@lobehub/ui';
import isEqual from 'fast-deep-equal';
import { memo } from 'react';
import WideScreenContainer from '@/features/WideScreenContainer';
import ToolAuthAlert from '@/routes/(main)/agent/features/Conversation/AgentWelcome/ToolAuthAlert';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import AgentInfo from './AgentInfo';
import InputArea from './InputArea';
import OpeningQuestions from './OpeningQuestions';
import RecentTopics from './RecentTopics';
import TaskList from './TaskList';
const AgentHome = memo(() => {
const openingQuestions = useAgentStore(agentSelectors.openingQuestions, isEqual);
return (
<Flexbox height={'100%'} style={{ overflowY: 'auto', paddingBottom: '8vh' }} width={'100%'}>
<WideScreenContainer>
<Flexbox gap={32}>
<AgentInfo />
<InputArea />
{openingQuestions.length > 0 && <OpeningQuestions questions={openingQuestions} />}
<ToolAuthAlert />
<RecentTopics />
<TaskList />
</Flexbox>
</WideScreenContainer>
</Flexbox>
);
});
export default AgentHome;
+9
View File
@@ -236,6 +236,11 @@ export default {
'minimap.previousMessage': 'Previous message',
'minimap.senderAssistant': 'Agent',
'minimap.senderUser': 'You',
'createModal.createBlank': 'Create Blank',
'createModal.groupPlaceholder': 'Describe what this group should do...',
'createModal.groupTitle': 'What should your group do?',
'createModal.placeholder': 'Let AI create an agent...',
'createModal.title': 'What should your agent do?',
'newAgent': 'Create Agent',
'newGroupChat': 'Create Group',
'newPage': 'Create Page',
@@ -463,13 +468,17 @@ export default {
'toolAuth.authorizing': 'Authorizing...',
'toolAuth.hint':
'Without authorization or configuration, Skills may not work. This can limit the Agent or cause errors.',
'task.title': 'Tasks',
'task.viewAll': 'View All Tasks',
'toolAuth.signIn': 'Sign In',
'toolAuth.title': 'Authorize Skills for this Agent',
'topic.checkOpenNewTopic': 'Start a new topic?',
'topic.checkSaveCurrentMessages': 'Do you want to save the current conversation as a topic?',
'topic.openNewTopic': 'Open New Topic',
'topic.defaultTitle': 'Untitled Topic',
'topic.recent': 'Recent Topics',
'topic.saveCurrentMessages': 'Save current session as topic',
'topic.viewAll': 'View All Topics',
'translate.action': 'Translate',
'translate.clear': 'Clear Translation',
'tts.action': 'Text-to-Speech',
@@ -11,7 +11,6 @@ import { useOperationState } from '@/hooks/useOperationState';
import { useChatStore } from '@/store/chat';
import { messageMapKey } from '@/store/chat/utils/messageMapKey';
import WelcomeChatItem from './AgentWelcome';
import ChatHydration from './ChatHydration';
import MainChatInput from './MainChatInput';
import MessageFromUrl from './MainChatInput/MessageFromUrl';
@@ -71,7 +70,7 @@ const Conversation = memo(() => {
position: 'relative',
}}
>
<ChatList welcome={<WelcomeChatItem />} />
<ChatList />
</Flexbox>
<TodoProgress />
<MainChatInput />
@@ -1,10 +1,12 @@
import { Flexbox, TooltipGroup } from '@lobehub/ui';
import React, { memo,Suspense } from 'react';
import React, { memo, Suspense } from 'react';
import DragUploadZone, { useUploadFiles } from '@/components/DragUploadZone';
import Loading from '@/components/Loading/BrandTextLoading';
import AgentHome from '@/features/AgentHome';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
import { useChatStore } from '@/store/chat';
import { useGlobalStore } from '@/store/global';
import { systemStatusSelectors } from '@/store/global/selectors';
@@ -19,12 +21,23 @@ const wrapperStyle: React.CSSProperties = {
const ChatConversation = memo(() => {
const showHeader = useGlobalStore(systemStatusSelectors.showChatHeader);
const activeTopicId = useChatStore((s) => s.activeTopicId);
// Get current agent's model info for vision support check
const model = useAgentStore(agentSelectors.currentAgentModel);
const provider = useAgentStore(agentSelectors.currentAgentModelProvider);
const { handleUploadFiles } = useUploadFiles({ model, provider });
// Show AgentHome when no topic is selected (new conversation)
if (!activeTopicId) {
return (
<Flexbox height={'100%'} style={{ overflow: 'hidden', position: 'relative' }} width={'100%'}>
{showHeader && <ChatHeader />}
<AgentHome />
</Flexbox>
);
}
return (
<Suspense fallback={<Loading debugId="Agent > ChatConversation" />}>
<DragUploadZone style={wrapperStyle} onUploadFiles={handleUploadFiles}>