agent对话步骤详情界面
对应源码路径:
这是一个使用 Next.js 框架开发的聊天界面组件代码。主要功能:
1. 基本功能:
- 实现了一个完整的聊天界面,支持与 AI 助手(Suna)进行对话
- 支持消息的发送、接收和实时流式显示
- 包含工具调用(Tool Calls)的侧边栏面板
- 支持文件上传和预览功能
2. 主要组件结构:
- 顶部导航栏(`SiteHeader`)
- 消息列表区域
- 底部输入框(`ChatInput`)
- 工具调用侧边栏(`ToolCallSidePanel`)
- 文件查看器模态框(`FileViewerModal`)
3. 核心功能实现:
- 消息处理:
- 支持用户消息和助手消息的显示
- 消息分组显示
- Markdown 渲染支持
- 实时流式响应
- 工具调用:
- 支持多种工具操作(如执行命令、文件操作等)
- 工具调用状态显示
- 工具调用结果展示
- 状态管理:
- 消息加载状态
- 代理运行状态
- 侧边栏开关状态
- 文件查看状态
4. 特色功能:
- 实时流式响应:支持 AI 助手的实时打字效果
- 工具调用可视化:通过侧边栏展示工具调用详情
- 文件处理:支持文件上传和预览
- 响应式设计:适配移动端和桌面端
- 计费限制:包含免费用户的使用限制检查
5. 用户体验优化:
- 自动滚动到最新消息
- 加载状态显示
- 错误处理和提示
- 键盘快捷键支持
- 滚动到底部按钮
6. 性能优化:
- 使用 `useCallback` 和 `useMemo` 优化性能
- 消息分组渲染
- 条件渲染优化
这是一个功能完整的现代聊天应用界面,集成了 AI 对话、工具调用、文件处理等多种功能,并注重用户体验和性能优化。
消息列表区域的实现逻辑:
1. 消息列表的基本结构:
```typescript
<div
ref={messagesContainerRef}
className="flex-1 overflow-y-auto px-6 py-4 pb-24 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
onScroll={handleScroll}
>
<div className="mx-auto max-w-3xl">
{/* 消息内容区域 */}
</div>
</div>
```
2. 消息分组逻辑:
```typescript
type MessageGroup = {
type: 'user' | 'assistant_group';
messages: UnifiedMessage[];
key: string;
};
// 消息分组处理
const groupedMessages: MessageGroup[] = [];
let currentGroup: MessageGroup | null = null;
messages.forEach((message, index) => {
const messageType = message.type;
const key = message.message_id || `msg-${index}`;
if (messageType === 'user') {
// 用户消息单独成组
if (currentGroup) {
groupedMessages.push(currentGroup);
}
groupedMessages.push({ type: 'user', messages: [message], key });
currentGroup = null;
} else if (messageType === 'assistant' || messageType === 'tool' || messageType === 'browser_state') {
// 助手消息、工具消息和浏览器状态消息归为一组
if (currentGroup && currentGroup.type === 'assistant_group') {
currentGroup.messages.push(message);
} else {
if (currentGroup) {
groupedMessages.push(currentGroup);
}
currentGroup = { type: 'assistant_group', messages: [message], key };
}
}
});
```
3. 消息渲染逻辑:
用户消息渲染:
```typescript
if (group.type === 'user') {
const message = group.messages[0];
const messageContent = (() => {
try {
const parsed = safeJsonParse<ParsedContent>(message.content, { content: message.content });
return parsed.content || message.content;
} catch {
return message.content;
}
})();
// 处理附件
const attachmentsMatch = messageContent.match(/\[Uploaded File: (.*?)\]/g);
const attachments = attachmentsMatch
? attachmentsMatch.map(match => {
const pathMatch = match.match(/\[Uploaded File: (.*?)\]/);
return pathMatch ? pathMatch[1] : null;
}).filter(Boolean)
: [];
// 清理消息内容
const cleanContent = messageContent.replace(/\[Uploaded File: .*?\]/g, '').trim();
return (
<div key={group.key} className="flex justify-end">
<div className="inline-flex max-w-[85%] rounded-lg bg-primary/10 px-4 py-3">
<div className="space-y-3">
{cleanContent && (
<Markdown className="text-sm prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">
{cleanContent}
</Markdown>
)}
{renderAttachments(attachments, handleOpenFileViewer)}
</div>
</div>
</div>
);
}
```
助手消息渲染:
```typescript
if (group.type === 'assistant_group') {
return (
<div key={group.key} ref={groupIndex === groupedMessages.length - 1 ? latestMessageRef : null}>
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-5 h-5 mt-2 rounded-md flex items-center justify-center overflow-hidden ml-auto mr-2">
<Image src="/kortix-symbol.svg" alt="Kortix" width={14} height={14} className="object-contain invert dark:invert-0 opacity-70" />
</div>
<div className="flex-1">
<div className="inline-flex max-w-[90%] rounded-lg bg-muted/5 px-4 py-3 text-sm">
<div className="space-y-2">
{group.messages.map((message, msgIndex) => {
if (message.type === 'assistant') {
const parsedContent = safeJsonParse<ParsedContent>(message.content, { content: '' });
const msgKey = message.message_id || `submsg-assistant-${msgIndex}`;
// 渲染消息内容
const renderedContent = renderMarkdownContent(
parsedContent.content || '',
handleToolClick,
message.message_id,
handleOpenFileViewer
);
// 检查是否是正在流式传输的消息
const isStreamingThisMessage = message.message_id === streamingMessageId;
return (
<div key={msgKey} className={msgIndex > 0 && group.messages[msgIndex-1]?.type === 'assistant' ? "mt-2" : ""}>
<div className="prose prose-sm dark:prose-invert chat-markdown max-w-none [&>:first-child]:mt-0 prose-headings:mt-3">
{renderedContent}
{isStreamingThisMessage && (
<span className="inline-block h-4 w-0.5 bg-primary ml-0.5 -mb-1 animate-pulse" />
)}
</div>
</div>
);
}
return null;
})}
</div>
</div>
</div>
</div>
</div>
);
}
```
4. 滚动处理:
```typescript
const handleScroll = () => {
if (!messagesContainerRef.current) return;
const { scrollTop, scrollHeight, clientHeight } = messagesContainerRef.current;
const isScrolledUp = scrollHeight - scrollTop - clientHeight > 100;
setShowScrollButton(isScrolledUp);
setUserHasScrolled(isScrolledUp);
};
const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
messagesEndRef.current?.scrollIntoView({ behavior });
}, []);
```
5. 流式响应处理:
```typescript
// 处理流式消息更新
const handleAssistantStreamChunk = useCallback(({ content: chunkContent, message_id }: { content: string; message_id: string | null }) => {
if (!message_id) return;
setMessages(prevMessages => {
const msgIndex = prevMessages.findIndex(m => m.message_id === message_id);
if (msgIndex === -1) return prevMessages;
return prevMessages.map((msg, index) => {
if (index === msgIndex) {
const existingParsedContent = safeJsonParse<ParsedContent>(msg.content, { role: 'assistant', content: '' });
const newInnerContent = (existingParsedContent.content || '') + chunkContent;
const updatedContentObject: ParsedContent = { ...existingParsedContent, content: newInnerContent };
return {
...msg,
content: JSON.stringify(updatedContentObject),
updated_at: new Date().toISOString()
};
}
return msg;
});
});
if (!userHasScrolled) scrollToBottom('auto');
}, [userHasScrolled]);
```
这个实现包含了以下关键特性:
1. 消息分组显示,提高可读性
2. 支持 Markdown 渲染
3. 实时流式响应
4. 文件附件处理
5. 自动滚动和手动滚动控制
6. 工具调用集成
7. 响应式布局适配
整个实现注重用户体验,包括消息的流畅显示、实时更新、滚动控制等,同时保持了代码的可维护性和性能优化。