Compare commits

...

3 Commits

Author SHA1 Message Date
3417709a79 feat: 多对话管理 + 历史对话列表
- 新增对话列表页面(首页)
- 每个对话显示标题、消息数、更新时间
- 对话列表支持删除对话
- 新建对话按钮
- 对话标题自动生成(第一条消息摘要)
- 点击对话进入具体对话页面
- 返回按钮回到对话列表
- 数据结构改为 conversations 多对话存储
- 兼容旧数据格式自动转换
2026-04-25 23:05:51 +08:00
3f72306e78 fix: 移除响应式消息宽度限制
- 删除 .message-content max-width: 60% 限制
- 消息宽度由 message-body 控制,更灵活
2026-04-25 17:25:21 +08:00
f06260cf78 fix: 增加用户消息宽度
- 用户消息宽度从 75% 增加到 85%
- AI 消息宽度调整为 80%
- 减少消息行数,显示更紧凑
2026-04-25 17:23:55 +08:00
3 changed files with 512 additions and 277 deletions

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',
@@ -8,11 +8,13 @@ const CONFIG = {
maxTokens: 2048
};
// 对话历史
let messages = [];
// 数据结构
let conversations = []; // 对话列表
let currentConversation = null; // 当前对话
let isLoading = false;
// DOM 元素
const appContainer = document.getElementById('app');
const messagesContainer = document.getElementById('messagesContainer');
const messagesDiv = document.getElementById('messages');
const userInput = document.getElementById('userInput');
@@ -21,20 +23,151 @@ const welcome = document.getElementById('welcome');
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 从本地存储恢复对话
const saved = localStorage.getItem('chat_history');
// 从本地存储加载对话列表
const saved = localStorage.getItem('conversations');
if (saved) {
messages = JSON.parse(saved);
renderMessages();
if (messages.length > 0) {
welcome.style.display = 'none';
}
conversations = JSON.parse(saved);
}
// 聚焦输入框
userInput.focus();
// 显示对话列表页面
showConversationList();
});
// ==================== 对话列表页面 ====================
function showConversationList() {
currentConversation = null;
// 渲染对话列表
const listHtml = `
<div class="conversation-list-page">
<header class="list-header">
<div class="header-title">
<span class="logo">🤖</span>
<h1>AI助手</h1>
</div>
</header>
<div class="list-content">
<button class="new-chat-btn" onclick="createNewConversation()">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
新建对话
</button>
<div class="conversation-list">
${conversations.length === 0
? '<div class="empty-list">暂无对话记录</div>'
: conversations.map(conv => `
<div class="conversation-item" onclick="openConversation('${conv.id}')">
<div class="conv-title">${escapeHtml(conv.title)}</div>
<div class="conv-meta">${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}</div>
<button class="conv-delete-btn" onclick="event.stopPropagation(); deleteConversation('${conv.id}')" title="删除对话">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</button>
</div>
`).join('')
}
</div>
</div>
</div>
`;
appContainer.innerHTML = listHtml;
}
// 创建新对话
function createNewConversation() {
const newConv = {
id: Date.now().toString(),
title: '新对话',
messages: [],
createdAt: Date.now(),
updatedAt: Date.now()
};
conversations.unshift(newConv);
saveConversations();
openConversation(newConv.id);
}
// 打开对话
function openConversation(id) {
currentConversation = conversations.find(c => c.id === id);
if (!currentConversation) {
showConversationList();
return;
}
// 渲染对话页面
const chatHtml = `
<div id="chatPage">
<header class="header">
<button class="back-btn" onclick="showConversationList()">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<div class="header-title">
<span class="logo">🤖</span>
<h1>${escapeHtml(currentConversation.title)}</h1>
</div>
<button class="clear-btn" onclick="clearCurrentChat()" title="清空对话">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
</button>
</header>
<div class="messages-container" id="messagesContainer">
<div class="welcome" id="welcome" style="${currentConversation.messages.length > 0 ? 'display:none' : ''}">
<div class="welcome-icon">👋</div>
<h2>你好我是AI助手</h2>
<p>有什么可以帮助你的吗?</p>
<div class="quick-actions">
<button onclick="sendQuickMessage('介绍一下你自己')">介绍一下你自己</button>
<button onclick="sendQuickMessage('帮我写一段代码')">帮我写代码</button>
<button onclick="sendQuickMessage('解释一个概念')">解释概念</button>
</div>
</div>
<div class="messages" id="messages"></div>
</div>
<div class="input-area">
<textarea
id="userInput"
placeholder="输入消息..."
rows="1"
onkeydown="handleKeyDown(event)"
oninput="autoResize(this)"
></textarea>
<button class="send-btn" onclick="sendMessage()" id="sendBtn">
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
</button>
</div>
</div>
`;
appContainer.innerHTML = chatHtml;
// 重新获取 DOM 元素
messagesContainer = document.getElementById('messagesContainer');
messagesDiv = document.getElementById('messages');
userInput = document.getElementById('userInput');
sendBtn = document.getElementById('sendBtn');
welcome = document.getElementById('welcome');
// 渲染消息
renderMessages();
userInput.focus();
}
// 删除对话
function deleteConversation(id) {
if (!confirm('确定要删除这个对话吗?')) return;
conversations = conversations.filter(c => c.id !== id);
saveConversations();
showConversationList();
}
// ==================== 对话页面 ====================
// 自动调整输入框高度
function autoResize(textarea) {
textarea.style.height = 'auto';
@@ -57,6 +190,8 @@ function sendQuickMessage(text) {
// 发送消息(流式输出)
async function sendMessage() {
if (!currentConversation) return;
const text = userInput.value.trim();
if (!text || isLoading) return;
@@ -64,33 +199,43 @@ async function sendMessage() {
welcome.style.display = 'none';
// 添加用户消息
messages.push({ role: 'user', content: text });
currentConversation.messages.push({ role: 'user', content: text });
// 更新对话标题(第一条用户消息)
if (currentConversation.title === '新对话') {
currentConversation.title = text.slice(0, 30) + (text.length > 30 ? '...' : '');
// 更新标题显示
const titleEl = document.querySelector('.header h1');
if (titleEl) {
titleEl.textContent = currentConversation.title;
}
}
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
userInput.value = '';
autoResize(userInput);
// 调用流式生成
await streamGenerate(messages.length - 1);
await streamGenerate(currentConversation.messages.length - 1);
}
// 流式生成 AI 回复
async function streamGenerate(userMsgIndex) {
// 显示加载状态
isLoading = true;
sendBtn.disabled = true;
// 创建 AI 消息容器(流式填充)
const aiMessageIndex = messages.length;
messages.push({ role: 'assistant', content: '' });
const aiMessageIndex = currentConversation.messages.length;
currentConversation.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流式
const response = await fetch(CONFIG.apiUrl, {
method: 'POST',
headers: {
@@ -99,7 +244,7 @@ async function streamGenerate(userMsgIndex) {
},
body: JSON.stringify({
model: CONFIG.model,
messages: messages.slice(0, aiMessageIndex).map(m => ({
messages: currentConversation.messages.slice(0, aiMessageIndex).map(m => ({
role: m.role,
content: m.content
})),
@@ -112,7 +257,6 @@ async function streamGenerate(userMsgIndex) {
throw new Error(`API 错误: ${response.status}`);
}
// 处理流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
@@ -122,8 +266,6 @@ async function streamGenerate(userMsgIndex) {
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 解析 SSE 数据
const lines = buffer.split('\n');
buffer = lines.pop() || '';
@@ -135,8 +277,8 @@ async function streamGenerate(userMsgIndex) {
try {
const data = JSON.parse(jsonStr);
if (data.choices && data.choices[0]?.delta?.content) {
messages[aiMessageIndex].content += data.choices[0].delta.content;
contentEl.innerHTML = renderMarkdown(messages[aiMessageIndex].content) + '<span class="streaming-cursor">▌</span>';
currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + '<span class="streaming-cursor">▌</span>';
scrollToBottom();
}
} catch (e) {}
@@ -144,45 +286,73 @@ async function streamGenerate(userMsgIndex) {
}
}
// 完成,移除光标
contentEl.innerHTML = renderMarkdown(messages[aiMessageIndex].content);
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} catch (error) {
console.error('Error:', error);
messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`;
contentEl.innerHTML = renderMarkdown(messages[aiMessageIndex].content);
currentConversation.messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`;
contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content);
} finally {
isLoading = false;
sendBtn.disabled = false;
saveHistory();
renderMessages(); // 重新渲染显示操作按钮
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
}
}
// 重新生成 AI 回复
async function regenerate(index) {
if (isLoading || index < 1) return;
if (!currentConversation || isLoading || index < 1) return;
// 找到对应的用户消息AI消息前一条
const userMsgIndex = index - 1;
if (messages[userMsgIndex].role !== 'user') return;
if (currentConversation.messages[userMsgIndex].role !== 'user') return;
// 删除当前 AI 回复
messages.splice(index, 1);
currentConversation.messages.splice(index, 1);
currentConversation.updatedAt = Date.now();
saveConversations();
// 重新生成
await streamGenerate(userMsgIndex);
}
// 删除消息
function deleteMessage(index) {
if (!currentConversation || isLoading) return;
const msg = currentConversation.messages[index];
if (msg.role === 'assistant') {
if (index > 0 && currentConversation.messages[index - 1].role === 'user') {
currentConversation.messages.splice(index - 1, 2);
} else {
currentConversation.messages.splice(index, 1);
}
} else {
if (index < currentConversation.messages.length - 1 && currentConversation.messages[index + 1].role === 'assistant') {
currentConversation.messages.splice(index, 2);
} else {
currentConversation.messages.splice(index, 1);
}
}
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
if (currentConversation.messages.length === 0) {
welcome.style.display = 'block';
}
}
// 复制消息(复制原文)
function copyMessage(index) {
const content = messages[index].content;
if (!currentConversation) return;
const content = currentConversation.messages[index].content;
navigator.clipboard.writeText(content).then(() => {
showToast('已复制到剪贴板');
}).catch(err => {
console.error('复制失败:', err);
// 备用方案
const textarea = document.createElement('textarea');
textarea.value = content;
textarea.style.position = 'fixed';
@@ -195,59 +365,28 @@ function copyMessage(index) {
});
}
// 显示提示
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
// 清空当前对话
function clearCurrentChat() {
if (!currentConversation) return;
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => document.body.removeChild(toast), 300);
}, 2000);
}
// 删除消息
function deleteMessage(index) {
if (isLoading) return;
const msg = messages[index];
if (msg.role === 'assistant') {
// 删除 AI 消息时,同时删除前一条用户消息
if (index > 0 && messages[index - 1].role === 'user') {
messages.splice(index - 1, 2); // 删除两条
} else {
messages.splice(index, 1);
}
} else {
// 删除用户消息时,同时删除后一条 AI 消息
if (index < messages.length - 1 && messages[index + 1].role === 'assistant') {
messages.splice(index, 2);
} else {
messages.splice(index, 1);
}
}
renderMessages();
saveHistory();
// 如果没有消息了,显示欢迎界面
if (messages.length === 0) {
if (confirm('确定要清空当前对话吗?')) {
currentConversation.messages = [];
currentConversation.updatedAt = Date.now();
saveConversations();
renderMessages();
welcome.style.display = 'block';
}
}
// 渲染消息
function renderMessages() {
messagesDiv.innerHTML = messages.map((msg, index) => {
if (!currentConversation) return;
messagesDiv.innerHTML = currentConversation.messages.map((msg, index) => {
const isUser = msg.role === 'user';
const avatar = isUser ? '👤' : '🤖';
const content = renderMarkdown(msg.content);
// 操作按钮(复制按钮放在最前面)
const copyIcon = `<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>`;
const actions = isUser
@@ -281,14 +420,15 @@ function renderMessages() {
scrollToBottom();
}
// ==================== 工具函数 ====================
// 渲染 Markdown
function renderMarkdown(text) {
if (!text) return '';
// 配置 marked
marked.setOptions({
breaks: true, // 支持 GFM 换行
gfm: true // GitHub Flavored Markdown
breaks: true,
gfm: true
});
return marked.parse(text);
@@ -296,24 +436,51 @@ function renderMarkdown(text) {
// 滚动到底部
function scrollToBottom() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
// 保存历史
function saveHistory() {
localStorage.setItem('chat_history', JSON.stringify(messages));
}
// 清空对话
function clearChat() {
if (confirm('确定要清空所有对话记录吗?')) {
messages = [];
messagesDiv.innerHTML = '';
welcome.style.display = 'block';
localStorage.removeItem('chat_history');
if (messagesContainer) {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
}
// 保存对话列表
function saveConversations() {
localStorage.setItem('conversations', JSON.stringify(conversations));
}
// 显示提示
function showToast(message) {
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => document.body.removeChild(toast), 300);
}, 2000);
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 格式化时间
function formatTime(timestamp) {
const date = new Date(timestamp);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
return date.toLocaleDateString('zh-CN');
}
// PWA 注册
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js').catch(() => {});

View File

@@ -9,52 +9,7 @@
<link rel="manifest" href="manifest.json">
</head>
<body>
<div id="app">
<!-- 头部 -->
<header class="header">
<div class="header-title">
<span class="logo">🤖</span>
<h1>AI助手</h1>
</div>
<button class="clear-btn" onclick="clearChat()" title="清空对话">
<svg viewBox="0 0 24 24" width="20" height="20">
<path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>
</svg>
</button>
</header>
<!-- 消息区域 -->
<div class="messages-container" id="messagesContainer">
<div class="welcome" id="welcome">
<div class="welcome-icon">👋</div>
<h2>你好我是AI助手</h2>
<p>有什么可以帮助你的吗?</p>
<div class="quick-actions">
<button onclick="sendQuickMessage('介绍一下你自己')">介绍一下你自己</button>
<button onclick="sendQuickMessage('帮我写一段代码')">帮我写代码</button>
<button onclick="sendQuickMessage('解释一个概念')">解释概念</button>
</div>
</div>
<div class="messages" id="messages"></div>
</div>
<!-- 输入区域 -->
<div class="input-area">
<textarea
id="userInput"
placeholder="输入消息..."
rows="1"
onkeydown="handleKeyDown(event)"
oninput="autoResize(this)"
></textarea>
<button class="send-btn" onclick="sendMessage()" id="sendBtn">
<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="currentColor" d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/>
</svg>
</button>
</div>
</div>
<div id="app"></div>
<script src="marked.min.js"></script>
<script src="app.js"></script>
</body>

View File

@@ -25,17 +25,155 @@ body {
}
#app {
min-height: 100vh;
min-height: 100dvh;
}
/* ==================== 对话列表页面 ==================== */
.conversation-list-page {
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: 100dvh;
background: var(--card-bg);
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
color: white;
position: sticky;
top: 0;
z-index: 100;
}
.list-header .header-title {
display: flex;
align-items: center;
gap: 10px;
}
.list-header .logo {
font-size: 24px;
}
.list-header h1 {
font-size: 18px;
font-weight: 600;
}
.list-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.new-chat-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 12px;
font-size: 16px;
cursor: pointer;
margin-bottom: 16px;
transition: transform 0.2s;
}
.new-chat-btn:active {
transform: scale(0.98);
}
.conversation-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.empty-list {
text-align: center;
padding: 40px;
color: var(--text-light);
}
.conversation-item {
display: flex;
flex-direction: column;
padding: 12px 16px;
background: white;
border: 1px solid var(--border-color);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.conversation-item:hover {
border-color: var(--primary);
background: rgba(102, 126, 234, 0.05);
}
.conversation-item:active {
background: rgba(102, 126, 234, 0.1);
}
.conv-title {
font-size: 16px;
font-weight: 500;
color: var(--text-color);
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conv-meta {
font-size: 12px;
color: var(--text-light);
}
.conv-delete-btn {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--text-light);
padding: 6px;
cursor: pointer;
opacity: 0;
transition: all 0.2s;
}
.conversation-item:hover .conv-delete-btn {
opacity: 1;
}
.conv-delete-btn:hover {
color: #e53e3e;
}
/* ==================== 对话页面 ==================== */
#chatPage {
display: flex;
flex-direction: column;
height: 100vh;
height: 100dvh; /* 移动端动态视口高度 */
height: 100dvh;
max-width: 800px;
margin: 0 auto;
background: var(--card-bg);
box-shadow: var(--shadow);
}
/* 头部 */
.header {
display: flex;
justify-content: space-between;
@@ -48,19 +186,38 @@ body {
z-index: 100;
}
.back-btn {
background: transparent;
border: none;
color: white;
padding: 8px;
cursor: pointer;
display: flex;
align-items: center;
}
.back-btn:active {
opacity: 0.8;
}
.header-title {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
margin-left: 8px;
}
.header-title h1 {
font-size: 16px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.logo {
font-size: 24px;
}
.header h1 {
font-size: 18px;
font-weight: 600;
font-size: 20px;
}
.clear-btn {
@@ -70,14 +227,12 @@ body {
padding: 8px;
color: white;
cursor: pointer;
transition: background 0.2s;
}
.clear-btn:active {
background: rgba(255,255,255,0.3);
}
/* 消息区域 */
.messages-container {
flex: 1;
overflow-y: auto;
@@ -98,7 +253,6 @@ body {
.welcome h2 {
font-size: 20px;
margin-bottom: 8px;
color: var(--text-color);
}
.welcome p {
@@ -121,7 +275,6 @@ body {
border-radius: 20px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.quick-actions button:active {
@@ -216,109 +369,16 @@ body {
background: rgba(0,0,0,0.2);
}
/* 加载动画 */
.typing-indicator {
display: flex;
gap: 4px;
padding: 8px 0;
/* 流式输出光标 */
.streaming-cursor {
animation: blink 1s infinite;
color: var(--primary);
font-weight: bold;
}
.typing-indicator span {
width: 8px;
height: 8px;
background: var(--text-light);
border-radius: 50%;
animation: typing 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes typing {
0%, 60%, 100% { transform: translateY(0); }
30% { transform: translateY(-8px); }
}
/* 输入区域 */
.input-area {
display: flex;
gap: 10px;
padding: 12px 16px;
background: white;
border-top: 1px solid var(--border-color);
position: sticky;
bottom: 0;
}
#userInput {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: 24px;
font-size: 16px;
resize: none;
outline: none;
font-family: inherit;
max-height: 120px;
transition: border-color 0.2s;
}
#userInput:focus {
border-color: var(--primary);
}
.send-btn {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s, opacity 0.2s;
flex-shrink: 0;
}
.send-btn:active {
transform: scale(0.95);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 错误提示 */
.error-msg {
color: #e53e3e;
font-size: 14px;
text-align: center;
padding: 8px;
}
/* Toast 提示 */
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: var(--text-color);
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
opacity: 0;
transition: all 0.3s ease;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* 消息结构 */
@@ -326,15 +386,14 @@ body {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 75%;
}
.message.user .message-body {
align-items: flex-end;
max-width: 85%;
}
.message.assistant .message-body {
align-items: flex-start;
max-width: 80%;
}
/* 消息操作按钮 */
@@ -356,7 +415,6 @@ body {
padding: 4px 6px;
color: var(--text-light);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
@@ -442,23 +500,78 @@ body {
font-weight: 600;
}
/* 流式输出光标 */
.streaming-cursor {
animation: blink 1s infinite;
color: var(--primary);
font-weight: bold;
/* 输入区域 */
.input-area {
display: flex;
gap: 10px;
padding: 12px 16px;
background: white;
border-top: 1px solid var(--border-color);
position: sticky;
bottom: 0;
}
@keyframes blink {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
#userInput {
flex: 1;
padding: 12px 16px;
border: 2px solid var(--border-color);
border-radius: 24px;
font-size: 16px;
resize: none;
outline: none;
font-family: inherit;
max-height: 120px;
transition: border-color 0.2s;
}
/* 响应式 */
@media (min-width: 768px) {
.message-content {
max-width: 60%;
}
#userInput:focus {
border-color: var(--primary);
}
.send-btn {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, var(--primary) 0%, #764ba2 100%);
border: none;
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: transform 0.2s, opacity 0.2s;
flex-shrink: 0;
}
.send-btn:active {
transform: scale(0.95);
}
.send-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Toast 提示 */
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%) translateY(-20px);
background: var(--text-color);
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
opacity: 0;
transition: all 0.3s ease;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* 安全区域适配(刘海屏) */