|
|
|
|
@@ -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;
|
|
|
|
|
|