1. 项目概述
1.1 项目背景
本项目实现了一个基于Web的AI对话机器人,集成于博客文章页面,为读者提供智能问答服务。机器人能够阅读文章内容,理解用户提问,并基于文章上下文给出精准回答。
效果预览
![]()
1.2 核心功能特性
- 智能问答:基于文章内容的上下文问答
- 打字机效果:AI回复采用逐字显示效果,提升用户体验
- 暂停/继续:支持在生成过程中暂停和继续
- 历史记录:本地存储对话历史,刷新页面后保留
- 快捷问题:预设常见问题,一键提问
- 响应式设计:适配桌面和移动端设备
2. 技术选型与架构设计
2.1 技术栈选型
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 前端框架 | 原生HTML5 + CSS3 + JavaScript | 轻量级,无需额外框架依赖 |
| Markdown解析 | Marked.js | 支持GitHub风格Markdown |
| 代码高亮 | Highlight.js | 支持多种编程语言语法高亮 |
| 图标库 | Font Awesome | 丰富的图标资源 |
| 后端API | RESTful API | 与AI服务通信 |
| 数据存储 | LocalStorage | 本地持久化存储对话历史 |
2.2 系统架构图
┌─────────────────────────────────────────────────────────────┐
│ 用户界面层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 聊天窗口 │ │ 输入框 │ │ 快捷问题 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 交互逻辑层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 状态管理 │ │ 事件处理 │ │ 动画控制 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 数据处理层 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Markdown解析 │ │ 代码高亮 │ │ 历史记录管理 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 服务接口层 │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ AI问答API │ │ 文章数据获取 │ │
│ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.3 状态机设计
AI生成过程采用状态机管理,确保流程清晰可控:
┌─────────┐ 发送消息 ┌───────────┐
│ idle │ ──────────────▶ │ thinking │
│ (空闲) │ │ (思考中) │
└─────────┘ └─────┬─────┘
▲ │
│ │ API响应
│ ▼
│ ┌───────────┐
│ │generating │
│ │(生成中) │
│ └─────┬─────┘
│ │
│ 完成 │ 暂停
│◀───────────────────────────┤
│ ▼
│ ┌───────────┐
└───────────────────────│ paused │
│ (已暂停) │
└───────────┘
3. 核心功能模块实现
3.1 状态管理模块
状态管理是整个系统的核心,负责协调各个功能模块的工作流程。
// AI生成状态管理
var aiGenerationState = 'idle'; // 'idle' | 'thinking' | 'generating' | 'paused' | 'completed'
var typingController = null; // 打字机效果控制器
var currentAIMessageDiv = null; // 当前AI消息DOM元素
var currentMessageContent = ''; // 当前消息内容
var resumeCallback = null; // 恢复生成回调函数
var pendingResponse = null; // 待处理的API响应数据
var hasNewConversationStarted = false; // 是否已开始新对话
3.2 打字机效果模块
打字机效果是提升用户体验的关键功能,实现逐字显示AI回复内容。
核心算法:
function typeWriter() {
if (typingController && typingController.stopped) return;
if (typingController && typingController.paused) {
currentMessageContent = currentText;
return;
}
if (index < text.length) {
var char = text.charAt(index);
currentText += char;
currentMessageContent = currentText;
index++;
// 根据字符类型调整速度
var currentSpeed = speed;
if (',。!?;:,.!?;:\n'.indexOf(char) !== -1) {
currentSpeed = speed * 2; // 标点符号停顿更久
}
// 定期渲染Markdown
if (index - lastRenderIndex >= renderInterval ||
char === '\n' || char === '.' || char === '。' ||
char === '!' || char === '!' || char === '?' || char === '?') {
renderMarkdownWithCursor(currentText, messageContentDiv);
lastRenderIndex = index;
}
scrollToBottom();
typingController.timeoutId = setTimeout(typeWriter, currentSpeed);
} else {
// 打字完成
typingController.stopped = true;
aiGenerationState = 'completed';
finalizeMessage();
}
}
3.3 暂停/继续功能模块
暂停功能是用户体验的重要组成部分,允许用户在生成过程中随时暂停。
暂停逻辑:
function pauseGeneration() {
if (typingController) {
typingController.paused = true;
if (typingController.timeoutId) {
clearTimeout(typingController.timeoutId);
}
}
var wasThinking = aiGenerationState === 'thinking';
aiGenerationState = 'paused';
isLoading = false;
updateInputState();
// 处理思考状态暂停的特殊情况
if (wasThinking && currentAIMessageDiv) {
convertLoadingToPausedMessage();
}
}
继续逻辑:
function resumeGeneration() {
// 检查是否有待处理的响应
if (pendingResponse) {
processPendingResponse();
return;
}
aiGenerationState = 'generating';
updateInputState();
// 执行恢复回调
if (resumeCallback) {
resumeCallback();
}
}
3.4 Markdown解析与代码高亮模块
支持Markdown格式回复,包括代码块、列表、标题等。
// 配置Marked.js解析器
function configureMarked() {
marked.setOptions({
breaks: true, // 支持GitHub风格的换行
gfm: true, // 启用GitHub风格的Markdown
headerIds: false, // 不生成标题ID
mangle: false, // 不转义内联HTML
sanitize: false, // 不进行HTML转义
smartLists: true, // 智能列表
smartypants: true, // 智能标点
xhtml: false // 不强制XHTML
});
}
// 应用代码高亮
function applyCodeHighlight(element) {
if (typeof hljs === 'undefined') return;
var codeBlocks = element.querySelectorAll('pre code');
codeBlocks.forEach(function(block) {
block.classList.add('hljs');
hljs.highlightElement(block);
});
}
3.5 历史记录管理模块
使用LocalStorage实现对话历史的本地持久化存储。
// 保存对话历史
function saveChatHistory() {
try {
var data = {
messages: messages,
timestamp: new Date().toISOString()
};
localStorage.setItem(storageKey, JSON.stringify(data));
} catch (e) {
console.error('保存对话历史失败:', e);
}
}
// 加载对话历史
function loadChatHistory() {
try {
var data = localStorage.getItem(storageKey);
if (data) {
var parsed = JSON.parse(data);
messages = parsed.messages || [];
renderMessages();
}
} catch (e) {
console.error('加载对话历史失败:', e);
}
}
4. 关键技术难点与解决方案
4.1 打字机效果与Markdown解析的同步
难点: 打字机效果逐字显示内容,但Markdown需要完整内容才能正确解析。
解决方案:
- 采用增量渲染策略,每输入N个字符或遇到特定标点时渲染一次
- 使用临时容器进行渲染,避免频繁操作DOM
- 打字完成后进行最终完整渲染
// 定期渲染Markdown
if (index - lastRenderIndex >= renderInterval ||
char === '\n' || char === '.' || char === '。') {
renderMarkdownWithCursor(currentText, messageContentDiv);
lastRenderIndex = index;
}
4.2 暂停状态的持久化
难点: 用户刷新页面后,暂停状态需要保留,且能正确恢复。
解决方案:
- 将暂停消息保存到历史记录,标记
paused: true - 保存待处理的API响应数据
pendingResponse - 页面加载时根据标记恢复暂停状态UI
// 保存暂停消息
var pausedMessage = {
role: 'ai',
content: '用户手动停止',
time: new Date().toISOString(),
paused: true,
pendingResponse: pendingResponse
};
messages.push(pausedMessage);
saveChatHistory();
4.3 多状态切换的稳定性
难点: 在thinking、generating、paused等状态间切换时,容易出现状态不一致。
解决方案:
- 采用单一状态变量
aiGenerationState管理所有状态 - 所有状态变更通过
updateInputState()统一更新UI - 清理状态时同步清理所有相关变量
// 状态变更统一入口
function updateInputState() {
switch(aiGenerationState) {
case 'thinking':
// 禁用暂停按钮
break;
case 'generating':
// 启用暂停按钮
break;
case 'paused':
// 显示发送按钮
break;
}
}
4.4 代码块复制功能
难点: 需要为代码块添加复制按钮,且不影响原有样式。
解决方案:
- 使用相对定位在代码块右上角添加复制按钮
- 悬停时显示按钮,保持界面简洁
- 点击后显示成功反馈
.ai-code-block {
position: relative;
}
.ai-code-copy-btn {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
transition: opacity 0.2s;
}
.ai-code-block:hover .ai-code-copy-btn {
opacity: 1;
}
5. 代码实现示例
5.1 完整的对话管理类
/**
* AI对话管理器
* 负责管理对话状态、消息渲染、历史记录等功能
*/
var AIChatManager = (function() {
// 私有变量
var config = {
storageKey: 'ai_chat_history_',
maxHistoryLength: 100,
typingSpeed: 20
};
// 状态变量
var state = {
isOpen: false,
isLoading: false,
generationState: 'idle',
messages: [],
typingController: null,
currentMessageDiv: null
};
// DOM元素引用
var elements = {};
/**
* 初始化函数
* @param {Object} options 配置选项
*/
function init(options) {
// 合并配置
Object.assign(config, options);
// 获取DOM元素
elements.widget = document.getElementById('aiChatWidget');
elements.messages = document.getElementById('aiChatMessages');
elements.input = document.getElementById('aiChatInput');
elements.sendBtn = document.getElementById('aiChatSendBtn');
// 绑定事件
bindEvents();
// 加载历史记录
loadHistory();
}
/**
* 发送消息
* @param {string} content 消息内容
*/
function sendMessage(content) {
if (!content || state.isLoading) return;
// 添加用户消息
addMessage({
role: 'user',
content: content,
time: new Date().toISOString()
});
// 清空输入框
elements.input.value = '';
// 调用AI接口
callAIAPI(content);
}
/**
* 添加消息到界面
* @param {Object} message 消息对象
* @param {boolean} withTyping 是否使用打字机效果
*/
function addMessage(message, withTyping) {
// 保存到历史
state.messages.push(message);
saveHistory();
if (withTyping) {
renderWithTyping(message);
} else {
renderMessage(message);
}
}
/**
* 渲染带打字机效果的消息
* @param {Object} message 消息对象
*/
function renderWithTyping(message) {
var messageDiv = createMessageElement(message.role);
var contentDiv = messageDiv.querySelector('.message-content');
elements.messages.appendChild(messageDiv);
state.currentMessageDiv = messageDiv;
// 创建打字机控制器
state.typingController = {
paused: false,
stopped: false,
timeoutId: null
};
// 开始打字效果
typeWriter(message.content, contentDiv, 0);
}
/**
* 打字机效果实现
* @param {string} text 完整文本
* @param {HTMLElement} element 目标元素
* @param {number} index 当前索引
*/
function typeWriter(text, element, index) {
var controller = state.typingController;
if (controller.stopped || controller.paused) return;
if (index >= text.length) {
finalizeTyping();
return;
}
// 显示当前字符
element.textContent += text.charAt(index);
// 计算延迟
var delay = config.typingSpeed;
var char = text.charAt(index);
if (',。!?;:,.!?;:\n'.indexOf(char) !== -1) {
delay *= 2;
}
// 继续下一个字符
controller.timeoutId = setTimeout(function() {
typeWriter(text, element, index + 1);
}, delay);
}
/**
* 暂停生成
*/
function pauseGeneration() {
if (state.typingController) {
state.typingController.paused = true;
clearTimeout(state.typingController.timeoutId);
}
state.generationState = 'paused';
updateUI();
}
/**
* 继续生成
*/
function resumeGeneration() {
state.generationState = 'generating';
updateUI();
if (state.typingController) {
state.typingController.paused = false;
// 恢复打字
var contentDiv = state.currentMessageDiv.querySelector('.message-content');
var currentText = contentDiv.textContent;
var index = currentText.length;
typeWriter(state.currentMessageContent, contentDiv, index);
}
}
/**
* 保存历史记录到LocalStorage
*/
function saveHistory() {
try {
var data = {
messages: state.messages.slice(-config.maxHistoryLength),
timestamp: new Date().toISOString()
};
localStorage.setItem(config.storageKey, JSON.stringify(data));
} catch (e) {
console.error('保存历史记录失败:', e);
}
}
/**
* 从LocalStorage加载历史记录
*/
function loadHistory() {
try {
var data = localStorage.getItem(config.storageKey);
if (data) {
var parsed = JSON.parse(data);
state.messages = parsed.messages || [];
renderAllMessages();
}
} catch (e) {
console.error('加载历史记录失败:', e);
}
}
/**
* 更新UI状态
*/
function updateUI() {
var btn = elements.sendBtn;
var input = elements.input;
switch(state.generationState) {
case 'thinking':
btn.disabled = true;
btn.innerHTML = '<i class="fas fa-pause"></i>';
btn.title = '请稍候...';
input.disabled = true;
break;
case 'generating':
btn.disabled = false;
btn.innerHTML = '<i class="fas fa-pause"></i>';
btn.title = '暂停生成';
input.disabled = true;
break;
case 'paused':
btn.disabled = !input.value.trim();
btn.innerHTML = '<i class="fas fa-paper-plane"></i>';
btn.title = '发送消息';
input.disabled = false;
break;
default:
btn.disabled = !input.value.trim();
btn.innerHTML = '<i class="fas fa-paper-plane"></i>';
btn.title = '发送消息';
input.disabled = false;
}
}
/**
* 绑定事件处理
*/
function bindEvents() {
// 发送按钮点击
elements.sendBtn.addEventListener('click', function() {
if (state.generationState === 'generating') {
pauseGeneration();
} else if (state.generationState === 'paused') {
sendMessage(elements.input.value.trim());
} else {
sendMessage(elements.input.value.trim());
}
});
// 输入框回车发送
elements.input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage(elements.input.value.trim());
}
});
// 输入框内容变化
elements.input.addEventListener('input', function() {
updateUI();
});
}
// 公开API
return {
init: init,
sendMessage: sendMessage,
pauseGeneration: pauseGeneration,
resumeGeneration: resumeGeneration,
clearHistory: function() {
state.messages = [];
saveHistory();
}
};
})();
5.2 CSS样式关键代码
/* 聊天窗口容器 */
.ai-chat-window {
position: fixed;
bottom: 100px;
right: 20px;
width: 380px;
height: 600px;
background: white;
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
z-index: 9998;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 消息气泡 */
.ai-chat-message {
display: flex;
margin-bottom: 16px;
animation: messageSlideIn 0.3s ease;
}
.ai-chat-message.user {
justify-content: flex-end;
}
.ai-chat-message.ai {
justify-content: flex-start;
}
.ai-chat-message-content {
max-width: 80%;
padding: 12px 16px;
border-radius: 16px;
font-size: 14px;
line-height: 1.6;
}
.ai-chat-message.user .ai-chat-message-content {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border-bottom-right-radius: 4px;
}
.ai-chat-message.ai .ai-chat-message-content {
background: white;
color: #333;
border-bottom-left-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
/* 打字机效果光标 */
.ai-chat-message-content.typing::after {
content: '|';
animation: blink 1s infinite;
margin-left: 2px;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* 继续生成按钮 */
.ai-chat-resume-text-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 6px 16px;
border: none;
border-radius: 16px;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
color: white;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
font-size: 12px;
font-weight: 600;
letter-spacing: 0.5px;
box-shadow: 0 3px 10px rgba(16, 185, 129, 0.35);
}
.ai-chat-resume-text-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(16, 185, 129, 0.45);
}
/* 消息操作按钮 */
.ai-chat-message-actions {
display: flex;
align-items: center;
gap: 6px;
margin-top: 8px;
}
.ai-chat-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border: none;
border-radius: 50%;
background: rgba(0, 0, 0, 0.04);
color: #9ca3af;
cursor: pointer;
transition: all 0.2s ease;
font-size: 12px;
}
.ai-chat-action-btn:hover {
background: rgba(102, 126, 234, 0.15);
color: #667eea;
transform: translateY(-1px);
}
6. 测试验证方法
6.1 功能测试用例
| 测试项 | 测试步骤 | 预期结果 |
|---|---|---|
| 发送消息 | 输入问题,点击发送 | 消息显示在对话区,AI开始回复 |
| 打字机效果 | 观察AI回复过程 | 文字逐字显示,有光标闪烁 |
| 暂停功能 | 在生成过程中点击暂停按钮 | 生成停止,显示"用户手动停止"和继续生成按钮 |
| 继续功能 | 点击继续生成按钮 | 从暂停处继续生成 |
| 新对话覆盖 | 暂停后发送新问题 | 旧继续生成按钮消失,只回复新问题 |
| 历史记录 | 刷新页面 | 对话历史保留,暂停状态恢复 |
| 代码高亮 | 询问包含代码的问题 | 代码块正确高亮显示 |
| Markdown解析 | 询问包含列表、标题的问题 | Markdown格式正确渲染 |
6.2 性能测试
// 性能测试代码
function performanceTest() {
console.time('消息渲染');
for (var i = 0; i < 100; i++) {
renderMessage({
role: 'ai',
content: '测试消息内容',
time: new Date().toISOString()
});
}
console.timeEnd('消息渲染');
}
// 内存泄漏检测
function memoryTest() {
var initialMemory = performance.memory.usedJSHeapSize;
// 执行大量操作
for (var i = 0; i < 1000; i++) {
addMessage({role: 'user', content: 'test'}, false);
}
// 强制垃圾回收后检查
setTimeout(function() {
var finalMemory = performance.memory.usedJSHeapSize;
console.log('内存增长:', (finalMemory - initialMemory) / 1024 / 1024, 'MB');
}, 5000);
}
6.3 兼容性测试
- 浏览器兼容性:Chrome、Firefox、Safari、Edge最新两个版本
- 移动端适配:iOS Safari、Android Chrome
- 响应式测试:320px - 1920px各种屏幕尺寸
7. 性能优化策略
7.1 渲染优化
- 虚拟滚动:当消息数量超过100条时,只渲染可视区域的消息
- 防抖处理:输入框resize事件使用防抖函数
- 批量DOM操作:使用DocumentFragment批量插入消息
// 批量渲染消息
function batchRenderMessages(messages) {
var fragment = document.createDocumentFragment();
messages.forEach(function(msg) {
var div = createMessageElement(msg);
fragment.appendChild(div);
});
messagesContainer.appendChild(fragment);
}
7.2 存储优化
- 历史记录限制:最多保存100条消息
- 数据压缩:使用LZ-string等库压缩存储数据
- 定期清理:超过30天的历史记录自动清理
// 清理过期历史记录
function cleanupOldHistory() {
var keys = Object.keys(localStorage);
keys.forEach(function(key) {
if (key.startsWith('ai_chat_history_')) {
var data = JSON.parse(localStorage.getItem(key));
var days = (Date.now() - new Date(data.timestamp)) / (1000 * 60 * 60 * 24);
if (days > 30) {
localStorage.removeItem(key);
}
}
});
}
7.3 网络优化
- 请求合并:短时间内多个问题合并为一个请求
- 超时处理:API请求设置30秒超时
- 重试机制:网络错误时自动重试3次
8. 未来功能扩展方向
8.1 短期规划(1-3个月)
- 语音输入:集成Web Speech API实现语音提问
- 图片理解:支持用户上传图片进行问答
- 多轮对话优化:改进上下文理解能力
- 导出功能:支持导出对话记录为PDF或Markdown
8.2 中期规划(3-6个月)
- 多语言支持:支持中英文等多语言切换
- 个性化配置:允许用户自定义机器人名称、头像
- 对话分享:支持分享对话链接
- 数据分析:统计常见问题,优化文章推荐
8.3 长期规划(6个月以上)
- 知识库集成:连接外部知识库,扩展回答范围
- 多模型支持:支持切换不同AI模型
- 插件系统:开放插件接口,支持第三方扩展
- 实时协作:支持多人同时与AI对话
附录
A. 项目文件结构
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/blog/
│ │ │ ├── controller/
│ │ │ │ └── AIChatController.java
│ │ │ ├── service/
│ │ │ │ └── AIChatService.java
│ │ │ └── model/
│ │ │ └── ChatMessage.java
│ │ └── resources/
│ │ └── templates/
│ │ └── themes/
│ │ └── pinghsu/
│ │ └── post.html
│ └── test/
│ └── java/
│ └── com/blog/
│ └── AIChatTest.java
├── docs/
│ └── AI对话机器人技术实现文档.md
└── README.md
B. API接口文档
发送对话请求
POST /api/ai-chat
Content-Type: application/json
{
"articleId": "12345",
"articleTitle": "文章标题",
"articleContent": "文章内容",
"question": "用户问题",
"history": [
{"role": "user", "content": "历史问题"},
{"role": "ai", "content": "历史回答"}
]
}
响应格式
{
"success": true,
"answer": "AI回答内容",
"message": "提示信息"
}

评论区 0