一、系统架构总览
用户 (前端/App)
↓ (自然语言指令)
[前端:智能体交互层]
↓ (包含会话ID的请求)
[中台:智能体编排与执行引擎]
┌───────────┬───────────┬───────────┐
↓ ↓ ↓ ↓
[工具1: [工具2: [工具3: [记忆服务:
餐馆/菜单查询] 购物车操作] 地址管理] 会话状态存储]
↓ ↓ ↓ ↓
└───────────┴─────┬─────┴───────────┘
↓
[中台:智能体决策与规划]
↓ (结构化下一步指令或最终结果)
[外部服务API网关] → 美团/饿了么等外卖平台开放API
↓
[前端:渲染与用户确认界面]
二、工作流程:
完整工作流程示例
-
用户输入:“帮我点一份麦当劳巨无霸套餐,送到XX大厦,帮我到付款界面”
-
前端:发送请求到中台
/api/agent/order-food,附带会话ID。 -
中台 - 第一轮:
- LLM分析意图:需要
query_restaurant_menu工具。 - 发送事件:
status: querying_menu到前端。 - 执行工具:调用菜单查询工具,找到附近的麦当劳和巨无霸套餐。
- 发送事件:
message: “找到‘麦当劳(XX店)’,有巨无霸套餐,价格38元。确认加入购物车吗?”同时发送action_preview: {action: “add_to_cart”, items: [...]}。
- LLM分析意图:需要
-
前端:显示消息和确认卡片。用户点击“确认”。
-
中台 - 第二轮:
- 前端发送确认指令。
- LLM分析:用户已确认,需要
manage_cart工具。 - 执行工具:将商品加入购物车,返回成功。
- LLM发现地址信息“XX大厦”不明确,需要
address工具解析或用户确认。 - 发送事件:
message: “需要确认具体地址。XX大厦有以下地址选项...”。
-
用户:在前端选择或确认具体地址。
-
中台 - 第三轮:
- LLM分析:所有信息(餐厅、商品、地址)齐备,用户最终目标是“到付款界面”。
- 调用
get_payment_page工具。 - 发送事件:
action_preview: {action: “get_payment_page”, need_confirm: true}(高风险操作,必须确认)。
-
前端:弹出确认框“即将跳转支付,总价38元,请确认”。
-
用户:点击“确认支付”。
-
中台 - 最终轮:
- 调用支付工具,从外卖平台获取预填充好商品、地址、价格的支付页面URL(带临时token) 。
- 发送事件:
final_result: {type: “payment_page”, url: “https://pay.ele.me/...”}。
-
前端:在应用内嵌的WebView中加载该URL,用户看到的是外卖平台标准的支付页面,只需输入密码或验证指纹即可完成支付。
三、前端实现详解
1. 界面设计(关键组件)
-
智能体对话面板:核心区域,显示与AI的对话历史。
-
智能体状态可视化面板:
-
思考状态:显示“正在分析你的需求...”、“正在查询麦当劳菜单...”
-
操作预览:以卡片形式展示智能体即将或正在执行的操作,例如:
json
{ “action”: “add_to_cart”, “restaurant”: “麦当劳(XX店)”, “items”: [{“name”: “巨无霸套餐”, “quantity”: 1}], “need_confirm”: true // 需要用户确认 } -
工具调用记录:一个小日志,显示“调用了【菜单查询工具】”、“调用了【地址校验工具】”。
-
-
混合输入区:用户既可以继续用自然语言对话,也可以直接点击智能体推荐的选项进行确认或修改(例如,点击“更换套餐”或“确认加入购物车”)。
-
最终结果嵌入区:当智能体完成“到付款界面”的任务后,这个区域将直接嵌入从外卖平台获取的、已填好所有信息的支付页面H5或WebView。
-
主要功能:
- 自动创建和管理会话
- 实时SSE通信显示智能体状态
- 操作预览和确认机制
- 支付页面嵌入
- 完整的响应式UI
2. 前端核心逻辑(代码思路)
// 1. 初始化智能体会话
const agentSession = useAgentSession('外卖助手');
// 2. 发送用户消息
const handleUserMessage = async (userInput) => {
// 2.1 将用户输入添加到对话历史,并显示“思考中”状态
agentSession.addMessage({ role: 'user', content: userInput });
agentSession.setStatus('thinking');
// 2.2 调用中台智能体API(流式响应)
const response = await fetch('/api/agent/order-food', {
method: 'POST',
body: JSON.stringify({
session_id: agentSession.id,
message: userInput,
// 可以附加上下文,如前端当前的地理位置
context: { user_location: userLocation }
}),
headers: { 'Content-Type': 'application/json' }
});
// 2.3 处理流式响应(中台返回的是Server-Sent Events)
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const events = chunk.split('\n\n').filter(e => e);
for (const event of events) {
const [eventLine, dataLine] = event.split('\n');
if (eventLine === 'event: status') {
// 更新智能体状态
agentSession.setStatus(JSON.parse(dataLine.replace('data: ', '')).status);
} else if (eventLine === 'event: action_preview') {
// 显示智能体将要执行的操作(需要用户确认的卡片)
const action = JSON.parse(dataLine.replace('data: ', ''));
renderActionPreviewCard(action);
} else if (eventLine === 'event: message') {
// 逐步显示智能体的回复文本
agentSession.appendAgentMessage(JSON.parse(dataLine.replace('data: ', '')).content);
} else if (eventLine === 'event: final_result') {
// 收到最终结果,例如支付页面的URL或嵌入代码
const result = JSON.parse(dataLine.replace('data: ', ''));
if (result.type === 'payment_page') {
embedPaymentPage(result.url, result.token); // 嵌入支付页面
}
}
}
}
agentSession.setStatus('idle');
};
// 3. 用户确认智能体操作
const handleConfirmAction = (actionId, confirmedData) => {
// 发送确认信息回中台,例如:确认加入购物车的商品和数量
fetch('/api/agent/confirm-action', {
method: 'POST',
body: JSON.stringify({
session_id: agentSession.id,
action_id: actionId,
user_confirmation: confirmedData
})
});
};
3. 前端完整代码--更新中,可参考
## 整前端实现:基于SSE和JavaScript的AI外卖助手
class AIFoodOrderingAssistant {
constructor() {
// 应用状态
this.currentSessionId = null;
this.eventSource = null;
this.isConnected = false;
this.isProcessing = false;
this.pendingActionId = null;
// DOM元素
this.elements = {
userInput: document.getElementById('userInput'),
sendButton: document.getElementById('sendButton'),
messagesContainer: document.getElementById('messagesContainer'),
statusContainer: document.getElementById('statusContainer'),
sessionId: document.getElementById('sessionId'),
connectionStatus: document.getElementById('connectionStatus'),
paymentSection: document.getElementById('paymentSection'),
paymentFrame: document.getElementById('paymentFrame')
};
// 初始化
this.init();
}
/**
* 初始化应用
*/
async init() {
// 1. 创建或加载会话
await this.initializeSession();
// 2. 设置事件监听器
this.setupEventListeners();
// 3. 建立SSE连接
this.connectToAgent();
// 4. 显示欢迎消息
this.showWelcomeMessage();
}
/**
* 初始化会话
*/
async initializeSession() {
try {
// 尝试从localStorage获取现有会话ID
const savedSessionId = localStorage.getItem('agent_session_id');
const savedSessionTime = localStorage.getItem('agent_session_time');
// 检查会话是否过期(超过1小时)
const sessionExpired = savedSessionTime &&
(Date.now() - parseInt(savedSessionTime)) > 3600000;
if (savedSessionId && !sessionExpired) {
this.currentSessionId = savedSessionId;
this.elements.sessionId.textContent = savedSessionId.substring(0, 8) + '...';
} else {
// 创建新会话
const response = await fetch('/api/agent/session', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
this.currentSessionId = data.session_id;
// 保存到localStorage
localStorage.setItem('agent_session_id', this.currentSessionId);
localStorage.setItem('agent_session_time', Date.now().toString());
this.elements.sessionId.textContent = this.currentSessionId.substring(0, 8) + '...';
}
} catch (error) {
console.error('初始化会话失败:', error);
// 生成一个临时会话ID
this.currentSessionId = 'temp_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
this.elements.sessionId.textContent = '临时会话';
}
}
/**
* 设置事件监听器
*/
setupEventListeners() {
// 发送按钮点击事件
this.elements.sendButton.addEventListener('click', () => this.sendMessage());
// 输入框回车事件
this.elements.userInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
this.sendMessage();
}
});
// 输入框输入事件(启用/禁用发送按钮)
this.elements.userInput.addEventListener('input', () => {
const hasText = this.elements.userInput.value.trim().length > 0;
this.elements.sendButton.disabled = !hasText || this.isProcessing;
});
// 页面卸载时关闭SSE连接
window.addEventListener('beforeunload', () => {
if (this.eventSource) {
this.eventSource.close();
}
});
// 断线重连
window.addEventListener('online', () => {
if (!this.isConnected) {
this.connectToAgent();
}
});
}
/**
* 连接到智能体SSE流
*/
connectToAgent() {
if (this.eventSource) {
this.eventSource.close();
}
const sseUrl = `/api/agent/stream?session_id=${this.currentSessionId}`;
this.eventSource = new EventSource(sseUrl);
this.eventSource.onopen = () => {
console.log('SSE连接已建立');
this.isConnected = true;
this.updateConnectionStatus('🟢 已连接');
};
this.eventSource.onerror = (error) => {
console.error('SSE连接错误:', error);
this.isConnected = false;
this.updateConnectionStatus('🔴 连接断开');
// 尝试重新连接
setTimeout(() => {
if (!this.isConnected) {
this.connectToAgent();
}
}, 5000);
};
// 处理不同的SSE事件
this.eventSource.addEventListener('status', this.handleStatusEvent.bind(this));
this.eventSource.addEventListener('action_preview', this.handleActionPreviewEvent.bind(this));
this.eventSource.addEventListener('message', this.handleMessageEvent.bind(this));
this.eventSource.addEventListener('final_result', this.handleFinalResultEvent.bind(this));
this.eventSource.addEventListener('error', this.handleErrorEvent.bind(this));
}
/**
* 更新连接状态显示
*/
updateConnectionStatus(status) {
this.elements.connectionStatus.textContent = status;
}
/**
* 处理状态事件
*/
handleStatusEvent(event) {
const data = JSON.parse(event.data);
this.addStatusCard(data.status, data.details);
// 更新UI状态
if (data.status === 'thinking') {
this.showTypingIndicator();
this.isProcessing = true;
this.updateUIState();
} else if (data.status === 'executing_tool') {
this.addMessage('assistant', `正在执行: ${data.tool_name}...`, false);
this.isProcessing = true;
this.updateUIState();
} else if (data.status === 'idle') {
this.removeTypingIndicator();
this.isProcessing = false;
this.updateUIState();
}
}
/**
* 处理操作预览事件
*/
handleActionPreviewEvent(event) {
const data = JSON.parse(event.data);
this.pendingActionId = data.action_id;
this.showActionPreview(data);
}
/**
* 处理消息事件
*/
handleMessageEvent(event) {
const data = JSON.parse(event.data);
this.removeTypingIndicator();
this.addMessage('assistant', data.content, true);
}
/**
* 处理最终结果事件
*/
handleFinalResultEvent(event) {
const data = JSON.parse(event.data);
if (data.type === 'payment_page') {
this.showPaymentPage(data.url, data.token);
this.addMessage('assistant', '已为您跳转到支付页面,请确认订单信息并完成支付。', true);
} else if (data.type === 'order_created') {
this.addMessage('assistant', `订单创建成功!订单号: ${data.order_id},总价: ¥${data.total_price}`, true);
}
this.isProcessing = false;
this.updateUIState();
}
/**
* 处理错误事件
*/
handleErrorEvent(event) {
const data = JSON.parse(event.data);
this.addMessage('assistant', `❌ 错误: ${data.message}`, true);
this.addStatusCard('error', data.message);
this.isProcessing = false;
this.updateUIState();
}
/**
* 发送用户消息
*/
async sendMessage() {
const userInput = this.elements.userInput.value.trim();
if (!userInput || this.isProcessing) return;
// 添加用户消息到界面
this.addMessage('user', userInput, true);
// 清空输入框
this.elements.userInput.value = '';
this.elements.sendButton.disabled = true;
this.isProcessing = true;
this.updateUIState();
try {
// 发送消息到服务器
const response = await fetch('/api/agent/process', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: this.currentSessionId,
message: userInput,
context: {
user_location: await this.getUserLocation(),
timestamp: new Date().toISOString()
}
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 显示思考中的状态
this.addStatusCard('thinking', '正在分析您的需求...');
this.showTypingIndicator();
} catch (error) {
console.error('发送消息失败:', error);
this.addMessage('assistant', '抱歉,发送消息时出现错误,请稍后重试。', true);
this.isProcessing = false;
this.updateUIState();
}
}
/**
* 确认智能体操作
*/
async confirmAction(confirmedData = null) {
if (!this.pendingActionId) return;
try {
const response = await fetch('/api/agent/confirm-action', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: this.currentSessionId,
action_id: this.pendingActionId,
user_confirmation: confirmedData
})
});
if (response.ok) {
this.removeActionPreview();
this.pendingActionId = null;
}
} catch (error) {
console.error('确认操作失败:', error);
}
}
/**
* 修改操作
*/
modifyAction() {
// 在实际应用中,这里可以打开一个修改界面
const modification = prompt('请输入修改内容(例如:换大杯可乐,加一份薯条):');
if (modification) {
this.addMessage('user', `修改:${modification}`, true);
this.removeActionPreview();
this.sendModification(modification);
}
}
/**
* 发送修改请求
*/
async sendModification(modification) {
try {
const response = await fetch('/api/agent/modify', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: this.currentSessionId,
modification: modification
})
});
if (!response.ok) {
throw new Error('修改请求失败');
}
} catch (error) {
console.error('发送修改失败:', error);
}
}
/**
* 显示欢迎消息
*/
showWelcomeMessage() {
const welcomeMessages = [
"👋 你好!我是AI外卖助手,可以帮你完成从选餐到支付的全流程。",
"💡 你可以这样对我说:",
"• \"帮我点一份麦当劳巨无霸套餐,送到XX大厦\"",
"• \"我想吃披萨,送到公司地址\"",
"• \"点一份外卖,预算50元左右\"",
"我会一步步引导你完成订单,并在最后跳转到支付页面。"
];
setTimeout(() => {
welcomeMessages.forEach((msg, index) => {
setTimeout(() => {
this.addMessage('assistant', msg, true);
}, index * 300);
});
}, 1000);
}
/**
* 添加消息到聊天界面
*/
addMessage(role, content, animate = true) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}-message`;
const bubble = document.createElement('div');
bubble.className = 'message-bubble';
bubble.textContent = content;
messageDiv.appendChild(bubble);
if (animate) {
messageDiv.style.opacity = '0';
messageDiv.style.transform = 'translateY(10px)';
}
this.elements.messagesContainer.appendChild(messageDiv);
if (animate) {
requestAnimationFrame(() => {
messageDiv.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
messageDiv.style.opacity = '1';
messageDiv.style.transform = 'translateY(0)';
});
}
// 滚动到底部
this.scrollToBottom();
}
/**
* 显示输入指示器
*/
showTypingIndicator() {
this.removeTypingIndicator(); // 先移除可能存在的现有指示器
const typingDiv = document.createElement('div');
typingDiv.className = 'message agent-message typing-indicator';
typingDiv.id = 'typingIndicator';
const typingBubble = document.createElement('div');
typingBubble.className = 'message-typing';
const dots = document.createElement('div');
dots.className = 'typing-dots';
dots.innerHTML = '<span></span><span></span><span></span>';
typingBubble.appendChild(dots);
typingDiv.appendChild(typingBubble);
this.elements.messagesContainer.appendChild(typingDiv);
this.scrollToBottom();
}
/**
* 移除输入指示器
*/
removeTypingIndicator() {
const existingIndicator = document.getElementById('typingIndicator');
if (existingIndicator) {
existingIndicator.remove();
}
}
/**
* 添加状态卡片
*/
addStatusCard(status, details) {
const statusCard = document.createElement('div');
statusCard.className = 'status-card';
const now = new Date();
const timeString = now.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
let statusIcon = '🔵';
let statusText = '处理中';
switch(status) {
case 'thinking':
statusIcon = '🤔';
statusText = '思考中';
break;
case 'querying_menu':
statusIcon = '📋';
statusText = '查询菜单';
break;
case 'executing_tool':
statusIcon = '⚙️';
statusText = '执行工具';
break;
case 'waiting_confirmation':
statusIcon = '⏳';
statusText = '等待确认';
break;
case 'error':
statusIcon = '❌';
statusText = '错误';
break;
case 'completed':
statusIcon = '✅';
statusText = '已完成';
break;
}
statusCard.innerHTML = `
<h4>${statusIcon} ${statusText}</h4>
<p>${details}</p>
<div class="timestamp">${timeString}</div>
`;
this.elements.statusContainer.appendChild(statusCard);
// 滚动状态容器到底部
this.elements.statusContainer.scrollTop = this.elements.statusContainer.scrollHeight;
}
/**
* 显示操作预览
*/
showActionPreview(data) {
const actionCard = document.createElement('div');
actionCard.className = 'action-preview-card';
actionCard.id = 'actionPreviewCard';
let actionDetails = '';
let actionTitle = '请确认操作';
switch(data.action) {
case 'add_to_cart':
actionTitle = '🛒 添加到购物车';
actionDetails = `
<div><strong>餐厅:</strong>${data.params.restaurant || '未知'}</div>
<div><strong>商品:</strong></div>
<ul>
${(data.params.items || []).map(item =>
`<li>${item.name} × ${item.quantity}</li>`
).join('')}
</ul>
`;
break;
case 'select_address':
actionTitle = '📍 选择地址';
actionDetails = `
<div><strong>请选择收货地址:</strong></div>
${(data.params.options || []).map((addr, idx) => `
<div style="margin: 8px 0; padding: 8px; border: 1px solid #e5e7eb; border-radius: 6px;">
<label>
<input type="radio" name="address" value="${addr.id}" ${idx === 0 ? 'checked' : ''}>
${addr.name} (${addr.address})
</label>
</div>
`).join('')}
`;
break;
case 'get_payment_page':
actionTitle = '💰 前往支付';
actionDetails = `
<div><strong>订单摘要:</strong></div>
<div>总金额:¥${data.params.total_price || '0.00'}</div>
<div>配送地址:${data.params.address || '请确认地址'}</div>
<div>预计送达:${data.params.estimated_delivery || '30分钟'}</div>
`;
break;
}
actionCard.innerHTML = `
<h3>${actionTitle}</h3>
<div class="action-details">${actionDetails}</div>
<div class="action-buttons">
<button onclick="assistant.confirmAction(getConfirmedData())" class="btn btn-confirm">确认</button>
<button onclick="assistant.modifyAction()" class="btn btn-modify">修改</button>
</div>
`;
this.elements.statusContainer.appendChild(actionCard);
this.elements.statusContainer.scrollTop = this.elements.statusContainer.scrollHeight;
}
/**
* 移除操作预览
*/
removeActionPreview() {
const existingCard = document.getElementById('actionPreviewCard');
if (existingCard) {
existingCard.remove();
}
}
/**
* 显示支付页面
*/
showPaymentPage(url, token) {
// 构建完整的支付URL(包含token)
const paymentUrl = `${url}${url.includes('?') ? '&' : '?'}token=${token}&embedded=true`;
// 更新iframe源
this.elements.paymentFrame.src = paymentUrl;
// 显示支付容器
this.elements.paymentSection.style.display = 'block';
// 滚动到支付区域
setTimeout(() => {
this.elements.paymentSection.scrollIntoView({ behavior: 'smooth' });
}, 500);
}
/**
* 隐藏支付页面
*/
hidePaymentSection() {
this.elements.paymentSection.style.display = 'none';
this.elements.paymentFrame.src = '';
}
/**
* 获取用户位置(模拟)
*/
async getUserLocation() {
return new Promise((resolve) => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
latitude: position.coords.latitude,
longitude: position.coords.longitude,
accuracy: position.coords.accuracy
});
},
() => {
// 获取失败时使用默认位置
resolve({
latitude: 39.9042,
longitude: 116.4074,
accuracy: 1000,
city: '北京市'
});
},
{ timeout: 5000 }
);
} else {
resolve({
latitude: 39.9042,
longitude: 116.4074,
city: '北京市'
});
}
});
}
/**
* 更新UI状态
*/
updateUIState() {
const hasText = this.elements.userInput.value.trim().length > 0;
this.elements.sendButton.disabled = !hasText || this.isProcessing;
this.elements.userInput.disabled = this.isProcessing;
if (this.isProcessing) {
this.elements.userInput.placeholder = '智能体正在处理中,请稍候...';
} else {
this.elements.userInput.placeholder = '告诉我你想吃什么?例如:帮我点一份麦当劳巨无霸套餐,送到XX大厦,帮我到付款界面';
}
}
/**
* 滚动到底部
*/
scrollToBottom() {
requestAnimationFrame(() => {
this.elements.messagesContainer.scrollTop = this.elements.messagesContainer.scrollHeight;
});
}
}
/**
* 获取确认数据(用于表单数据)
*/
function getConfirmedData() {
const actionCard = document.getElementById('actionPreviewCard');
if (!actionCard) return null;
// 检查是否有地址选择
const addressRadio = actionCard.querySelector('input[name="address"]:checked');
if (addressRadio) {
return { address_id: addressRadio.value };
}
return null;
}
// 创建助手实例
const assistant = new AIFoodOrderingAssistant();
// 使助手全局可用(用于按钮点击事件)
window.assistant = assistant;
window.hidePaymentSection = () => assistant.hidePaymentSection();
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI外卖助手</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.app-container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 24px;
text-align: center;
}
.header h1 {
font-size: 28px;
margin-bottom: 8px;
font-weight: 600;
}
.header p {
opacity: 0.9;
font-size: 14px;
}
.main-content {
display: flex;
height: 70vh;
}
/* 左侧:对话区 */
.chat-section {
flex: 3;
border-right: 1px solid #e5e7eb;
display: flex;
flex-direction: column;
}
.chat-header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
font-weight: 600;
color: #374151;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 20px;
background: #f9fafb;
}
.message {
margin-bottom: 20px;
animation: fadeIn 0.3s ease;
}
.user-message {
text-align: right;
}
.agent-message {
text-align: left;
}
.message-bubble {
display: inline-block;
max-width: 70%;
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
line-height: 1.4;
}
.user-message .message-bubble {
background: #3b82f6;
color: white;
border-bottom-right-radius: 4px;
}
.agent-message .message-bubble {
background: white;
color: #374151;
border: 1px solid #e5e7eb;
border-bottom-left-radius: 4px;
}
.message-typing {
display: inline-flex;
align-items: center;
padding: 12px 16px;
background: white;
border: 1px solid #e5e7eb;
border-radius: 18px;
border-bottom-left-radius: 4px;
}
.typing-dots {
display: flex;
gap: 4px;
}
.typing-dots span {
width: 6px;
height: 6px;
background: #9ca3af;
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
.typing-dots span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-dots span:nth-child(3) {
animation-delay: 0.4s;
}
/* 右侧:智能体状态和操作区 */
.status-section {
flex: 2;
display: flex;
flex-direction: column;
background: white;
}
.status-header {
padding: 16px;
border-bottom: 1px solid #e5e7eb;
font-weight: 600;
color: #374151;
}
.status-container {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.status-card {
background: #f3f4f6;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
border-left: 4px solid #3b82f6;
animation: slideIn 0.3s ease;
}
.status-card h4 {
color: #374151;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
}
.status-card p {
color: #6b7280;
font-size: 13px;
margin-bottom: 4px;
}
.status-card .timestamp {
font-size: 11px;
color: #9ca3af;
margin-top: 8px;
}
/* 操作预览卡片 */
.action-preview-card {
background: #fef3c7;
border: 2px solid #f59e0b;
border-radius: 12px;
padding: 20px;
margin-bottom: 20px;
animation: pulse 2s infinite;
}
.action-preview-card h3 {
color: #92400e;
margin-bottom: 12px;
font-size: 16px;
}
.action-details {
background: white;
border-radius: 8px;
padding: 12px;
margin-bottom: 16px;
font-size: 14px;
}
.action-buttons {
display: flex;
gap: 12px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
}
.btn-confirm {
background: #10b981;
color: white;
flex: 1;
}
.btn-confirm:hover {
background: #059669;
}
.btn-modify {
background: #f3f4f6;
color: #374151;
flex: 1;
}
.btn-modify:hover {
background: #e5e7eb;
}
/* 输入区 */
.input-section {
padding: 20px;
border-top: 1px solid #e5e7eb;
background: white;
}
.input-container {
display: flex;
gap: 12px;
}
#userInput {
flex: 1;
padding: 14px 20px;
border: 2px solid #e5e7eb;
border-radius: 12px;
font-size: 16px;
transition: border-color 0.2s ease;
}
#userInput:focus {
outline: none;
border-color: #3b82f6;
}
.btn-send {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
padding: 0 28px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s ease;
}
.btn-send:hover:not(:disabled) {
transform: translateY(-2px);
}
.btn-send:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 支付页面容器 */
.payment-container {
margin-top: 20px;
border: 2px dashed #10b981;
border-radius: 12px;
padding: 20px;
background: #f0fdf4;
animation: fadeIn 0.5s ease;
}
.payment-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.payment-header h3 {
color: #065f46;
font-size: 16px;
}
.payment-frame {
width: 100%;
height: 400px;
border: 1px solid #d1d5db;
border-radius: 8px;
overflow: hidden;
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-4px); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
/* 会话管理 */
.session-info {
display: flex;
align-items: center;
gap: 12px;
font-size: 12px;
color: #9ca3af;
margin-top: 8px;
}
.session-id {
background: rgba(255,255,255,0.2);
padding: 2px 8px;
border-radius: 10px;
font-family: monospace;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
flex-direction: column;
height: auto;
}
.status-section {
height: 300px;
}
body {
padding: 10px;
}
.message-bubble {
max-width: 85%;
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="header">
<h1>🤖 AI外卖助手</h1>
<p>智能点餐,一句话完成从选餐到支付的全流程</p>
<div class="session-info">
<span>会话ID: <span id="sessionId" class="session-id">loading...</span></span>
<span>状态: <span id="connectionStatus">🟡 连接中...</span></span>
</div>
</div>
<div class="main-content">
<!-- 左侧:对话区 -->
<div class="chat-section">
<div class="chat-header">
对话记录
</div>
<div class="messages-container" id="messagesContainer">
<!-- 消息会动态添加到这里 -->
</div>
</div>
<!-- 右侧:智能体状态区 -->
<div class="status-section">
<div class="status-header">
智能体状态与操作
</div>
<div class="status-container" id="statusContainer">
<!-- 状态卡片和操作预览会动态添加到这里 -->
</div>
</div>
</div>
<!-- 支付页面容器(初始隐藏) -->
<div id="paymentSection" class="payment-container" style="display: none;">
<div class="payment-header">
<h3>💰 支付页面</h3>
<button onclick="hidePaymentSection()" class="btn" style="background: #ef4444; color: white; padding: 8px 16px;">关闭</button>
</div>
<iframe id="paymentFrame" class="payment-frame" src="" title="支付页面"></iframe>
</div>
<!-- 输入区 -->
<div class="input-section">
<div class="input-container">
<input
type="text"
id="userInput"
placeholder="告诉我你想吃什么?例如:帮我点一份麦当劳巨无霸套餐,送到XX大厦,帮我到付款界面"
autocomplete="off"
>
<button id="sendButton" class="btn-send" onclick="sendMessage()">发送</button>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>
四、配套的Node.js Express服务器示例
// server.js - 简化的服务器示例
const express = require('express');
const cors = require('cors');
const { v4: uuidv4 } = require('uuid');
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static('public')); // 静态文件服务
// 存储会话状态(生产环境中应该使用Redis)
const sessions = new Map();
// 创建新会话
app.post('/api/agent/session', (req, res) => {
const sessionId = uuidv4();
sessions.set(sessionId, {
created_at: new Date(),
messages: [],
state: {
step: 'initial',
selected_restaurant: null,
cart_id: null,
address_id: null
}
});
res.json({ session_id: sessionId });
});
// 处理用户消息
app.post('/api/agent/process', (req, res) => {
const { session_id, message, context } = req.body;
if (!sessions.has(session_id)) {
return res.status(404).json({ error: '会话不存在' });
}
// 这里在实际应用中应该触发智能体处理流程
// 我们只是模拟一个响应
res.json({
success: true,
message_id: uuidv4()
});
});
// SSE流端点
app.get('/api/agent/stream', (req, res) => {
const sessionId = req.query.session_id;
if (!sessionId) {
return res.status(400).json({ error: '缺少会话ID' });
}
// 设置SSE头
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*'
});
// 发送初始状态
res.write(`event: status\ndata: ${JSON.stringify({ status: 'connected' })}\n\n`);
// 模拟一些事件(实际应用中这些事件来自智能体处理流程)
const sendSimulatedEvents = () => {
setTimeout(() => {
res.write(`event: status\ndata: ${JSON.stringify({
status: 'thinking',
details: '正在分析您的需求...'
})}\n\n`);
}, 1000);
setTimeout(() => {
res.write(`event: status\ndata: ${JSON.stringify({
status: 'querying_menu',
details: '查询麦当劳菜单中...'
})}\n\n`);
}, 2000);
setTimeout(() => {
res.write(`event: message\ndata: ${JSON.stringify({
content: '找到"麦当劳(国贸店)",有巨无霸套餐,价格38元。确认加入购物车吗?'
})}\n\n`);
}, 3000);
setTimeout(() => {
res.write(`event: action_preview\ndata: ${JSON.stringify({
action_id: uuidv4(),
action: 'add_to_cart',
params: {
restaurant: '麦当劳(国贸店)',
items: [
{ name: '巨无霸套餐', quantity: 1, price: 38 }
],
need_confirm: true
}
})}\n\n`);
}, 3500);
};
// 开始发送模拟事件
sendSimulatedEvents();
// 保持连接
req.on('close', () => {
console.log(`客户端断开连接: ${sessionId}`);
res.end();
});
});
// 确认操作
app.post('/api/agent/confirm-action', (req, res) => {
const { session_id, action_id, user_confirmation } = req.body;
// 在实际应用中,这里会更新会话状态并继续智能体流程
res.json({
success: true,
message: '操作已确认'
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
```