diff --git a/package-lock.json b/package-lock.json index 93031f1..ef300c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 56f10d9..22df96e 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/index.html b/public/index.html index 905880d..0b5c469 100644 --- a/public/index.html +++ b/public/index.html @@ -6,7 +6,7 @@ - + diff --git a/src/components/AIChatModuleVue2.zip b/src/components/AIChatModuleVue2.zip new file mode 100644 index 0000000..6808450 Binary files /dev/null and b/src/components/AIChatModuleVue2.zip differ diff --git a/src/components/AIChatModuleVue2/AIChat.vue b/src/components/AIChatModuleVue2/AIChat.vue new file mode 100644 index 0000000..b9e1f8e --- /dev/null +++ b/src/components/AIChatModuleVue2/AIChat.vue @@ -0,0 +1,790 @@ + + + + + + + + + + + + + + + + + + + + + {{ formatTime(message.timestamp) }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/AIChatModuleVue2/AIFloatButton.vue b/src/components/AIChatModuleVue2/AIFloatButton.vue new file mode 100644 index 0000000..c872761 --- /dev/null +++ b/src/components/AIChatModuleVue2/AIFloatButton.vue @@ -0,0 +1,535 @@ + + + + + + + + + + + + + + + + + + {{ showChat ? closeText : openText }} + + + + + + + + + + + + + + + + {{ headerTitle }} + + × + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/AIChatModuleVue2/README.md b/src/components/AIChatModuleVue2/README.md new file mode 100644 index 0000000..42c3b0a --- /dev/null +++ b/src/components/AIChatModuleVue2/README.md @@ -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 + + + + + + + + + +``` + +### 4. 自定义内容插槽 + +```vue + + + + 💬 + ✖ + + + + + 🤖 + + + + + 自定义聊天内容 + 关闭 + + +``` + +## 配置说明 + +### 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管理聊天状态 \ No newline at end of file diff --git a/src/components/AIChatModuleVue2/aiAssistant.js b/src/components/AIChatModuleVue2/aiAssistant.js new file mode 100644 index 0000000..370a958 --- /dev/null +++ b/src/components/AIChatModuleVue2/aiAssistant.js @@ -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} 知识库查询结果 + */ + 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} 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 }; diff --git a/src/main.js b/src/main.js index 966ce94..1a3081e 100644 --- a/src/main.js +++ b/src/main.js @@ -14,6 +14,7 @@ Vue.use(ElementUI); + Vue.config.productionTip = false diff --git a/src/views/Book.vue b/src/views/Book.vue index ff18e03..0bdc023 100644 --- a/src/views/Book.vue +++ b/src/views/Book.vue @@ -78,6 +78,7 @@ + @@ -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 { diff --git a/vue.config.js b/vue.config.js index 7aa1c66..5ae9cad 100644 --- a/vue.config.js +++ b/vue.config.js @@ -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: {