liyanbo 3 месяцев назад
Родитель
Сommit
b29c0dfb1b

+ 8 - 0
src/api/questions.js

@@ -92,3 +92,11 @@ export async function VideoGetMys(ids) {
   })
 }
 
+// 视觉理解
+export function VisionThink (data){
+  return axios({
+    url: "bjdxWeb/ai/vision-think",
+    method: 'post',
+    data
+  })
+}

+ 1 - 0
src/api/teachers.js

@@ -16,6 +16,7 @@ export const ModelTypeEnum = {
   IMAGE_TO_IMAGE: 7, // 图生图
   TEXT_TO_VIDEO: 4, // 文生视频
   IMAGE_TO_VIDEO: 4, // 图生视频
+  VISION_THINK: 9, // 视觉理解
 }
 // 模型类型枚举
 export const ModelPlatformEnum = {

+ 2 - 2
src/components/ai/image/Avatar.vue

@@ -131,7 +131,7 @@ import {Message} from "@/utils/message/Message.js";
 import ImageUpload from '@/components/ImageUpload/index.vue';
 
 // 导入接口
-import { getModelIdByType } from '@/api/teachers.js'
+import {getModelIdByType, ModelPlatformEnum} from '@/api/teachers.js'
 import { ModelTypeEnum } from '@/api/teachers.js'
 
 // 存储上传的图片
@@ -167,7 +167,7 @@ onMounted(async () => {
         brpProgress: 1
       });
       // 获取modelId
-    const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_IMAGE, platform: "DouBao" })
+    const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_IMAGE, platform: ModelPlatformEnum.DOUBAO })
     modelId.value = modelRes.data
   }catch(error){
     console.error('保存记录失败:', error);

+ 2 - 2
src/components/ai/video/ImageToVideo.vue

@@ -115,7 +115,7 @@ import {Message} from "@/utils/message/Message.js";
 // 上传参考图
 import ImageUpload from '@/components/ImageUpload/index.vue';
 // 导入getModelIdByType接口
-import { getModelIdByType } from '@/api/teachers.js'
+import {getModelIdByType, ModelPlatformEnum} from '@/api/teachers.js'
 import { ModelTypeEnum } from '@/api/teachers.js'
 
 // 定义props
@@ -153,7 +153,7 @@ onMounted(async () => {
   gradeId.value = globalState.initGradeId()
   try{
     // 获取modelId
-    const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_VIDEO, platform: "DouBao" })
+    const modelRes = await getModelIdByType({ type: ModelTypeEnum.IMAGE_TO_VIDEO, platform: ModelPlatformEnum.DOUBAO })
     modelId.value = modelRes.data
   }catch(error){
     console.error('保存记录失败:', error);

+ 561 - 0
src/components/ai/vision/VisionThink.vue

@@ -0,0 +1,561 @@
+<template>
+  <!-- 右侧AI问答 -->
+  <div class="number-people">
+    <div class="content-box">
+      <!-- AI对话框 -->
+      <div class="chat-dialog">
+        <!-- 对话消息列表 -->
+        <div class="message-list">
+          <div v-if="imageAllList.length > 0">
+            <div  v-for="(item, index) in imageAllList" :key="index">
+              <!-- 用户消息 -->
+              <div class="user-message" v-if="item.type === 'user'">
+                {{ item.content }}
+                <div class="user-image-list" v-if="item.imageUrl">
+                  <el-image
+                      style="width: fit-content; height: 180px; margin: 10px;"
+                      :src="item.imageUrl"
+                      :preview-src-list="[item.imageUrl]"
+                      fit="cover"
+                      show-progress
+                  >
+                    <template
+                        #toolbar="{ actions, prev, next, reset, activeIndex, setActiveItem }"
+                    >
+                      <el-icon @click="prev"><Back /></el-icon>
+                      <el-icon @click="next"><Right /></el-icon>
+                      <el-icon @click="setActiveItem(item.imageList.length - 1)">
+                        <DArrowRight />
+                      </el-icon>
+                      <el-icon @click="actions('zoomOut')"><ZoomOut /></el-icon>
+                      <el-icon @click="actions('zoomIn', { enableTransition: false, zoomRate: 2 })"><ZoomIn /></el-icon>
+                      <el-icon @click="actions('clockwise', { rotateDeg: 180, enableTransition: false })"><RefreshRight /></el-icon>
+                      <el-icon @click="actions('anticlockwise')"><RefreshLeft /></el-icon>
+                      <el-icon @click="reset"><Refresh /></el-icon>
+                      <el-icon @click="download(activeIndex)"><Download /></el-icon>
+                    </template>
+                  </el-image>
+                </div>
+              </div>
+              <!-- AI生成图片对话框 -->
+              <div class="ai-message" v-if="item.type !== 'user'">
+                {{ item.content }}
+                <span v-if="item.loading" class="loading-dots">
+                    <span class="dot"></span>
+                    <span class="dot"></span>
+                    <span class="dot"></span>
+                  </span>
+                <!-- 显示AI返回的HTML内容 -->
+                <div v-if="item.htmlContent" v-html="item.htmlContent" class="ai-html-content"></div>
+                <div class="image-list" v-if="item.imageList && item.imageList.length > 0">
+                  <video
+                      v-for="(video, index) in item.imageList"
+                      :key="index"
+                      style="width: 60%; max-width: 600px; height: auto; margin: 10px;border-radius: 2%;"
+                      :src="video"
+                      controls
+                      playsinline
+                  >
+                    您的浏览器不支持视频播放
+                  </video>
+                </div>
+              </div>
+
+            </div>
+          </div>
+        </div>
+
+        <!-- 输入框和发送按钮 -->
+        <div class="input-section">
+          <input
+              type="text"
+              v-model="displayedPrompt"
+              placeholder="描述任何画面..."
+              @keyup.enter="sendMessage"
+              style="flex: 1; margin-right: 8px;"
+          />
+          <ImageUpload v-model="uploadedImage" ref="imageUploadRef"/>
+          <!-- 语音输入按钮 -->
+          <VoiceInput
+              @voiceRecognized="handleVoiceRecognized"
+              @recordingStatusChanged="handleRecordingStatusChanged"
+              lang="zh-CN"
+              maxDuration="10"
+          />
+
+          <!-- 终止按钮 -->
+          <div
+              v-if="conversationInProgress"
+              @click="stopStream"
+              class="stop-btn"
+              title="终止问答"
+          >
+            <img :src="stopicon" alt="停止" />
+          </div>
+          <button v-if="!conversationInProgress"
+                  @click="sendMessage">发送</button>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import {ref, onMounted, defineEmits, computed} from 'vue'
+import {VisionThink} from '@/api/questions.js'
+import { useRouter } from 'vue-router'
+
+import VoiceInput from '../voice/VoiceInput.vue'
+
+// 导入全局状态
+import { globalState } from '@/utils/globalState.js'
+// 终止按钮
+import stopicon from "@/assets/icon/stopicon.png";
+// 消息组件
+import {Message} from "@/utils/message/Message.js";
+
+// 上传参考图
+import ImageUpload from '@/components/ImageUpload/index.vue';
+// 导入getModelIdByType接口
+import {getModelIdByType, ModelPlatformEnum} from '@/api/teachers.js'
+import { ModelTypeEnum } from '@/api/teachers.js'
+
+// 定义props
+const props = defineProps({
+  //根据需求传输
+  isCourse: { type: Boolean, default: false},
+  cacheDataKey: { type: String, default: localStorage.getItem('token') + "_ai_visionThink" },
+  cacheDataHistoryKey: { type: String, default: localStorage.getItem('token') + "_ai_visionThink_history" },
+  preDialogueList: { type: Array, default: () => []},
+  replySupplement: { type: String, default: ''},
+})
+
+// 定义emits
+const emits = defineEmits(['saveProgress'])
+
+//历史记录缓存集合
+const cacheDataHistoryList = ref([]);
+
+// 存储上传的图片
+const uploadedImage = ref('https://learn-ai.com.cn/admin-api/infra/file/29/get/20251217/网页地图level1-32_1765957974334.png');
+const imageUploadRef = ref(null);
+
+// 对话状态变量
+const conversationInProgress = ref(false); // 对话是否正在进行中
+const router = useRouter()
+
+// modelId响应式变量
+const modelId = ref(0)
+// 保存记录
+onMounted(async () => {
+  try{
+    // 获取modelId
+    const modelRes = await getModelIdByType({ type: ModelTypeEnum.VISION_THINK, platform: ModelPlatformEnum.DOUBAO })
+    modelId.value = modelRes.data
+  }catch(error){
+    console.error('保存记录失败:', error);
+  }
+
+  // 取缓存历史记录
+  let cacheDataHistoryStr = localStorage.getItem(props.cacheDataHistoryKey);
+  if (cacheDataHistoryStr) {
+    cacheDataHistoryList.value = JSON.parse(cacheDataHistoryStr) || [];
+    imageAllList.value.push(...cacheDataHistoryList.value);
+  }else{
+    // 加入预置对话
+    imageAllList.value.push(...props.preDialogueList);
+    cacheDataHistoryList.value.push(...props.preDialogueList);
+    if (cacheDataHistoryList.value.length > 0) localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+  }
+});
+
+// 消息列表和输入内容的响应式变量
+const messages = ref([])
+const inputMessage = ref('')
+
+// 语音输入状态跟踪
+const isVoiceRecording = ref(false); // 当前是否正在录音
+const voiceRecognizedText = ref(""); // 实时语音识别结果
+
+// 用于控制输入框显示的内容
+const displayedPrompt = computed({
+  get() {
+    // 录音时,显示inputMessage.value + 实时语音识别结果
+    if (isVoiceRecording.value) {
+      return inputMessage.value ? `${inputMessage.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+    }
+    // 不录音时,只显示inputMessage.value
+    return inputMessage.value;
+  },
+  set(newValue) {
+    // 只在用户手动输入时更新inputMessage.value
+    if (!isVoiceRecording.value) {
+      inputMessage.value = newValue;
+    }
+  }
+});
+
+// 语音输入识别结果处理
+const handleVoiceRecognized = (text) => {
+  if (isVoiceRecording.value) {
+    // 在同一次录音过程中,只更新临时变量,不修改inputMessage.value
+    voiceRecognizedText.value = text;
+  } else {
+    // 在录音结束时,将最终的语音内容追加到inputMessage.value
+    inputMessage.value = inputMessage.value ? `${inputMessage.value} ${text}` : text;
+    // 清空临时变量
+    voiceRecognizedText.value = "";
+  }
+};
+
+// 处理录音状态变化
+const handleRecordingStatusChanged = (status) => {
+  const wasRecording = isVoiceRecording.value;
+  isVoiceRecording.value = status;
+
+  // 如果是从录音状态切换到非录音状态,需要将临时的语音识别结果追加到inputMessage.value
+  if (wasRecording && !isVoiceRecording.value) {
+    if (voiceRecognizedText.value) {
+      inputMessage.value = inputMessage.value ? `${inputMessage.value} ${voiceRecognizedText.value}` : voiceRecognizedText.value;
+      // 清空临时变量
+      voiceRecognizedText.value = "";
+    }
+  }
+};
+
+// 停止操作函数
+const stopStream = async () => {
+  // 直接设置为 false
+  conversationInProgress.value = false;
+};
+
+// 发送消息函数
+const sendMessage = async() => {
+  if (uploadedImage.value) {
+    // 标记对话进行中
+    conversationInProgress.value = true;
+    // 先保存内容 再置空输入框
+    let content = inputMessage.value;
+    inputMessage.value = ''
+    // 创建用户消息对象,包含可能的图片
+    const userMessage = {
+      type: 'user',
+      content: content,
+    };
+
+    // 如果有上传的图片,添加到用户消息中
+    if (uploadedImage.value) {
+      userMessage.imageUrl = uploadedImage.value;
+    }
+    // 添加用户消息到消息列表
+    imageAllList.value.push(userMessage);
+    imageAllList.value.push({
+      type: 'ai',
+      content: "正在处理您的请求,请稍等",
+      loading: true
+    })
+
+    // 通过emit事件通知父组件保存进度
+    emits('saveProgress', "aiCount", 1)
+    if (props.isCourse){
+      emits('saveProgress', "course", 100)
+    }
+
+    // 同步历史记录缓存
+    cacheDataHistoryList.value.push(userMessage);
+    localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+
+    try {
+      
+      const res = await VisionThink({
+        "modelId": modelId.value,
+        "prompt":content,
+        "promptImage":[uploadedImage.value]
+      });
+      console.log("视觉理解结果", res);
+
+      // 移除加载中的消息
+      imageAllList.value.pop();
+
+      // 添加AI返回的HTML内容
+      let aiMsg = {
+        type: 'ai',
+        content: "",
+        htmlContent: res.data?.result || "请求失败,请稍后重试!",
+      };
+      imageAllList.value.push(aiMsg);
+
+      // 添加补充信息
+      if (props.replySupplement) {
+        imageAllList.value.push({
+          type: "system",
+          content: props.replySupplement,
+        });
+      }
+
+      // 同步缓存历史记录
+      cacheDataHistoryList.value.push(aiMsg);
+      localStorage.setItem(props.cacheDataHistoryKey, JSON.stringify(cacheDataHistoryList.value));
+    } catch (error) {
+      console.error('视觉理解失败:', error);
+      // 移除加载中的消息
+      imageAllList.value.pop();
+      // 添加错误消息
+      imageAllList.value.push({
+        type: 'ai',
+        content: "处理失败,请稍后重试"
+      });
+    } finally {
+      // 图片生成请求完成后更新状态
+      conversationInProgress.value = false;
+    }
+  } else {
+    // 如果没有上传图片,显示提示信息
+    Message().error('请先上传参考图!', true);
+  }
+  // 调用子组件的方法清除预览图
+  imageUploadRef.value?.clearPreview();
+};
+
+
+// 生成图片
+import { ElIcon } from 'element-plus'
+import {
+  Back,
+  DArrowRight,
+  Download,
+  Refresh,
+  RefreshLeft,
+  RefreshRight,
+  Right,
+  ZoomIn,
+  ZoomOut,
+} from '@element-plus/icons-vue'
+
+
+const imageAllList = ref([]) // 对话的消息列表
+const imageList = ref([]) // image 列表
+</script>
+
+<style scoped lang="scss">
+@use 'sass:math';
+// 定义rpx转换函数
+@function rpx($px) {
+  @return math.div($px, 750) * 100vw;
+}
+// 用户图片列表样式
+.user-image-list {
+  display: flex;
+  flex-wrap: wrap;
+  margin-top: rpx(5);
+}
+//===========================================全局除了删除侧边栏内容唯一改动的地方
+.number-people {
+  flex: 1;
+  height: 100%;
+  display: flex;
+  background-color: #ece9fd;
+}
+
+.content-box {
+  flex: 1;
+  // margin-top: rpx(10);
+  // margin-bottom: rpx(10);
+  margin: rpx(7);
+  border-radius: rpx(15);
+  background: rgba($color: #ffffff, $alpha: 0.5);
+  overflow-y: auto;
+}
+
+//左侧展览区图标
+.img-box {
+  margin-top: rpx(50);
+  color: #a39dce;
+}
+// 对话框
+.chat-dialog {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+.message-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: rpx(15);
+}
+/* 自定义滚动条样式 */
+.message-list::-webkit-scrollbar {
+  width: rpx(2); /* 滚动条宽度 */
+}
+.message-list::-webkit-scrollbar-track {
+  background: #f1effd; /* 滚动条轨道背景色 */
+  border-radius: rpx(4);
+}
+.message-list::-webkit-scrollbar-thumb {
+  background: #e2ddfc; /* 滚动条滑块颜色 */
+  border-radius: rpx(4);
+}
+.message-list::-webkit-scrollbar-thumb:hover {
+  background: #e2ddfc; /* 滚动条滑块 hover 状态颜色 */
+}
+
+.message-list .user-message {
+  background-color: #ffffff;
+  margin-left: auto; // 消息靠右显示
+  margin-right: 0; // 重置右边距
+  max-width: rpx(400);
+  font-size: rpx(8);
+  width: fit-content; // 宽度随文字内容变化
+  border-radius: rpx(5);
+  padding: rpx(5);
+  text-align: left; // 文字左对齐
+}
+
+.message-list .ai-message {
+  background-color: #ffdd55;
+  margin-left: 0; // 消息靠左显示
+  margin-right: auto; // 重置右边距
+  margin-bottom: rpx(10);
+  width: fit-content;
+  max-width: rpx(400);
+  padding: rpx(5);
+  font-size: rpx(8);
+  border-radius: rpx(5);
+  text-align: left; // 文字左对齐
+}
+
+// 加载动画效果
+.loading-dots {
+  display: inline-block;
+  margin-left: rpx(5);
+}
+
+.loading-dots .dot {
+  display: inline-block;
+  width: rpx(3);
+  height: rpx(3);
+  border-radius: 50%;
+  background-color: #333;
+  margin: 0 rpx(1);
+  animation: loading-dot 1.4s infinite ease-in-out both;
+}
+
+.loading-dots .dot:nth-child(1) {
+  animation-delay: -0.32s;
+}
+
+.loading-dots .dot:nth-child(2) {
+  animation-delay: -0.16s;
+}
+
+@keyframes loading-dot {
+  0%, 80%, 100% {
+    transform: scale(0);
+  }
+  40% {
+    transform: scale(1);
+  }
+}
+
+
+.image-list {
+  display: flex;
+  flex-wrap: wrap;
+}
+
+/* AI返回的HTML内容样式 */
+.ai-html-content {
+  margin-top: 10px;
+  padding: 10px;
+  border-radius: 5px;
+  background-color: rgba(255, 255, 255, 0.8);
+  max-width: 100%;
+  overflow-x: auto;
+}
+
+/* 确保HTML内容中的图片适应容器 */
+.ai-html-content img {
+  max-width: 100%;
+  height: auto;
+  border-radius: 3px;
+}
+
+/* 确保HTML内容中的文本样式 */
+.ai-html-content p {
+  margin: 5px 0;
+  line-height: 1.5;
+}
+
+
+.content-demo {
+  background-color: #f4f2fa;
+  border-radius: 15px;
+  padding: 30px 10px;
+}
+
+.input-section {
+  display: flex;
+  padding: rpx(10);
+  gap: rpx(5);
+  .speech-btn {
+    padding: rpx(5) rpx(10);
+    background: #fff;
+    border: 1px solid #ffce1b;
+    border-radius: rpx(5);
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    &.recording {
+      background: #ffeeba;
+      border-color: #ffc107;
+
+      .el-icon {
+        color: #dc3545;
+      }
+    }
+    .el-icon {
+      font-size: rpx(8);
+      color: #666;
+    }
+  }
+  // 终止按钮样式
+  .stop-btn {
+    cursor: pointer;
+    display: flex;
+    align-items: center;
+    img {
+      width: rpx(20);
+      height: rpx(20);
+    }
+  }
+}
+.input-section input {
+  flex: 1;
+  padding: rpx(5);
+  font-size: rpx(7);
+  border: 1px solid #ccc;
+  border-radius: rpx(5);
+}
+.input-section button {
+  padding: rpx(5) rpx(15);
+  background: linear-gradient(
+          to bottom,
+          #fee78a,
+          #ffce1b
+  ); /* 设置悬停、聚焦、点击状态下的背景色 */
+  color: black;
+  border: none;
+  font-size: rpx(7);
+  border-radius: rpx(5);
+  cursor: pointer;
+  box-shadow: 0 0px 2px rgba(0, 0, 0, 0.3);
+
+}
+
+.image-upload-section {
+  padding: rpx(10);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+</style>

+ 1 - 1
src/utils/request.js

@@ -10,7 +10,7 @@ import router, {homeRoutes} from '@/router/index.js'
 const isDev = process.env.NODE_ENV == 'development'
 
 // 修正赋值逻辑,使用逻辑或运算符
-const BASE_URL = import.meta.env.VITE_BASE_URL || 'http://192.168.110.8:8080/admin-api'
+const BASE_URL = import.meta.env.VITE_BASE_URL || 'http://192.168.110.9:8080/admin-api'
 
 // 创建一个请求对象
 const request = axios.create({

+ 2 - 3
src/views/AIPage/PlantExperts.vue

@@ -38,12 +38,11 @@
               class="avatar user"
           />
         </p>
-        <p>期待你的画作喔~</p>
       </div>
     </div>
 
     <!-- 右侧图生视频组件 -->
-    <ImageToVideo />
+    <VisionThink />
     
   </div>
 </template>
@@ -53,8 +52,8 @@ import { ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { ArrowLeftBold } from '@element-plus/icons-vue'
 import LeftPanel from '@/components/LeftPanel.vue'
-import ImageToVideo from '@/components/ai/video/ImageToVideo.vue'
 import {homeRoutes} from "@/router/index.js";
+import VisionThink from "@/components/ai/vision/VisionThink.vue";
 
 const router = useRouter()
 const leftPanelRef = ref(null)