前端 + AI 进阶 Day 10 富交互消息
前端 + AI 进阶学习路线|Week 9-10:对话式界面设计
Day 10:富交互消息
学习时间:2026年1月3日(星期六)
关键词:富交互消息、按钮/卡片/表单、消息流嵌入、AI 原生交互、action 触发
📁 项目文件结构
day10-rich-interactive-messages/
├── src/
│ ├── components/
│ │ ├── MessageRenderer.jsx # 智能消息渲染器(支持富交互)
│ │ ├── ChatInput.jsx # 聊天输入框(支持快捷操作)
│ │ └── SessionSidebar.jsx # 复用 Day 9 的会话管理
│ ├── hooks/
│ │ └── useSessionManager.js # 复用 Day 9 的会话 Hook
│ ├── lib/
│ │ └── aiClient.js # 复用 Day 4 的 AI 客户端
│ └── App.jsx # 主应用集成
└── public/
✅ 本日核心:让 AI 消息不再只是文本,而是可交互的 UI 元素
🎯 今日学习目标
- 实现 消息流中嵌入按钮、卡片、表单
- 支持 用户点击按钮触发 AI 动作(如“重试”、“查看详情”)
- 构建 AI 原生交互范式(消息即界面)
- 与多会话系统无缝集成(复用 Day 9)
💡 为什么需要富交互消息?
传统聊天界面只能“看文字”,但 AI 可以:
- 生成 带操作按钮的回复(如“是否保存此代码?” → ✅ 保存 / ❌ 取消)
- 返回 结构化卡片(商品信息 + “加入购物车”)
- 嵌入 表单(“请填写以下信息” → 输入框 + 提交)
✅ 消息即界面(Message-as-UI) 是 AI 原生应用的核心范式,超越传统聊天
📚 核心设计思路
| 功能 | 实现方式 |
|---|---|
| 消息类型扩展 | `message.type = 'text' |
| 动作触发 | 按钮点击 → 调用 onAction(actionType, payload) |
| 状态管理 | 消息流中包含可交互元素,状态由会话管理器统一维护 |
| 安全渲染 | 富消息由前端解析(非 dangerouslySetInnerHTML) |
⚠️ 注意:AI 返回的富消息需为 结构化 JSON(非 HTML),前端安全渲染
🔧 动手实践:构建富交互消息系统
步骤 1:创建项目并复用 Day 9 组件
npx create-react-app day10-rich-interactive-messages
cd day10-rich-interactive-messages
# 复制 Day 9 的 hooks/ 和 components/SessionSidebar.jsx
# 安装依赖(同 Day 9)
npm install react-virtual react-markdown remark-gfm rehype-highlight rehype-katex katex @microsoft/fetch-event-source
步骤 2:扩展消息数据结构(在会话管理中)
更新
useSessionManager.js中的createNewSession
// 默认消息支持富交互
const createNewSession = (title = '新会话') => ({
id: generateSessionId(),
title,
messages: [
{
id: 'welcome',
role: 'assistant',
type: 'text',
content: '你好!我是你的 AI 助手。我可以生成代码、回答问题,甚至提供交互式操作。试试问我:“帮我生成一个登录表单”?'
}
],
createdAt: Date.now(),
});
步骤 3:创建智能消息渲染器
// src/components/MessageRenderer.jsx
import React from 'react';
import StreamingMarkdown from './StreamingMarkdown'; // 复用 Day 2
const MessageRenderer = ({ message, onAction, isStreaming = false }) => {
// 流式消息或普通文本
if (message.type === 'text' || !message.type) {
return <StreamingMarkdown content={message.content} />;
}
// 按钮消息
if (message.type === 'button') {
return (
<div style={{ marginTop: '8px' }}>
<div style={{ marginBottom: '8px' }}>
<StreamingMarkdown content={message.content} />
</div>
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
{message.buttons?.map((btn, idx) => (
<button
key={idx}
onClick={() => onAction?.(btn.action, btn.payload)}
style={{
padding: '6px 12px',
backgroundColor: btn.style === 'primary' ? '#1890ff' : '#f0f0f0',
color: btn.style === 'primary' ? 'white' : '#333',
border: '1px solid #d9d9d9',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
}}
>
{btn.text}
</button>
))}
</div>
</div>
);
}
// 卡片消息
if (message.type === 'card') {
return (
<div style={{
border: '1px solid #e8e8e8',
borderRadius: '8px',
padding: '16px',
backgroundColor: '#fafafa',
marginTop: '8px',
}}>
<div style={{ fontWeight: '600', marginBottom: '8px' }}>
{message.title}
</div>
<div style={{ marginBottom: '12px', fontSize: '14px' }}>
<StreamingMarkdown content={message.content} />
</div>
{message.actions && (
<div style={{ display: 'flex', gap: '8px' }}>
{message.actions.map((action, idx) => (
<button
key={idx}
onClick={() => onAction?.(action.type, action.payload)}
style={{
padding: '6px 12px',
fontSize: '14px',
backgroundColor: '#52c41a',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
{action.text}
</button>
))}
</div>
)}
</div>
);
}
// 表单消息(简化版)
if (message.type === 'form') {
const [formData, setFormData] = React.useState({});
const handleChange = (field, value) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
const handleSubmit = () => {
onAction?.('submit_form', { formId: message.formId, data: formData });
};
return (
<div style={{
border: '1px solid #e8e8e8',
borderRadius: '8px',
padding: '16px',
backgroundColor: '#fafafa',
marginTop: '8 p
}}>
<div style={{ fontWeight: '600', marginBottom: '12px' }}>
{message.title}
</div>
{message.fields?.map((field) => (
<div key={field.name} style={{ marginBottom: '12px' }}>
<div style={{ fontSize: '14px', marginBottom: '4px' }}>
{field.label} {field.required && <span style={{ color: '#ff4d4f' }}>*</span>}
</div>
{field.type === 'text' && (
<input
type="text"
value={formData[field.name] || ''}
onChange={(e) => handleChange(field.name, e.target.value)}
placeholder={field.placeholder}
style={{
width: '100%',
padding: '8px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
}}
/>
)}
{field.type === 'textarea' && (
<textarea
value={formData[field.name] || ''}
onChange={(e) => handleChange(field.name, e.target.value)}
placeholder={field.placeholder}
rows={3}
style={{
width: '100%',
padding: '8px',
border: '1px solid #d9d9d9',
borderRadius: '4px',
}}
/>
)}
</div>
))}
<button
onClick={handleSubmit}
style={{
padding: '8px 16px',
backgroundColor: '#1890ff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '14px',
}}
>
提交
</button>
</div>
);
}
// 默认回退到文本
return <StreamingMarkdown content={message.content} />;
};
export default MessageRenderer;
步骤 4:在聊天窗口中处理动作
// src/components/ChatWindow.jsx(关键部分)
import { useState, useRef, useEffect } from 'react';
import VirtualChatList from './VirtualChatList';
import MessageRenderer from './MessageRenderer';
import { streamAIResponse } from '../lib/aiClient';
const ChatWindow = ({ session, onUpdateMessages }) => {
const [inputValue, setInputValue] = useState('');
const [streamingMessage, setStreamingMessage] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
const chatContainerRef = useRef(null);
// 处理 AI 返回的富消息(模拟)
const handleAIResponse = (fullResponse) => {
let messages = session.messages;
// 模拟 AI 返回结构化消息
if (fullResponse.includes('登录表单')) {
const formMessage = {
id: Date.now(),
role: 'assistant',
type: 'form',
formId: 'login_form',
title: '请填写登录信息',
fields: [
{ name: 'email', label: '邮箱', type: 'text', required: true, placeholder: 'your@email.com' },
{ name: 'password', label: '密码', type: 'text', required: true, placeholder: '••••••••' }
]
};
messages = [...messages, formMessage];
} else if (fullResponse.includes('代码')) {
const buttonMessage = {
id: Date.now(),
role: 'assistant',
type: 'button',
content: '这是你请求的 React 组件代码:\n\n```jsx\nfunction Hello() {\n return <h1>Hello World</h1>;\n}\n```',
buttons: [
{ text: '✅ 保存到项目', action: 'save_code', payload: { code: 'function Hello() { return <h1>Hello World</h1>; }' }, style: 'primary' },
{ text: '🔄 重新生成', action: 'regenerate', payload: { prompt: inputValue }, style: 'default' }
]
};
messages = [...messages, buttonMessage];
} else {
// 普通文本消息
messages = [...messages, { id: Date.now(), role: 'assistant', type: 'text', content: fullResponse }];
}
onUpdateMessages(messages);
};
// 处理用户动作(按钮点击/表单提交)
const handleAction = (actionType, payload) => {
const userActionMessage = {
id: Date.now(),
role: 'user',
type: 'action',
content: `[用户执行操作: ${actionType}]`,
action: actionType,
payload
};
const updatedMessages = [...session.messages, userActionMessage];
onUpdateMessages(updatedMessages);
// 模拟 AI 对动作的响应
setTimeout(() => {
let aiResponse = '';
if (actionType === 'save_code') {
aiResponse = '✅ 代码已保存到你的项目中!是否需要我帮你部署?';
} else if (actionType === 'regenerate') {
aiResponse = '🔄 正在重新生成代码...\n\n```jsx\nfunction Greeting({ name }) {\n return <h1>Hello, {name}!</h1>;\n}\n```';
} else if (actionType === 'submit_form') {
aiResponse = `✅ 收到你的表单数据!邮箱: ${payload.data.email}。我们将尽快联系你。`;
} else {
aiResponse = '已收到你的操作。';
}
const aiMessage = {
id: Date.now() + 1,
role: 'assistant',
type: 'text',
content: aiResponse
};
onUpdateMessages([...updatedMessages, aiMessage]);
}, 800);
};
// 发送消息逻辑(略,复用 Day 9)
const handleSend = async () => { /* ... */ };
// 渲染消息列表(含流式)
const renderMessages = [...session.messages];
if (streamingMessage) {
renderMessages.push({
id: 'streaming',
role: 'assistant',
type: 'text',
content: streamingMessage,
});
}
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
<div
ref={chatContainerRef}
style={{
flex: 1,
overflow: 'auto',
padding: '16px',
}}
>
<VirtualChatList
messages={renderMessages}
onAction={handleAction}
isStreaming={isStreaming}
/>
</div>
{/* 聊天输入框(略) */}
</div>
);
};
// 更新 VirtualChatList 以传递 onAction
// VirtualChatList.jsx 中每个消息渲染:
// <MessageRenderer message={message} onAction={onAction} isStreaming={isStreaming} />
步骤 5:在 App.jsx 中集成
// src/App.jsx(复用 Day 9 结构,仅需确保 ChatWindow 支持 onAction)
import 'katex/dist/katex.min.css';
import { useSessionManager } from './hooks/useSessionManager';
import SessionSidebar from './components/SessionSidebar';
import ChatWindow from './components/ChatWindow';
function App() {
const {
sessions,
currentSession,
createSession,
switchSession,
updateSessionMessages,
updateSessionTitle,
deleteSession,
} = useSessionManager();
if (!currentSession) return <div style={{ padding: '20px' }}>加载中...</div>;
return (
<div style={{ display: 'flex', height: '100vh', fontFamily: 'Inter, -apple-system, sans-serif' }}>
<SessionSidebar {...{ sessions, currentSessionId: currentSession.id, createSession, switchSession, updateSessionTitle, deleteSession }} />
<div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '16px 24px', borderBottom: '1px solid #e8e8e8', fontSize: '16px', fontWeight: '600' }}>
{currentSession.title}
</div>
<ChatWindow
session={currentSession}
onUpdateMessages={updateSessionMessages}
/>
</div>
</div>
);
}
export default App;
✅ 效果验证
- ✅ 输入“帮我生成一个登录表单” → 显示可填写表单
- ✅ 输入“生成 React 代码” → 显示代码 + “保存”/“重新生成”按钮
- ✅ 点击按钮 → 触发用户动作消息 + AI 响应
- ✅ 表单提交 → 显示确认消息
- ✅ 所有交互消息支持 Markdown(代码高亮/公式)
- ✅ 与多会话系统无缝集成(不同会话独立状态)
🤔 思考与延伸
-
AI 返回结构化消息:如何让 LLM 返回 JSON 而非纯文本?
→ 在 prompt 中指定格式,如“请以 JSON 格式返回,包含 type、content、buttons 字段” -
安全性:如何防止恶意 payload?
→ 前端对action和payload做白名单校验 -
扩展性:如何支持更多交互类型(如日历、地图)?
→ 在MessageRenderer中增加case分支
💡 为 Week 3 做准备:富交互消息可作为工作流节点的输入/输出组件
📅 明日预告
Day 11:语音输入/输出
- 集成 Web Speech API 实现语音输入
- AI 回答自动语音播报
- 构建全语音 AI 交互体验
✍️ 小结
今天,我们让 AI 消息从“只读文本”进化为“可操作界面”!通过嵌入按钮、卡片、表单,用户可直接在消息流中完成操作,无需跳转。消息即界面,是 AI 原生应用的终极形态。
💬 实践提示:真实项目中,建议用 JSON Schema 校验 AI 返回的富消息结构。欢迎分享你的富交互消息设计!

浙公网安备 33010602011771号