Compare commits

...

2 Commits

Author SHA1 Message Date
108a496dab feat: AI回复流式输出
- 使用 SSE 流式调用智谱 API
- 实时显示 AI 回复内容
- 添加闪烁光标动画效果
- 移除打字指示器,改为实时内容显示
2026-04-25 17:06:04 +08:00
63c597248f fix: 更换端口 19019 → 19021 2026-04-25 17:03:39 +08:00
3 changed files with 71 additions and 44 deletions

View File

@@ -3,7 +3,7 @@
"version": "1.0.0",
"description": "AI对话助手移动端应用",
"scripts": {
"dev": "npx http-server -p 19019 -c-1",
"dev": "npx http-server -p 19021 -c-1",
"build:android": "npx cap sync android",
"build:ios": "npx cap sync ios",
"open:android": "npx cap open android",

View File

@@ -1,5 +1,5 @@
// AI助手 - 前端应用
// 使用智谱 GLM-4.5-Air 模型
// 使用智谱 GLM-4.5-Air 模型(流式输出)
const CONFIG = {
apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions',
@@ -55,7 +55,7 @@ function sendQuickMessage(text) {
sendMessage();
}
// 发送消息
// 发送消息(流式输出)
async function sendMessage() {
const text = userInput.value.trim();
if (!text || isLoading) return;
@@ -72,10 +72,19 @@ async function sendMessage() {
// 显示加载状态
isLoading = true;
sendBtn.disabled = true;
showTypingIndicator();
// 创建 AI 消息容器(流式填充)
const aiMessageIndex = messages.length;
messages.push({ role: 'assistant', content: '' });
renderMessages();
// 获取最后一条消息的 DOM 元素
const lastMessageEl = messagesDiv.lastElementChild;
const contentEl = lastMessageEl.querySelector('.message-content');
contentEl.innerHTML = '<span class="streaming-cursor">▌</span>';
try {
// 调用 API
// 调用 API(流式)
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
@@ -84,12 +93,12 @@ async function sendMessage() {
},
body: JSON.stringify({
model: CONFIG.model,
messages: messages.map(m => ({
messages: messages.slice(0, aiMessageIndex).map(m => ({
role: m.role,
content: m.content
})),
max_tokens: CONFIG.maxTokens,
stream: false
stream: true // 开启流式输出
})
});
@@ -97,24 +106,53 @@ async function sendMessage() {
throw new Error(`API 错误: ${response.status}`);
}
const data = await response.json();
// 添加 AI 回复
if (data.choices && data.choices[0]) {
const assistantMessage = data.choices[0].message.content;
messages.push({ role: 'assistant', content: assistantMessage });
// 处理流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 数据
const lines = buffer.split('\n');
buffer = lines.pop() || ''; // 保留未完成的行
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.slice(6).trim();
if (jsonStr === '[DONE]') continue;
try {
const data = JSON.parse(jsonStr);
if (data.choices && data.choices[0]?.delta?.content) {
// 追加内容
messages[aiMessageIndex].content += data.choices[0].delta.content;
// 更新显示(带光标)
contentEl.innerHTML = formatContent(messages[aiMessageIndex].content) + '<span class="streaming-cursor">▌</span>';
scrollToBottom();
}
} catch (e) {
// 忽略解析错误
}
}
}
}
// 完成,移除光标
contentEl.innerHTML = formatContent(messages[aiMessageIndex].content);
} catch (error) {
console.error('Error:', error);
messages.push({
role: 'assistant',
content: `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`
});
messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`;
contentEl.innerHTML = formatContent(messages[aiMessageIndex].content);
} finally {
isLoading = false;
sendBtn.disabled = false;
hideTypingIndicator();
renderMessages();
saveHistory();
}
}
@@ -140,6 +178,8 @@ function renderMessages() {
// 格式化内容(简单处理代码块)
function formatContent(text) {
if (!text) return '';
// 处理代码块
text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
// 处理行内代码
@@ -149,31 +189,6 @@ function formatContent(text) {
return text;
}
// 显示打字指示器
function showTypingIndicator() {
const indicator = document.createElement('div');
indicator.id = 'typingIndicator';
indicator.className = 'message assistant';
indicator.innerHTML = `
<div class="message-avatar">🤖</div>
<div class="message-content">
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
</div>
`;
messagesDiv.appendChild(indicator);
scrollToBottom();
}
// 隐藏打字指示器
function hideTypingIndicator() {
const indicator = document.getElementById('typingIndicator');
if (indicator) {
indicator.remove();
}
}
// 滚动到底部
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;

View File

@@ -297,6 +297,18 @@ body {
padding: 8px;
}
/* 流式输出光标 */
.streaming-cursor {
animation: blink 1s infinite;
color: var(--primary);
font-weight: bold;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* 响应式 */
@media (min-width: 768px) {
.message-content {