790 lines
22 KiB
Vue
790 lines
22 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="ai-chat-container">
|
|||
|
|
<div class="chat-messages" ref="messagesContainer">
|
|||
|
|
<div v-for="(message, index) in messages" :key="index" :class="['message', message.role]">
|
|||
|
|
<div class="avatar">
|
|||
|
|
<div v-if="message.role === 'user'" class="user-avatar">
|
|||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|||
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|||
|
|
<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" />
|
|||
|
|
<circle cx="12" cy="7" r="4" />
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
<div v-else class="ai-avatar">
|
|||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|||
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|||
|
|
<path d="m12 8-9.04 9.06a2.82 2.82 0 1 0 3.98 3.98L16 12" />
|
|||
|
|
<circle cx="17" cy="7" r="5" />
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="message-content">
|
|||
|
|
<div class="message-text" v-html="message.content"></div>
|
|||
|
|
<div class="message-time">{{ formatTime(message.timestamp) }}</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="isLoading" class="message assistant">
|
|||
|
|
<div class="avatar">
|
|||
|
|
<div class="ai-avatar">
|
|||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
|
|||
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|||
|
|
<path d="m12 8-9.04 9.06a2.82 2.82 0 1 0 3.98 3.98L16 12" />
|
|||
|
|
<circle cx="17" cy="7" r="5" />
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="message-content">
|
|||
|
|
<div class="message-text typing-indicator">
|
|||
|
|
<span></span>
|
|||
|
|
<span></span>
|
|||
|
|
<span></span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div v-if="messages.length === 0" class="welcome-message">
|
|||
|
|
<div class="welcome-icon">
|
|||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none"
|
|||
|
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|||
|
|
<path d="m12 8-9.04 9.06a2.82 2.82 0 1 0 3.98 3.98L16 12" />
|
|||
|
|
<circle cx="17" cy="7" r="5" />
|
|||
|
|
</svg>
|
|||
|
|
</div>
|
|||
|
|
<h3 v-html="formatMessage('你好!我是AI助手')"></h3>
|
|||
|
|
<p v-html="formatMessage('有什么我可以帮你的吗?')"></p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="chat-input">
|
|||
|
|
<div class="input-container">
|
|||
|
|
<input v-model="inputMessage" @keyup.enter="sendMessage" :disabled="isLoading" placeholder="输入消息..."
|
|||
|
|
class="message-input" />
|
|||
|
|
<button @click="sendMessage" :disabled="isLoading || !inputMessage.trim()" class="send-button"
|
|||
|
|
:class="{ 'loading': isLoading }">
|
|||
|
|
<svg v-if="!isLoading" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
|||
|
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|||
|
|
stroke-linejoin="round">
|
|||
|
|
<line x1="22" y1="2" x2="11" y2="13" />
|
|||
|
|
<polygon points="22 2 15 22 11 13 2 9 22 2" />
|
|||
|
|
</svg>
|
|||
|
|
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
|||
|
|
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|||
|
|
stroke-linejoin="round">
|
|||
|
|
<line x1="12" y1="2" x2="12" y2="6" />
|
|||
|
|
<line x1="12" y1="18" x2="12" y2="22" />
|
|||
|
|
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76" />
|
|||
|
|
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07" />
|
|||
|
|
<line x1="2" y1="12" x2="6" y2="12" />
|
|||
|
|
<line x1="18" y1="12" x2="22" y2="12" />
|
|||
|
|
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24" />
|
|||
|
|
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93" />
|
|||
|
|
</svg>
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// Vue 2 导入方式
|
|||
|
|
import { AIAssistant } from './aiAssistant.js';
|
|||
|
|
import ClipboardJS from 'clipboard';
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'AIChat',
|
|||
|
|
props: {
|
|||
|
|
chatModelUrl: {
|
|||
|
|
type: String,
|
|||
|
|
default: 'https://qianfan.baidubce.com/v2',
|
|||
|
|
required: false
|
|||
|
|
},
|
|||
|
|
modelAuthKey: {
|
|||
|
|
type: String,
|
|||
|
|
required: true
|
|||
|
|
},
|
|||
|
|
knowledgeBaseId: {
|
|||
|
|
type: String,
|
|||
|
|
default: null
|
|||
|
|
},
|
|||
|
|
knowledgeBaseUrl: {
|
|||
|
|
type: String,
|
|||
|
|
default: '/api/knowledgebases/query'
|
|||
|
|
},
|
|||
|
|
knowledgeBaseAuthKey: {
|
|||
|
|
type: String,
|
|||
|
|
required: true
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
data() {
|
|||
|
|
return {
|
|||
|
|
messages: [],
|
|||
|
|
inputMessage: '',
|
|||
|
|
isLoading: false,
|
|||
|
|
eventSource: null,
|
|||
|
|
aiAssistant: null,
|
|||
|
|
clipboard: null
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
mounted() {
|
|||
|
|
// 初始化AI助手实例
|
|||
|
|
this.aiAssistant = new AIAssistant({
|
|||
|
|
chatModelUrl: this.chatModelUrl,
|
|||
|
|
modelAuthKey: this.modelAuthKey,
|
|||
|
|
knowledgeBaseId: this.knowledgeBaseId,
|
|||
|
|
knowledgeBaseUrl: this.knowledgeBaseUrl,
|
|||
|
|
knowledgeBaseAuthKey: this.knowledgeBaseAuthKey
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 初始化剪贴板功能
|
|||
|
|
this.clipboard = new ClipboardJS('.copy-button');
|
|||
|
|
|
|||
|
|
// 初始化时渲染可能存在的内容
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
this.renderMathJax();
|
|||
|
|
this.highlightCode();
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
beforeDestroy() {
|
|||
|
|
if (this.eventSource) {
|
|||
|
|
this.eventSource.close();
|
|||
|
|
}
|
|||
|
|
if (this.clipboard) {
|
|||
|
|
this.clipboard.destroy();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
methods: {
|
|||
|
|
formatTime(timestamp) {
|
|||
|
|
return new Date(timestamp).toLocaleTimeString('zh-CN', {
|
|||
|
|
hour: '2-digit',
|
|||
|
|
minute: '2-digit'
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
scrollToBottom() {
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
if (this.$refs.messagesContainer) {
|
|||
|
|
this.$refs.messagesContainer.scrollTop = this.$refs.messagesContainer.scrollHeight;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
clearChat() {
|
|||
|
|
this.messages = [];
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
async sendMessage() {
|
|||
|
|
const message = this.inputMessage.trim();
|
|||
|
|
if (!message || this.isLoading) return;
|
|||
|
|
|
|||
|
|
// 添加用户消息
|
|||
|
|
this.messages.push({
|
|||
|
|
role: 'user',
|
|||
|
|
content: message,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
this.inputMessage = '';
|
|||
|
|
this.isLoading = true;
|
|||
|
|
this.scrollToBottom();
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 添加助手的空消息占位
|
|||
|
|
const assistantMessageIndex = this.messages.length;
|
|||
|
|
this.messages.push({
|
|||
|
|
role: 'assistant',
|
|||
|
|
content: '',
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 使用AI助手发送消息
|
|||
|
|
const reader = await this.aiAssistant.sendMessage(message);
|
|||
|
|
const decoder = new TextDecoder();
|
|||
|
|
|
|||
|
|
let accumulatedContent = '';
|
|||
|
|
|
|||
|
|
while (true) {
|
|||
|
|
const { done, value } = await reader.read();
|
|||
|
|
if (done) break;
|
|||
|
|
|
|||
|
|
const chunk = decoder.decode(value, { stream: true });
|
|||
|
|
const lines = chunk.split('\n');
|
|||
|
|
|
|||
|
|
for (const line of lines) {
|
|||
|
|
if (line.startsWith('data:')) {
|
|||
|
|
const data = line.slice(5).trim();
|
|||
|
|
|
|||
|
|
if (data === '[DONE]') {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const parsed = JSON.parse(data);
|
|||
|
|
// 根据 OpenAI API 格式解析数据
|
|||
|
|
const content = parsed.choices[0].delta.content || '';
|
|||
|
|
if (content) {
|
|||
|
|
accumulatedContent += content;
|
|||
|
|
// 实时更新内容并格式化
|
|||
|
|
this.messages[assistantMessageIndex].content = accumulatedContent;
|
|||
|
|
this.scrollToBottom();
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.warn('Error parsing JSON:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 渲染数学公式和代码高亮
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
this.renderMathJax();
|
|||
|
|
this.highlightCode();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 发送新消息事件
|
|||
|
|
this.$emit('new-message');
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('Error sending message:', error);
|
|||
|
|
this.messages.push({
|
|||
|
|
role: 'assistant',
|
|||
|
|
content: '抱歉,发送消息时出现错误: ' + error.message,
|
|||
|
|
timestamp: Date.now()
|
|||
|
|
});
|
|||
|
|
} finally {
|
|||
|
|
this.isLoading = false;
|
|||
|
|
this.scrollToBottom();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 渲染数学公式
|
|||
|
|
renderMathJax() {
|
|||
|
|
if (window.MathJax) {
|
|||
|
|
window.MathJax.Hub.Queue(["Typeset", window.MathJax.Hub]);
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 代码高亮
|
|||
|
|
highlightCode() {
|
|||
|
|
this.$nextTick(() => {
|
|||
|
|
// 查找所有代码块元素
|
|||
|
|
const codeBlocks = this.$refs.messagesContainer.querySelectorAll('pre code') || [];
|
|||
|
|
|
|||
|
|
// 如果有Prism.js,则使用它来高亮代码
|
|||
|
|
if (window.Prism) {
|
|||
|
|
codeBlocks.forEach((block) => {
|
|||
|
|
// 获取语言类名
|
|||
|
|
const classes = block.className.split(' ');
|
|||
|
|
const langClass = classes.find(cls => cls.startsWith('language-'));
|
|||
|
|
|
|||
|
|
if (langClass) {
|
|||
|
|
// 如果指定了语言,则使用指定的语言进行高亮
|
|||
|
|
const lang = langClass.replace('language-', '');
|
|||
|
|
if (window.Prism.languages[lang]) {
|
|||
|
|
block.className = `language-${lang}`;
|
|||
|
|
window.Prism.highlightElement(block);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 默认高亮
|
|||
|
|
window.Prism.highlightElement(block);
|
|||
|
|
});
|
|||
|
|
} else {
|
|||
|
|
// 如果没有Prism.js,则至少确保基本样式
|
|||
|
|
codeBlocks.forEach((block) => {
|
|||
|
|
block.classList.add('no-highlight');
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
|
|||
|
|
// 格式化消息内容,处理HTML和Markdown格式
|
|||
|
|
formatMessage(content) {
|
|||
|
|
if (!content) return '';
|
|||
|
|
|
|||
|
|
// 处理HTML实体编码
|
|||
|
|
content = content.replace(/&/g, '&')
|
|||
|
|
.replace(/</g, '<')
|
|||
|
|
.replace(/>/g, '>')
|
|||
|
|
.replace(/"/g, '"')
|
|||
|
|
.replace(/'/g, ''');
|
|||
|
|
|
|||
|
|
// 处理代码块 - 将 ```code``` 转换为 <pre><code>code</code></pre>
|
|||
|
|
content = content.replace(/```(\w*)\s*([\s\S]*?)```/g, (match, lang, code) => {
|
|||
|
|
if (lang) {
|
|||
|
|
return `<pre><code class="language-${lang}">${code.trim()}</code></pre>`;
|
|||
|
|
} else {
|
|||
|
|
return `<pre><code>${code.trim()}</code></pre>`;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// 处理行内代码 - 将 `code` 转换为 <code>code</code>
|
|||
|
|
content = content.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|||
|
|
|
|||
|
|
// 处理粗体文本 - 将 **bold** 转换为 <strong>bold</strong>
|
|||
|
|
content = content.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|||
|
|
|
|||
|
|
// 处理斜体文本 - 将 *italic* 转换为 <em>italic</em>
|
|||
|
|
content = content.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|||
|
|
|
|||
|
|
// 处理删除线 - 将 ~~text~~ 转换为 <del>text</del>
|
|||
|
|
content = content.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
|||
|
|
|
|||
|
|
// 处理链接 - 将 [text](url) 转换为 <a href="url">text</a>
|
|||
|
|
content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
|
|||
|
|
|
|||
|
|
// 处理无序列表 - 将 * item 转换为 <li>item</li>
|
|||
|
|
content = content.replace(/^\s*\*\s+(.*)$/gm, '<li>$1</li>');
|
|||
|
|
// 包裹相邻的li元素到ul中
|
|||
|
|
content = content.replace(/((?:<li>.*<\/li>\s*)+)/g, '<ul>$1</ul>');
|
|||
|
|
|
|||
|
|
// 处理有序列表 - 将 1. item 转换为 <li>item</li>
|
|||
|
|
content = content.replace(/^\s*\d+\.\s+(.*)$/gm, '<li>$1</li>');
|
|||
|
|
// 包裹相邻的li元素到ol中
|
|||
|
|
content = content.replace(/((?:<li>.*<\/li>\s*)+)/g, '<ol>$1</ol>');
|
|||
|
|
|
|||
|
|
// 处理数学公式(行内)
|
|||
|
|
content = content.replace(/\$([^\$]+)\$/g, '<span class="math-inline">$1</span>');
|
|||
|
|
|
|||
|
|
// 处理数学公式(块级)
|
|||
|
|
content = content.replace(/\$\$([^\$]+)\$\$/g, '<div class="math-display">$1</div>');
|
|||
|
|
|
|||
|
|
// 处理换行
|
|||
|
|
content = content.replace(/\n/g, '<br>');
|
|||
|
|
|
|||
|
|
return content;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.ai-chat-container {
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
height: 100%;
|
|||
|
|
min-height: 400px;
|
|||
|
|
max-height: calc(80vh - 20px);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
|||
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chat-header {
|
|||
|
|
background: linear-gradient(120deg, #4a6cf7 0%, #6a11cb 100%);
|
|||
|
|
color: white;
|
|||
|
|
padding: 16px;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-content {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 12px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-icon {
|
|||
|
|
background: rgba(255, 255, 255, 0.2);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
padding: 8px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-text h3 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.status {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 12px;
|
|||
|
|
opacity: 0.9;
|
|||
|
|
margin-top: 2px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.icon-button {
|
|||
|
|
background: rgba(255, 255, 255, 0.2);
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
width: 36px;
|
|||
|
|
height: 36px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.icon-button:hover {
|
|||
|
|
background: rgba(255, 255, 255, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chat-messages {
|
|||
|
|
flex: 1;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
padding: 15px;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 15px;
|
|||
|
|
background-color: #fff;
|
|||
|
|
scroll-behavior: smooth;
|
|||
|
|
-webkit-overflow-scrolling: touch;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 隐藏滚动条但保持可滚动 */
|
|||
|
|
.chat-messages::-webkit-scrollbar {
|
|||
|
|
width: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chat-messages::-webkit-scrollbar-track {
|
|||
|
|
background: transparent;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chat-messages::-webkit-scrollbar-thumb {
|
|||
|
|
background: rgba(0, 0, 0, 0.1);
|
|||
|
|
border-radius: 3px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chat-messages::-webkit-scrollbar-thumb:hover {
|
|||
|
|
background: rgba(0, 0, 0, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-message {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 40px 20px;
|
|||
|
|
color: #666;
|
|||
|
|
margin: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-icon {
|
|||
|
|
margin-bottom: 20px;
|
|||
|
|
color: #4a6cf7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-message h3 {
|
|||
|
|
margin: 0 0 10px 0;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-message p {
|
|||
|
|
margin: 0;
|
|||
|
|
color: #666;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
max-width: 95%;
|
|||
|
|
animation: fadeIn 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes fadeIn {
|
|||
|
|
from {
|
|||
|
|
opacity: 0;
|
|||
|
|
transform: translateY(10px);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
to {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateY(0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message.user {
|
|||
|
|
align-self: flex-end;
|
|||
|
|
flex-direction: row-reverse;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message.user .message-content {
|
|||
|
|
background: linear-gradient(120deg, #4a6cf7 0%, #6a11cb 100%);
|
|||
|
|
color: white;
|
|||
|
|
border-top-right-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message.assistant .message-content {
|
|||
|
|
background-color: #f0f4f8;
|
|||
|
|
color: #333;
|
|||
|
|
border-top-left-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.avatar {
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
width: 28px;
|
|||
|
|
height: 28px;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-avatar,
|
|||
|
|
.ai-avatar {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.user-avatar {
|
|||
|
|
background: linear-gradient(120deg, #4a6cf7 0%, #6a11cb 100%);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.ai-avatar {
|
|||
|
|
background: #e4edf5;
|
|||
|
|
color: #4a6cf7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-content {
|
|||
|
|
padding: 10px 14px;
|
|||
|
|
border-radius: 18px;
|
|||
|
|
position: relative;
|
|||
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text {
|
|||
|
|
word-wrap: break-word;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
font-size: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 代码块样式 */
|
|||
|
|
.message-text pre {
|
|||
|
|
background-color: #2d2d2d;
|
|||
|
|
border-radius: 8px;
|
|||
|
|
padding: 16px;
|
|||
|
|
overflow-x: auto;
|
|||
|
|
margin: 12px 0;
|
|||
|
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
|||
|
|
position: relative;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text pre code {
|
|||
|
|
font-family: 'Courier New', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
|||
|
|
background: none;
|
|||
|
|
color: #f8f8f2;
|
|||
|
|
text-shadow: none;
|
|||
|
|
padding: 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
display: block;
|
|||
|
|
white-space: pre;
|
|||
|
|
word-wrap: normal;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text pre code.no-highlight {
|
|||
|
|
color: #f8f8f2;
|
|||
|
|
font-size: 14px;
|
|||
|
|
line-height: 1.5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text strong {
|
|||
|
|
font-weight: 700;
|
|||
|
|
color: #333;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text em {
|
|||
|
|
font-style: italic;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text del {
|
|||
|
|
text-decoration: line-through;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text a {
|
|||
|
|
color: #4a6cf7;
|
|||
|
|
text-decoration: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text a:hover {
|
|||
|
|
text-decoration: underline;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text ul,
|
|||
|
|
.message-text ol {
|
|||
|
|
padding-left: 20px;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text li {
|
|||
|
|
margin: 5px 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text ul ul,
|
|||
|
|
.message-text ol ol,
|
|||
|
|
.message-text ul ol,
|
|||
|
|
.message-text ol ul {
|
|||
|
|
margin: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text code {
|
|||
|
|
font-family: 'Courier New', Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
|||
|
|
background-color: #f0f0f0;
|
|||
|
|
padding: 2px 6px;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: #d6336c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-text pre code {
|
|||
|
|
background-color: transparent;
|
|||
|
|
padding: 0;
|
|||
|
|
color: #f8f8f2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 行内数学公式 */
|
|||
|
|
.message-text span.math-inline {
|
|||
|
|
color: #e83e8c;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 块级数学公式 */
|
|||
|
|
.message-text div.math-display {
|
|||
|
|
text-align: center;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
padding: 10px;
|
|||
|
|
background-color: #f8f9fa;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-time {
|
|||
|
|
font-size: 11px;
|
|||
|
|
opacity: 0.7;
|
|||
|
|
text-align: right;
|
|||
|
|
margin-top: 6px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.typing-indicator {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 4px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.typing-indicator span {
|
|||
|
|
width: 8px;
|
|||
|
|
height: 8px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: #999;
|
|||
|
|
display: inline-block;
|
|||
|
|
animation: typing 1.4s infinite ease-in-out;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.typing-indicator span:nth-child(1) {
|
|||
|
|
animation-delay: 0s;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.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(-5px);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chat-input {
|
|||
|
|
padding: 16px;
|
|||
|
|
background: white;
|
|||
|
|
border-top: 1px solid #eee;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.input-container {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 10px;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-input {
|
|||
|
|
flex: 1;
|
|||
|
|
padding: 12px 16px;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
border-radius: 24px;
|
|||
|
|
outline: none;
|
|||
|
|
font-size: 15px;
|
|||
|
|
transition: border-color 0.2s ease;
|
|||
|
|
background: #f8fafc;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message-input:focus {
|
|||
|
|
border-color: #4a6cf7;
|
|||
|
|
box-shadow: 0 0 0 2px rgba(74, 108, 247, 0.2);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.send-button {
|
|||
|
|
width: 44px;
|
|||
|
|
height: 44px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
border: none;
|
|||
|
|
background: linear-gradient(120deg, #4a6cf7 0%, #6a11cb 100%);
|
|||
|
|
color: white;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
justify-content: center;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.2s ease;
|
|||
|
|
flex-shrink: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.send-button:hover:not(:disabled) {
|
|||
|
|
transform: scale(1.05);
|
|||
|
|
box-shadow: 0 4px 12px rgba(74, 108, 247, 0.3);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.send-button:disabled {
|
|||
|
|
opacity: 0.6;
|
|||
|
|
cursor: not-allowed;
|
|||
|
|
transform: none;
|
|||
|
|
box-shadow: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.send-button.loading {
|
|||
|
|
animation: spin 1s linear infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes spin {
|
|||
|
|
0% {
|
|||
|
|
transform: rotate(0deg);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
100% {
|
|||
|
|
transform: rotate(360deg);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@media (max-width: 600px) {
|
|||
|
|
.ai-chat-container {
|
|||
|
|
height: 100%;
|
|||
|
|
border-radius: 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.message {
|
|||
|
|
max-width: 98%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chat-header {
|
|||
|
|
padding: 12px 16px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.chat-messages {
|
|||
|
|
padding: 12px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|