This commit is contained in:
caoyuchun 2025-10-23 10:39:42 +08:00
parent 0fd35475dd
commit b3a47589d8
11 changed files with 1798 additions and 5 deletions

67
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.1.0",
"dependencies": {
"axios": "^1.7.2",
"clipboard": "^2.0.11",
"core-js": "^3.6.5",
"crypto-js": "^4.2.0",
"echarts": "^5.6.0",
@ -4739,6 +4740,16 @@
"node": ">= 10"
}
},
"node_modules/clipboard": {
"version": "2.0.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/clipboard/-/clipboard-2.0.11.tgz",
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/clipboardy": {
"version": "2.3.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/clipboardy/-/clipboardy-2.3.0.tgz",
@ -6077,6 +6088,11 @@
"node": ">=0.4.0"
}
},
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/depd/-/depd-2.0.0.tgz",
@ -7979,6 +7995,14 @@
"node": ">=6"
}
},
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "http://mirrors.cloud.tencent.com/npm/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/gopd/-/gopd-1.0.1.tgz",
@ -12769,6 +12793,11 @@
"url": "https://opencollective.com/webpack"
}
},
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"node_modules/select-hose": {
"version": "2.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/select-hose/-/select-hose-2.0.0.tgz",
@ -14284,6 +14313,11 @@
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
"dev": true
},
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"node_modules/tmp": {
"version": "0.0.33",
"resolved": "https://mirrors.cloud.tencent.com/npm/tmp/-/tmp-0.0.33.tgz",
@ -20135,6 +20169,16 @@
"integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==",
"dev": true
},
"clipboard": {
"version": "2.0.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/clipboard/-/clipboard-2.0.11.tgz",
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"requires": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"clipboardy": {
"version": "2.3.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/clipboardy/-/clipboardy-2.3.0.tgz",
@ -21176,6 +21220,11 @@
"resolved": "https://mirrors.cloud.tencent.com/npm/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"delegate": {
"version": "3.2.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
},
"depd": {
"version": "2.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/depd/-/depd-2.0.0.tgz",
@ -22682,6 +22731,14 @@
"slash": "^2.0.0"
}
},
"good-listener": {
"version": "1.2.2",
"resolved": "http://mirrors.cloud.tencent.com/npm/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"requires": {
"delegate": "^3.1.2"
}
},
"gopd": {
"version": "1.0.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/gopd/-/gopd-1.0.1.tgz",
@ -26504,6 +26561,11 @@
"ajv-keywords": "^3.5.2"
}
},
"select": {
"version": "1.1.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/select/-/select-1.1.2.tgz",
"integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/select-hose/-/select-hose-2.0.0.tgz",
@ -27755,6 +27817,11 @@
"integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==",
"dev": true
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "http://mirrors.cloud.tencent.com/npm/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://mirrors.cloud.tencent.com/npm/tmp/-/tmp-0.0.33.tgz",

View File

@ -13,6 +13,7 @@
},
"dependencies": {
"axios": "^1.7.2",
"clipboard": "^2.0.11",
"core-js": "^3.6.5",
"crypto-js": "^4.2.0",
"echarts": "^5.6.0",

View File

@ -6,7 +6,7 @@
<!-- <meta name="viewport" content="width=device-width,initial-scale=1.0"> -->
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico"> -->
<link rel="icon" href="favicon.ico">
<link rel="icon" href="/favicon.ico">
<!-- <link rel="https://axdbook.zxkedu.com/bundles/app/images/zwsjimg2_06.png"> -->

Binary file not shown.

View File

@ -0,0 +1,790 @@
<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');
});
}
});
},
// HTMLMarkdown
formatMessage(content) {
if (!content) return '';
// HTML
content = content.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
// - ```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>');
// liul
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>');
// liol
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>

View File

@ -0,0 +1,535 @@
<template>
<div class="ai-float-wrapper">
<div class="ai-float-container" ref="containerRef">
<!-- 悬浮按钮 -->
<button ref="buttonRef" @click.stop="toggleChat" @mousedown="startDrag" class="ai-float-button"
:class="{ 'pulse-animation': isNewMessage, 'active': showChat }" :style="buttonStyle">
<span v-if="isNewMessage" class="notification-dot"></span>
<div class="button-content">
<slot name="button-icon" :isChatOpen="showChat">
<svg v-if="!showChat" 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">
<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>
<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="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</slot>
<span class="button-text">{{ showChat ? closeText : openText }}</span>
</div>
</button>
</div>
<!-- 聊天面板 -->
<div v-if="showChat" class="chat-panel">
<div class="chat-panel-content">
<div class="chat-panel-header">
<div class="header-title">
<slot name="header-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" 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>
</slot>
<h3>{{ headerTitle }}</h3>
</div>
<button @click="closeChat" class="close-button">&times;</button>
</div>
<div class="chat-panel-body">
<slot :closeChat="closeChat">
<!-- 默认插槽内容如果未提供则使用AIChat组件 -->
<AIChat v-if="useDefaultAIChat" v-bind="aiChatProps" @new-message="handleNewMessage" />
</slot>
</div>
</div>
</div>
</div>
</template>
<script>
import AIChat from './AIChat.vue';
export default {
name: 'AIFloatButton',
components: {
AIChat
},
props: {
// AIChat
baseUrl: {
type: String,
default: null
},
modelAuthKey: {
type: String,
default: null
},
knowledgeBaseId: {
type: String,
default: null
},
knowledgeBaseUrl: {
type: String,
default: '/api/knowledgebases/query'
},
knowledgeBaseAuthKey: {
type: String,
default: ''
},
// UI
openText: {
type: String,
default: 'AI助手'
},
closeText: {
type: String,
default: '关闭'
},
headerTitle: {
type: String,
default: 'AI 助手'
},
//
useDefaultAIChat: {
type: Boolean,
default: true
},
//
initialPosition: {
type: Object,
default: function () { return { x: null, y: null }; } // null使
}
},
data() {
return {
showChat: false,
isNewMessage: false,
notificationTimeout: null,
isDragging: false,
dragState: {
startX: 0,
startY: 0,
startLeft: 0,
startTop: 0
},
buttonPosition: {
x: this.initialPosition.x,
y: this.initialPosition.y
}
};
},
computed: {
// AIChat
aiChatProps() {
return {
baseUrl: this.baseUrl,
modelAuthKey: this.modelAuthKey,
knowledgeBaseId: this.knowledgeBaseId,
knowledgeBaseUrl: this.knowledgeBaseUrl,
knowledgeBaseAuthKey: this.knowledgeBaseAuthKey
};
},
//
buttonStyle() {
// 使
if (this.buttonPosition.x !== null && this.buttonPosition.y !== null) {
return {
left: `${this.buttonPosition.x}px`,
top: `${this.buttonPosition.y}px`,
};
}
// 使
return {
bottom: '20px',
right: '20px',
};
}
},
mounted() {
document.addEventListener('keydown', this.handleEscapeKey);
window.addEventListener('resize', this.handleWindowResize);
},
beforeDestroy() {
document.removeEventListener('keydown', this.handleEscapeKey);
document.removeEventListener('mousemove', this.onDrag);
document.removeEventListener('mouseup', this.stopDrag);
window.removeEventListener('resize', this.handleWindowResize);
if (this.notificationTimeout) {
clearTimeout(this.notificationTimeout);
}
},
methods: {
//
startDrag(event) {
//
if (event.target.closest('.button-content')) {
return;
}
event.preventDefault();
this.isDragging = true;
//
const buttonRect = this.$refs.buttonRef.getBoundingClientRect();
this.dragState = {
startX: event.clientX,
startY: event.clientY,
startLeft: buttonRect.left,
startTop: buttonRect.top
};
//
if (this.showChat) {
// buttonStyle
this.$forceUpdate();
}
//
document.addEventListener('mousemove', this.onDrag);
document.addEventListener('mouseup', this.stopDrag);
},
//
onDrag(event) {
if (!this.isDragging) return;
event.preventDefault();
//
const deltaX = event.clientX - this.dragState.startX;
const deltaY = event.clientY - this.dragState.startY;
const newLeft = this.dragState.startLeft + deltaX;
const newTop = this.dragState.startTop + deltaY;
//
const boundedLeft = Math.max(0, Math.min(newLeft, window.innerWidth - 64));
const boundedTop = Math.max(0, Math.min(newTop, window.innerHeight - 64));
//
this.buttonPosition = {
x: boundedLeft,
y: boundedTop
};
//
if (this.showChat) {
// buttonStyle
this.$forceUpdate();
}
},
//
stopDrag() {
this.isDragging = false;
//
document.removeEventListener('mousemove', this.onDrag);
document.removeEventListener('mouseup', this.stopDrag);
},
//
toggleChat(event) {
//
if (this.isDragging) {
this.isDragging = false;
return;
}
event.stopPropagation();
this.showChat = !this.showChat;
this.isNewMessage = false;
//
if (this.notificationTimeout) {
clearTimeout(this.notificationTimeout);
this.notificationTimeout = null;
}
this.$emit('toggle', this.showChat);
},
//
closeChat() {
this.showChat = false;
this.$emit('close');
},
//
handleNewMessage() {
//
if (!this.showChat) {
this.isNewMessage = true;
// 3
if (this.notificationTimeout) {
clearTimeout(this.notificationTimeout);
}
this.notificationTimeout = setTimeout(() => {
this.isNewMessage = false;
}, 3000);
}
this.$emit('new-message');
},
// Escape
handleEscapeKey(event) {
if (this.showChat && event.key === 'Escape') {
this.closeChat();
}
},
//
handleWindowResize() {
//
if (this.showChat) {
//
this.$forceUpdate();
}
}
}
};
</script>
<style scoped>
.ai-float-wrapper {
position: relative;
z-index: 1000;
}
.ai-float-container {
position: fixed;
bottom: 20px;
right: 20px;
}
.ai-float-button {
position: fixed;
background: linear-gradient(135deg, #4a6cf7 0%, #6a11cb 100%);
color: white;
border: none;
border-radius: 50px;
width: 64px;
height: 64px;
font-weight: bold;
cursor: move;
/* 显示拖动光标 */
box-shadow: 0 6px 20px rgba(74, 108, 247, 0.4);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
padding: 0;
overflow: hidden;
z-index: 1000;
}
.ai-float-button:hover:not(:active) {
transform: translateY(-3px) scale(1.05);
box-shadow: 0 10px 25px rgba(74, 108, 247, 0.5);
}
.ai-float-button:active {
transform: translateY(0) scale(1);
}
.ai-float-button.active {
display: none;
}
.button-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
pointer-events: none;
/* 防止内容区域影响拖动 */
}
.button-text {
font-size: 10px;
font-weight: 600;
}
.notification-dot {
position: absolute;
top: -2px;
right: -2px;
width: 16px;
height: 16px;
background: #ff4757;
border: 2px solid white;
border-radius: 50%;
animation: pulse 1.5s infinite;
z-index: 1;
pointer-events: none;
/* 防止通知点影响拖动 */
}
.pulse-animation {
animation: pulse-button 2s infinite;
}
@keyframes pulse {
0% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 8px rgba(255, 71, 87, 0);
}
100% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(255, 71, 87, 0);
}
}
@keyframes pulse-button {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.chat-panel {
width: 450px;
min-height: 400px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
border-radius: 16px;
overflow: hidden;
background: white;
z-index: 1001;
animation: slideIn 0.3s ease-out;
position: fixed;
bottom: 20px;
right: 20px;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-5%) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.chat-panel-content {
display: flex;
flex-direction: column;
height: 100%;
max-height: 80vh;
}
.chat-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
background: linear-gradient(120deg, #4a6cf7 0%, #6a11cb 100%);
color: white;
flex-shrink: 0;
}
.header-title {
display: flex;
align-items: center;
gap: 8px;
}
.header-title h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.close-button {
background: rgba(255, 255, 255, 0.2);
border: none;
border-radius: 50%;
font-size: 20px;
cursor: pointer;
color: white;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close-button:hover {
background: rgba(255, 255, 255, 0.3);
transform: rotate(90deg);
}
.chat-panel-body {
flex: 1;
height: 100%;
overflow: auto;
display: flex;
flex-direction: column;
}
@media (max-width: 600px) {
.chat-panel {
width: calc(100vw - 20px);
max-height: 80vh;
max-width: 350px;
bottom: 10px;
right: 10px;
}
.chat-panel-content {
max-height: 80vh;
}
.chat-panel-body {
min-height: 300px;
}
.ai-float-button {
width: 56px;
height: 56px;
}
.ai-float-button.active {
border-radius: 20px 20px 6px 6px;
}
.button-text {
font-size: 9px;
}
}
</style>

View File

@ -0,0 +1,174 @@
# AI 聊天模块 (Vue 2 版本)
这个模块提供了一套完整的AI聊天组件专为Vue 2项目设计。组件包括悬浮按钮、聊天面板和AI助手工具类可以轻松集成到任何Vue 2项目中。
## 组件介绍
### AIFloatButton.vue
悬浮按钮组件,点击后会显示聊天面板。支持拖动、消息提醒和自定义位置功能。
### AIChat.vue
聊天面板组件,包含消息展示区和输入区域。支持发送消息、文件上传、历史记录管理等功能。
### aiAssistant.js
AI助手工具类提供知识库查询、内容提取和消息格式化功能。
## 功能特性
- 支持悬浮按钮拖动定位
- 消息提醒和动画效果
- 支持发送文本消息和文件上传
- 支持知识库查询和内容提取
- 自适应响应式设计
- 键盘快捷操作支持Enter发送Shift+Enter换行
## 使用方法
### 1. 安装依赖
确保项目中已安装必要的依赖:
```bash
npm install axios clipboard --save
```
### 2. 组件引入
```javascript
// 在Vue 2组件中引入
import AIFloatButton from './components/AIChatModuleVue2/AIFloatButton.vue';
export default {
components: {
AIFloatButton
}
}
```
### 3. 基本使用
```vue
<template>
<div id="app">
<!-- 基本使用方式 -->
<AIFloatButton />
<!-- 自定义配置 -->
<AIFloatButton
:chatModelUrl="'https://api.example.com'"
:modelAuthKey="'your-model-auth-key'"
:knowledgeBaseId="'your-knowledge-base-id'"
:headerTitle="'智能助手'"
:openText="'AI助手'"
:closeText="'关闭'"
:useDefaultAIChat="true"
/>
</div>
</template>
```
### 4. 自定义内容插槽
```vue
<AIFloatButton>
<template v-slot:button-icon="{ isChatOpen }">
<!-- 自定义按钮图标 -->
<span v-if="!isChatOpen">💬</span>
<span v-else></span>
</template>
<template v-slot:header-icon>
<!-- 自定义头部图标 -->
<span>🤖</span>
</template>
<!-- 自定义聊天面板内容 -->
<div slot-scope="{ closeChat }">
<h2>自定义聊天内容</h2>
<button @click="closeChat">关闭</button>
</div>
</template>
```
## 配置说明
### AIFloatButton 组件属性
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| chatModelUrl | String | 否 | 'https://qianfan.baidubce.com/v2' | AI模型聊天完成URL |
| modelAuthKey | String | 否 | null | 模型认证密钥 |
| knowledgeBaseId | String | 否 | null | 知识库ID |
| knowledgeBaseUrl | String | 否 | '/api/knowledgebases/query' | 知识库查询接口 |
| knowledgeBaseAuthKey | String | 否 | 'Bearer bce-v3/ALTAK-Whk44lUWwOsyro8mNlVhq/a0f9f719410e695928289034690003afe1571556' | 知识库认证密钥 |
| openText | String | 否 | 'AI助手' | 按钮打开文本 |
| closeText | String | 否 | '关闭' | 按钮关闭文本 |
| headerTitle | String | 否 | 'AI 助手' | 聊天面板标题 |
| useDefaultAIChat | Boolean | 否 | true | 是否使用默认聊天组件 |
| initialPosition | Object | 否 | { x: null, y: null } | 按钮初始位置 |
### AIFloatButton 组件事件
| 事件名 | 说明 | 参数 |
|--------|------|------|
| toggle | 聊天面板显示状态切换 | showChat: Boolean |
| close | 聊天面板关闭事件 | 无 |
| new-message | 收到新消息事件 | 无 |
### AIChat 组件属性
| 属性名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| chatModelUrl | String | 否 | 'https://qianfan.baidubce.com/v2' | AI模型聊天完成URL |
| modelAuthKey | String | 否 | null | 模型认证密钥 |
| knowledgeBaseId | String | 否 | null | 知识库ID |
| knowledgeBaseUrl | String | 否 | '/api/knowledgebases/query' | 知识库查询接口 |
| knowledgeBaseAuthKey | String | 否 | 'Bearer bce-v3/ALTAK-Whk44lUWwOsyro8mNlVhq/a0f9f719410e695928289034690003afe1571556' | 知识库认证密钥 |
### AIChat 组件事件
| 事件名 | 说明 | 参数 |
|--------|------|------|
| new-message | 收到新消息事件 | message: Object |
| send-message | 发送消息事件 | message: Object |
| loading-change | 加载状态变化事件 | isLoading: Boolean |
## 键盘快捷键
- **Enter**: 发送消息
- **Shift + Enter**: 换行
- **Esc**: 关闭聊天面板
## 响应式设计
组件支持桌面端和移动端自适应布局:
- 桌面端:聊天面板固定位置,支持拖动
- 移动端:聊天面板自适应屏幕宽度,优化触摸操作
## 浏览器兼容性
支持所有现代浏览器,包括:
- Chrome (最新2个版本)
- Firefox (最新2个版本)
- Safari (最新2个版本)
- Edge (最新2个版本)
## Vue 2 版本与 Vue 3 版本的主要差异
1. **语法差异**使用Vue 2的Options API替代Vue 3的Composition API
2. **生命周期钩子**使用Vue 2的生命周期钩子mounted, beforeDestroy
3. **响应式数据**使用data选项替代ref/reactive
4. **计算属性**使用computed选项替代computed函数
5. **监听器**使用watch选项替代watch/onMounted等组合式API
## 注意事项
1. 确保Vue 2项目版本 >= 2.6.0,以支持所有特性
2. 在生产环境中请正确配置API密钥和知识库ID
3. 如需自定义主题请通过CSS变量或覆盖样式实现
4. 对于大型项目建议使用Vuex管理聊天状态

View File

@ -0,0 +1,210 @@
/**
* AI问答助手核心功能封装
* 可在不同项目中复用的AI问答功能 - Vue 2版本
*/
class AIAssistant {
/**
* 构造函数
* @param {Object} config - 配置选项
* @param {string} config.chatModelUrl - AI服务基础URL
* @param {string} config.modelAuthKey - AI服务认证密钥
* @param {string} config.knowledgeBaseId - 知识库ID可选
* @param {string} config.knowledgeBaseUrl - 知识库查询URL可选
* @param {string} config.knowledgeBaseAuthKey - 知识库认证密钥可选
*/
constructor(config = {}) {
this.chatModelUrl = config.chatModelUrl || "/api/chat/completions";
this.modelAuthKey = config.modelAuthKey;
this.knowledgeBaseId = config.knowledgeBaseId;
this.knowledgeBaseUrl =
config.knowledgeBaseUrl || "/api/knowledgebases/query";
this.knowledgeBaseAuthKey =
config.knowledgeBaseAuthKey ||
"Bearer bce-v3/ALTAK-Whk44lUWwOsyro8mNlVhq/a0f9f719410e695928289034690003afe1571556";
}
/**
* 查询知识库获取参考内容
* @param {string} query - 查询内容
* @param {string} knowledgeBaseId - 知识库ID可选优先使用
* @returns {Promise<Object|null>} 知识库查询结果
*/
async queryKnowledgeBase(query, knowledgeBaseId = null) {
const kbId = knowledgeBaseId || this.knowledgeBaseId;
if (!kbId) {
console.log("未提供知识库ID");
return null;
}
console.log("正在查询知识库:", kbId, "查询内容:", query);
try {
const response = await fetch(this.knowledgeBaseUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: this.knowledgeBaseAuthKey,
},
body: JSON.stringify({
query: query,
knowledgebase_ids: [kbId],
rank_score_threshold: "0.4",
pipeline_config: {
ranking: "true",
},
}),
});
console.log("知识库查询响应状态:", response.status);
if (response.ok) {
const data = await response.json();
console.log("知识库查询结果:", data);
return data;
} else {
console.error("知识库查询失败,状态码:", response.status);
const errorText = await response.text();
console.error("错误详情:", errorText);
}
} catch (error) {
console.error("查询知识库时出错:", error);
}
return null;
}
/**
* 从知识库返回的数据中提取参考内容
* @param {Object} knowledgeData - 知识库返回的数据
* @returns {string} 提取的参考内容
*/
extractKnowledgeContent(knowledgeData) {
if (!knowledgeData) return "";
let knowledgeContent = "";
if (knowledgeData.chunks) {
// 从 chunks 数组中提取 content 并拼接
knowledgeContent = knowledgeData.chunks
.map((chunk) => chunk.content || "")
.join("\n\n");
console.log("从chunks提取的知识库内容:", knowledgeContent);
} else if (knowledgeData.references) {
// 保留原有逻辑以确保向后兼容
knowledgeContent = knowledgeData.references
.map((ref) => `文档片段: ${ref.title || ""}\n${ref.content || ""}`)
.join("\n\n");
console.log("从references提取的知识库内容:", knowledgeContent);
} else {
console.log("未找到相关参考内容");
}
return knowledgeContent;
}
/**
* 构造发送给AI的消息
* @param {string} question - 用户问题
* @param {string} knowledgeContent - 知识库参考内容
* @returns {Array} 消息数组
*/
constructMessages(question, knowledgeContent = "") {
// 系统消息
const systemMessage = {
role: "system",
content: `# 角色任务
作为读书辅导教师你的核心任务是理解用户的问题结合提供的参考内容如书籍片段笔记解析等理解并把握问题的核心你需要能够区分参考内容中与问题相关的部分并排除无关的信息在用户提问时结合你的知识和理解给出针对性的回答
# 工具能力
1. 理解用户问题你需要能够准确理解用户的问题明确用户需求和困惑点
2. 分析参考内容结合提供的参考内容如书籍片段笔记解析等找出与问题相关的关键信息
3. 排除无关内容在理解用户问题和分析参考内容的基础上排除与根问题无关的信息
4. 知识储备作为读书辅导教师你需要有广泛的知识储备能够针对用户的问题给出解答
5. 交互能力在用户提问时与用户进行交互引导用户表达更清楚自己的问题
# 要求与限制
1. 响应格式返回的回答需要以HTML格式呈现不要强调HTML格式
2. 针对性回答结合用户的问题和参考内容给出针对性的回答不要提及参考内容
3. 问题与参考内容关联判断如果问题与参考内容无关要明确告诉用户例如"这个问题与本书内容无关"进行提示并给出建议例如以下建议来自互联网仅供参考...
4. 简洁明了在回答用户问题时力求简洁明了避免冗余和复杂的表述
5. 准确性确保给出的答案准确无误能够真正帮助用户解决问题
6. 如果参考内容里有图片视频等资源也一起返回
`,
};
// 用户消息
let userMessageContent = question;
if (knowledgeContent) {
userMessageContent = `问题:${question} 参考:${knowledgeContent}`;
}
const userMessage = {
role: "user",
content: userMessageContent,
};
return [systemMessage, userMessage];
}
/**
* 发送消息到AI并获取回复
* @param {string} question - 用户问题
* @param {string} knowledgeBaseId - 知识库ID可选
* @returns {Promise<ReadableStream>} AI回复的流
*/
async sendMessage(question, knowledgeBaseId = null) {
try {
// 查询知识库获取参考内容
let knowledgeContent = "";
if (this.knowledgeBaseId || knowledgeBaseId) {
console.log("开始查询知识库...");
const knowledgeData = await this.queryKnowledgeBase(
question,
knowledgeBaseId
);
console.log("知识库返回数据:", knowledgeData);
knowledgeContent = this.extractKnowledgeContent(knowledgeData);
} else {
console.log("未提供知识库ID跳过查询");
}
// 构造发送给AI的消息
const messages = this.constructMessages(question, knowledgeContent);
// 创建请求
const url = `${this.chatModelUrl}/chat/completions`;
const requestBody = {
messages: messages,
model: "ernie-4.5-turbo-128k",
stream: true,
};
console.log("发送给AI的请求:", JSON.stringify(requestBody, null, 2));
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.modelAuthKey}`,
},
body: JSON.stringify(requestBody),
});
if (!response.body) {
throw new Error("ReadableStream not supported");
}
return response.body.getReader();
} catch (error) {
console.error("Error sending message:", error);
throw error;
}
}
}
// 导出 AIAssistant 类
export { AIAssistant };

View File

@ -14,6 +14,7 @@ Vue.use(ElementUI);
Vue.config.productionTip = false

View File

@ -78,6 +78,7 @@
<div v-show="showError">
<no-data :msg="errormsg"></no-data>
</div>
<AIFloatButton />
</div>
</template>
@ -91,7 +92,7 @@ import {bookApi} from "../service/getData"
import Vue from 'vue'
import XML from "../plugin/xml-digital-teaching/lib/index"
import {eventBus} from '../eventBus'
import AIFloatButton from '../components/AIChatModuleVue2/AIFloatButton.vue';
Vue.use(XML)
@ -99,6 +100,7 @@ export default {
components:{
NoData,
Navigation,
AIFloatButton
},
data() {
return {

View File

@ -6,7 +6,19 @@ console.log("cycccccccc")
//
var targetServer = "https://xinsiketang.com"
if(process.env.NODE_ENV === 'prod'){
console.log("hellolllllllooooo")
console.log(process.env.NODE_ENV)
// process.exit(0); // 0 表示正常退出非0表示异常退出
var publicPath = 'https://smile-ebook-online.xinsiketang.com/reading/'
publicPath = "./"
console.log(publicPath)
// process.exit(0); // 0 表示正常退出非0表示异常退出
console.log(process.env.NODE_ENV)
// process.exit(0);
if(process.env.NODE_ENV === 'production'){
targetServer = "https://xinsiketang.com"
}else{
targetServer = "https://dev.xinsiketang.com"
@ -14,10 +26,11 @@ if(process.env.NODE_ENV === 'prod'){
// targetServer = "https://xinsiketang.com"
console.log("targetServer",targetServer)
module.exports = {
lintOnSave: false,
// publicPath: './', // 设置公共路径为相对路径
publicPath: "https://smile-ebook-online.xinsiketang.com/reading/",
publicPath: publicPath, // 设置公共路径为相对路径
// publicPath: "https://smile-ebook-online.xinsiketang.com/reading/",
outputDir:process.env.BUILD_DIR? process.env.BUILD_DIR :"dist",
devServer: {
// headers: {