去皮卡多 发表于 2025-2-27 08:57:55

人工智能之web前端开发(deepSeek与文心一言结合版)

一.项目功能:


[*]智能问答(实时谈天+流通打字机效果+自动滚动)
[*]克制天生(取消接口调用)、重新天生
[*]复制功能、问答分页
二.效果展示:

https://i-blog.csdnimg.cn/direct/9e9aff542886421ba87a801bfee76af5.png
三.技术分析:


[*] fetchEventSource:传统axios哀求是等接口将所有数据一次性相应回来后再渲染到页面上,当数据量较大时,相应速度较慢,且无法做到实时输出。而fetchEventSource允许客户端吸收来自服务器的实时更新,前端可以实时的将流式数据展示到页面上,类似于打字机的效果。
fetchEventSource(url, {
      method: "GET",
      headers: {
          "Content-type": "application/json",
          Accept: "text/event-stream"
      },
      openWhenHidden: true,
      onopen: (e) => {
          //接口请求成功,但此时数据还未响应回来
      },
      onmessage: (event) => {
         //响应数据持续数据
      },
      onclose: () => {
         //请求关闭
      },
      onerror: () => {
          //请求错误
      }
      })
[*] MarkdownIt :SSE相应的数据格式是markdown,无法直接展示,需要使用MarkdownIt第三方库转换成html,然后通过v-model展示到页面上。
    // 1、新建实例md:
    const md = new MarkdownIt()
    // 2.将markdown转化为html
    const htmlStr= md.render(markdownStr)
[*] Clipboard+html-to-text:复制时,需要使用html-to-text第三方库将html转化为text,然后借助Clipboard复制到粘贴板上。
//1.在html中设置“copy”类名,并绑定data-clipboard-text
<el-icon class="copy" @click="copyFn(copyHtmlStr)" :data-clipboard-text="copyText"> </el-icon>

//2.先将html转化成text,然后复制到粘贴板
const copyFn = (copyHtmlStr) => {
   copyText.value=htmlToText(copyHtmlStr)
      const clipboard = new Clipboard(".copy")
      // 成功
      clipboard.on("success", function (e) {
      ElMessage.success("复制成功")
      e.clearSelection()
      // 释放内存
      clipboard.destroy()
      })
      // 失败
      clipboard.on("error", function (e) {
      ElMessage.error("复制失败")
      clipboard.destroy()
      })
    }
[*] scrollEvent:由于数据流式输出,页面内容持续增加,大概会溢出屏幕,因此需要在fetchEventSource吸收消息onmessage的过程中,通过设置scrollTop =scrollHeight让页面实现自动滚动。
fetchEventSource(url, {
      ...,
      onmessage: (event) => {
         chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
      },
      ...
      })
四:疑难点及办理方案:

1.  题目形貌:当页面哀求fetchEventSource已发出时,切换url到其他网站再切换回来到这个页面时,fetchEventSource会重复哀求,导致这两次哀求的内容重复。
     办理方案:设置openWhenHidden为true,表示当页面退至后台时仍保持连接,默认值为false
2.  题目形貌:前端调用AbortController的abort()方法取消哀求时,只有第一次取消见效,当重新哀求时,再次点击克制按钮不见效。
    办理方案:每哀求一次创建一个新的AbortController()实例,由于AbortController实例的abort()方法被设计为只能调用一次来取消哀求,一旦调用了abort(),与AbortController干系的AbortSigal的aborted属性就会被设置成true,表示哀求已取消,当再次调用abort()不会有任何效果。
https://i-blog.csdnimg.cn/direct/8fbbdc8b5b0e44e6b22d69b579c7d60a.png
3.  题目形貌:当在fetchEventSource的onmessage中设置scrollTop =scrollHeight时,在天生题目的过程中无法向上滚动,但业务想要边天生边滚动检察。
    办理方案:监听鼠标滚轮事件,在设置scrollTop =scrollHeight时添加判断,假如鼠标滚轮滑动且未到页面底部,则不自动滚动。
const isRolling = ref(false) //鼠标滚轮是否滚动
const isBottom = ref(false) //滚动参数

// 处理鼠标滚轮事件
const moveWheel1 = ref(true)
const moveWheel2 = ref(false)
const wheelClock = ref()
const stopWheel=()=> {
      if (moveWheel2.value == true) {
      moveWheel2.value = false
      moveWheel1.value = true
      }
    }
const moveWheel=()=> {
      if (moveWheel1.value == true) {
      isRolling.value = true
      moveWheel1.value = false
      moveWheel2.value = true
      //这里写开始滚动时调用的方法
      wheelClock.value = setTimeout(stopWheel, 200)
      } else {
      clearTimeout(wheelClock.value)
      wheelClock.value = setTimeout(stopWheel, 150)
      }
    }
const sendFn=()=>{
   fetchEventSource(url, {
      ...,
      onmessage: (event) => {
            if (isRolling.value === false) {
            chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
            isRolling.value = false
            }
            if (isBottom.value) {
            isRolling.value = false
            chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
            }
      },
      ...
      })
}


4.  题目形貌:SSE相应回来的数据中表格样式未见效。
     办理方案:MarkdownIt第三方库将markdown转换成html时,部门样式会丢失,需使用github-markdown-css添加样式。
npm i github-markdown-css
import "github-markdown-css"
五.完整代码示例:

   index.vue:
<template>
<div class="report-page">
    <div class="chat-container" ref="chatContainerRef" @scroll="scrollEvent">
      <div v-for="(item, index) in chatList" :key="index" class="chat-list-container">
      <div class="chat-item" :class="item.type === 'user' ? 'user' : 'ai'">
          <div v-if="item.type === 'user'" class="question">
            <div class="message" v-if="item.message && item.message.length > 0">{{ item.message }}</div>
            <img class="avatar" src="../../assets/chat/userAvatar.png" alt="" />
          </div>
          <div v-else-if="item.type === 'ai'">
            <div v-if="item.message.length > 0 || item.isLoading" class="answer-container">
            <div class="answer">
                <div class="avatar-page">
                  <div><img class="avatar" src="../../assets/chat/aiAvatar.png" alt="" /></div>
                  <div class="page-container" v-if="item.message.length > 1">
                  <span class="pre-page" @click="preFn(index, item.answerIndex)">&lt;</span
                  >{{ item.answerIndex + 1 }} / {{ item.message.length
                  }}<span class="next-page" @click="nextFn(index, item.answerIndex)">&gt;</span>
                  </div>
                </div>

                <div class="answer-message">
                  <div v-if="item.isLoading">
                  <div v-html="currentHTML" class="markdown-body" />
                  </div>
                  <div v-else>
                  <div v-html="item.message" class="markdown-body" />
                  <!-- <div v-if="item.reportFlag === 1">
                      <el-button @click="downloadReport(index, item.answerIndex)" class="download-btn">
                        <SvgIcon class="icon-img" name="download" />
                        下载尽调报告</el-button
                      >
                  </div> -->
                  </div>
                </div>
            </div>
            <div class="btn-container">
                <div class="opt-container">
                  <div v-if="index === chatList.length - 1">
                  <!-- <span v-if="item.isLoading" class="stop-btn" @click="stopFn">停止生成</span> -->
                  <span v-if="!item.isLoading && preInputValue" class="regenerate-btn" @click="regenerateFn"
                      >重新生成</span
                  >
                  </div>
                </div>
                <div class="tool-container" v-if="!item.isLoading">
                  <el-icon
                  class="copy"
                  :class="copyIndex === index ? 'copy-acive' : ''"
                  @click="copyFn(index, item.answerIndex)"
                  :data-clipboard-text="copyText"
                  >
                  <CopyDocument />
                  </el-icon>
                </div>
            </div>
            </div>
          </div>
      </div>
      </div>
      <div class="loading-status" v-if="loadingStatus">
      <div><img class="avatar" src="../../assets/chat/aiAvatar.png" alt="" /></div>
      <div class="think">思考中…</div>
      <div><img class="loading-img" src="../../assets/chat/loading.gif" alt="" /></div>
      </div>
      <div v-if="!isLoading">
      <div class="scroll-container scroll-top" v-if="scrollTopShow">
          <el-icon style="vertical-align: middle" @click="scrollTopFn">
            <ArrowUp />
          </el-icon>
      </div>
      <div class="scroll-container scroll-bottom" v-else-if="scrollBottomShow">
          <el-icon style="vertical-align: middle" @click="scrollBottomFn">
            <ArrowDown />
          </el-icon>
      </div>
      </div>
    </div>
    <div class="input-container">
      <div class="stop-container" @click="stopFn" v-if="isLoading">
      <img class="stop-img" src="../../assets/chat/stop.png" alt="" />停止生成
      </div>
      <el-input
      class="input"
      type="textarea"
      v-model="inputValue"
      placeholder="你可以这样问:请写一份江苏省xxx有限公司的尽调报告"
      :autosize="{ minRows: 1, maxRows: 11 }"
      @keydown.enter.native="inputBlurFn($event)"
      />
      <div class="icon-container" :style="{ cursor: isLoading ? '' : 'pointer' }" @click="inputBlurFn">
      <img v-if="isLoading" class="loading-icon" src="../../assets/chat/loading.gif" alt="" />
      <img v-else-if="!isLoading && inputValue" src="../../assets/chat/send.svg" class="send-icon" alt="" />
      <img v-else src="../../assets/chat/unsend.svg" class="send-icon" alt="" />
      </div>
      <Agreement />
    </div>
    <ViewDialog ref="viewDialogRef" />
</div>
</template>
<script lang="ts" src="./index.ts"></script>
   index.ts:
import "./index.scss"
import { defineComponent, ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue"
import { fetchEventSource } from "@microsoft/fetch-event-source"
import { ElMessageBox, ElMessage } from "element-plus"
import MarkdownIt from "markdown-it"
import "github-markdown-css"
import Clipboard from "clipboard"
import { htmlToText } from "html-to-text"
import type { ChatItem } from "../../../types/chat"
export default defineComponent({
components: { },
setup() {
    const inputValue = ref("")
    const preInputValue = ref("") //上一次查询输入内容,用于重新生成使用
    const isLoading = ref(false) //节流loading
    const loadingStatus = ref(false) //加载状态显示loading
    const chatList = ref<ChatItem[]>([])
    const contentItems = ref("") //当前正在输出的数据流
    const chatContainerRef = ref()
    const isRegenerate = ref(false) //是否重新生成
    const controller = ref()
    const signal = ref()
    const copyText = ref("") //复制的文字
    const copyIndex = ref<number>()
    const scrollTopShow = ref(false)
    const scrollBottomShow = ref(false)
    const isRolling = ref(false) //鼠标滚轮是否滚动
    const isBottom = ref(false) //滚动参数
    onMounted(() => {
      initFn()
      chatContainerRef.value.addEventListener("wheel", moveWheel)
      window.addEventListener("message", function (event) {
      // 处理接收到的消息
      if (event.data && event.data.message) {
          inputValue.value = event.data.message
          sendFn()
      }
      })
    })
    // 处理鼠标滚轮事件
    const moveWheel1 = ref(true)
    const moveWheel2 = ref(false)
    const wheelClock = ref()
    function stopWheel() {
      if (moveWheel2.value == true) {
      // console.log("滚轮停止了")
      // isRolling.value = false
      moveWheel2.value = false
      moveWheel1.value = true
      }
    }
    function moveWheel() {
      if (moveWheel1.value == true) {
      // console.log("滚动了")
      isRolling.value = true
      moveWheel1.value = false
      moveWheel2.value = true
      //这里写开始滚动时调用的方法
      wheelClock.value = setTimeout(stopWheel, 200)
      } else {
      clearTimeout(wheelClock.value)
      wheelClock.value = setTimeout(stopWheel, 150)
      }
    }

    //初始化
    const initFn = () => {
      chatList.value = []
    }
    //上一页
    const preFn = (index: number, answerIndex: number) => {
      if (isLoading.value) return ElMessage.error("正在生成内容,请勿切换。")
      if (answerIndex === 0) {
      chatList.value.answerIndex = chatList.value.message.length - 1
      } else {
      chatList.value.answerIndex = chatList.value.answerIndex - 1
      }
    }
    //下一页
    const nextFn = (index: number, answerIndex: number) => {
      if (isLoading.value) return ElMessage.error("正在生成内容,请勿切换。")
      if (answerIndex === chatList.value.message.length - 1) {
      chatList.value.answerIndex = 0
      } else {
      chatList.value.answerIndex = chatList.value.answerIndex + 1
      }
    }
    // 1、新建实例md:
    const md = new MarkdownIt()
    const currentHTML = computed(() => {
      // 先判断存不存在,因为一开始currentPost有可能是undefined,在没有拿回数据的时候。
      if (contentItems.value) {
      if (contentItems.value.includes("</sy_think>")) {
          const arr = contentItems.value.split("</sy_think>")
          const thinkStr = `
          <h4> 师爷模型深度思考中...</h4>
          <div style="color: gray;font-size:14px;padding-left:10px;margin-bottom:10px;line-height:25px;border-left:1px solid #e5e5e5">${arr}</div>
          <div><div>
         `
          return thinkStr + md.render(arr)
      } else {
          const thinkStr = `
          <h4> 师爷模型深度思考中...</h4>
          <div style="color: gray;font-size:14px;padding-left:10px;margin-bottom:10px;line-height:25px;border-left:1px solid #e5e5e5">${contentItems.value}</div>
          <div><div>
          `
          return thinkStr
      }
      }
    })
    //发送问题调用接口
    const sendFn = () => {
      showList.value = false
      controller.value = new AbortController()
      signal.value = controller.value.signal
      //先判断inputStr有没有值,isRegenerate表示是否重新生成
      const inputStr = isRegenerate.value ? preInputValue.value : inputValue.value
      if (!inputStr) return ElMessage.error("请输入要查询的问题。")
      if (isLoading.value) return ElMessage.error("正在生成内容,请稍后。")
      isLoading.value = true
      if (!isRegenerate.value) {
      //第一次生成
      chatList.value.push({ type: "user", message: , answerIndex: 0, isLoading: false, reportFlag: null })
      }
      loadingStatus.value = true
      const url = `/recheck-web/open/shiye/chat?message=${inputStr}`
      fetchEventSource(url, {
      method: "GET",
      headers: {
          "Content-type": "application/json",
          Accept: "text/event-stream"
      },
      signal: signal.value,
      openWhenHidden: true,
      // params: JSON.stringify({ message: inputStr }),
      onopen: (e) => {
          if (e.status === 500) return ElMessage.error("服务器忙,请稍后再试。")
          if (isRegenerate.value) {
            //重新生成
            chatList.value.message.push("")
            chatList.value.answerIndex =
            chatList.value.message.length - 1
          } else {
            chatList.value.push({ type: "ai", message: [], answerIndex: 0, isLoading: true })
            preInputValue.value = inputValue.value
          }
          chatList.value.isLoading = true
          inputValue.value = ""
          isLoading.value = true
          loadingStatus.value = false
      },
      onmessage: (event) => {
          const data = JSON.parse(event.data)
          const newItem = data ? data.content : ""
          contentItems.value = contentItems.value + newItem
          if (data.status !== "end") {
          } else {
            if (isRegenerate.value) {
            //重新生成
            chatList.value.message[
                chatList.value.message.length - 1
            ] = currentHTML.value + ""
            } else {
            //第一次生成
            chatList.value.message.push(currentHTML.value + "")
            }
            if (data.type) {
            chatList.value.reportFlag = data.type
            }
          }
          nextTick(() => {
            if (isRolling.value === false) {
            chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
            isRolling.value = false
            }
            if (isBottom.value) {
            isRolling.value = false
            chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight
            }
          })
      },
      onclose: () => {
          isLoading.value = false
          loadingStatus.value = false
          chatList.value.isLoading = false
          contentItems.value = ""
      },
      onerror: () => {
          isLoading.value = false
          loadingStatus.value = false
          chatList.value.isLoading = false
          contentItems.value = ""
      }
      })
    }
    //停止生成
    const stopFn = () => {
      isLoading.value = false
      loadingStatus.value = false
      controller.value.abort()
      //chatList最后一项
      const lastChatItem = chatList.value
      if (isRegenerate.value) {
      // lastChatItem.message = md.render(contentItems.value + "\n" + "\n" + "停止生成")
      lastChatItem.message =
          currentHTML.value + "<div style='font-size:16px;margin-top:10px'>停止生成</div>"
      } else {
      // lastChatItem.message.push(md.render(contentItems.value + "\n" + "\n" + "停止生成"))
      lastChatItem.message.push(currentHTML.value + "<div style='font-size:16px;margin-top:10px'>停止生成</div>")
      }
      contentItems.value = ""
      lastChatItem.isLoading = false
    }
    //重新生成
    const regenerateFn = () => {
      isRegenerate.value = true
      sendFn()
    }
    //发送
    const inputBlurFn = (event: any) => {
      if (!event.ctrlKey) {
      // 如果没有按下组合键ctrl,则会阻止默认事件
      event.preventDefault()
      isRegenerate.value = false
      sendFn()
      } else {
      // 如果同时按下ctrl+回车键,则会换行
      inputValue.value += "\n"
      }
    }
    //复制功能
    const copyFn = (index: number, answerIndex: number) => {
      copyIndex.value = index
      copyText.value = htmlToText(chatList.value.message)
      const clipboard = new Clipboard(".copy")
      // 成功
      clipboard.on("success", function (e) {
      ElMessage.success("复制成功")
      e.clearSelection()
      // 释放内存
      clipboard.destroy()
      })
      // 失败
      clipboard.on("error", function (e) {
      ElMessage.error("复制失败")
      clipboard.destroy()
      })
    }
    //试问
    const askFn = (question: string) => {
      inputValue.value = question
      isRegenerate.value = false
      sendFn()
    }
    //滚动事件
    const scrollEvent = (e: any) => {
      //如果滚动到底部,显示向上滚动按钮
      //如果滚动到顶部,显示向下滚动按钮
      const scrollTop = e.target.scrollTop
      const scrollHeight = e.target.scrollHeight
      const offsetHeight = Math.ceil(e.target.getBoundingClientRect().height)
      const currentHeight = scrollTop + offsetHeight

      if (currentHeight >= scrollHeight) {
      scrollTopShow.value = true
      isBottom.value = true
      } else {
      isBottom.value = false
      scrollTopShow.value = false
      }

      if (scrollHeight > offsetHeight) {
      scrollBottomShow.value = true
      } else {
      scrollBottomShow.value = false
      }
    }
    //向上滚动
    const scrollTopFn = () => {
      chatContainerRef.value.scrollTop = 0
    }
    //向下滚动
    const scrollBottomFn = () => {
      chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight + 250
    }
    //下载尽调报告
    const downloadReport = (index: number, answerIndex: number) => {
      const downStr = chatList.value.message
      const arr = downStr.split("<div><div>")
      const blob = new Blob(], { type: "text/plain" })
      const link = document.createElement("a")
      link.href = URL.createObjectURL(blob)
      link.download = "尽调报告.docx"
      link.click()
    }
    const generateReport = (question: string) => {
      inputValue.value = question
      isRegenerate.value = false
      sendFn()
    }
    const viewDialogRef = ref()
    const viewFn = () => {
      viewDialogRef.value.dialogVisible = true
    }
    watch(
      () => chatList.value,
      () => {
      if (chatList.value && chatList.value.length > 0) {
          chatContainerRef.value.scrollTop = chatContainerRef.value.scrollHeight + 250
      }
      },
      { deep: true }
    )
    onUnmounted(() => {
      if (isLoading.value) {
      isLoading.value = false
      loadingStatus.value = false
      controller.value.abort()
      }
    })
    return {
      inputValue,
      isLoading,
      chatList,
      preFn,
      nextFn,
      sendFn,
      contentItems,
      stopFn,
      preInputValue,
      currentHTML,
      chatContainerRef,
      regenerateFn,
      inputBlurFn,
      copyFn,
      copyText,
      askFn,
      loadingStatus,
      copyIndex,
      downloadReport,
      generateReport,
      showList,
      scrollEvent,
      scrollTopFn,
      scrollBottomFn,
      scrollTopShow,
      scrollBottomShow,
      viewDialogRef,
      viewFn
    }
}
})
   index.scss:
.report-page {
height: 100%;
width: 1201px;
margin: 20px calc((100% - 1201px) / 2 - 35px) 20px calc((100% - 1201px) / 2 + 35px);
.chat-container::-webkit-scrollbar {
    width: 0; /* 对于垂直滚动条 */
}
.chat-container {
    height: 85vh;
    overflow-y: auto;
    padding-bottom: 170px;
    margin-top: 0px;
    box-sizing: border-box;
    .chat-list-container {
      margin-top: 30px;
      .chat-item {
      display: flex;
      margin-bottom: 20px;
      .avatar {
          width: 40px;
          height: 40px;
      }
      .question {
          display: flex;
          margin-bottom: 30px;
          margin-left: 70px;
          margin-right: 10px;
          .message {
            padding: 12px 10px 10px;
            border-radius: 14px 0px 14px 14px;
            background: linear-gradient(128deg, #4672ff -1.27%, #7daafc 109.62%);
            color: #fff;
          }
          .avatar {
            margin-left: 20px;
          }
      }
      .answer-container {
          margin-right: 70px;
          margin-bottom: 30px;
          .answer {
            display: flex;
            .avatar-page {
            width: 70px;
            position: relative;
            .avatar {
                width: 60px;
                height: 60px;
                margin-right: 10px;
            }

            .page-container {
                position: absolute;
                top: 60px;
                left: 3px;
                color: #000;
                font-family: "PingFang SC";
                font-size: 14px;
                font-style: normal;
                font-weight: 400;
                line-height: 24px;
                .pre-page {
                  margin-right: 1px;
                  cursor: pointer;
                }
                .next-page {
                  margin-left: 1px;
                  cursor: pointer;
                }
            }
            }
            .answer-message {
            background-color: #fff;
            padding: 20px;
            border-radius: 0 14px 14px 14px;
            min-width: 500px;
            .download-btn {
                color: #333;
                text-align: center;
                font-family: "PingFang SC";
                font-size: 14px;
                font-style: normal;
                font-weight: 400;
                line-height: 14px;
                background-color: #f2f3f8;
                height: 34px;
                margin-top: 20px;
                border-radius: 6px;
                border-color: transparent;
                .icon-img {
                  width: 17px;
                  height: 17px;
                  margin-right: 5px;
                }
                &:hover {
                  background: linear-gradient(128deg, #4672ff -1.27%, #7daafc 109.62%);
                  color: #fff;
                }
            }
            }
          }
          .btn-container {
            margin-left: 80px;
            margin-top: 18px;
            text-align: left;
            display: flex;
            justify-content: space-between;
            .opt-container {
            .stop-btn,
            .regenerate-btn {
                cursor: pointer;
                color: #57f;
                font-family: "PingFang SC";
                font-size: 14px;
                font-style: normal;
                font-weight: 500;
                line-height: 24px;
            }
            }
            .tool-container {
            background-color: #fff;
            padding: 6px 10px 8px;
            height: 40px;
            border-radius: 20px;
            min-width: 70px;
            text-align: center;
            .copy {
                width: 28px;
                height: 28px;
                cursor: pointer;
            }
            .copy-acive {
                color: #5577ff;
            }
            }
          }
      }
      }
    }
    .user {
      flex-direction: row-reverse;
    }
    .loading-status {
      display: flex;
      .avatar {
      width: 60px;
      height: 60px;
      margin-right: 20px;
      }
      .think {
      height: 52px;
      line-height: 52px;
      background-color: #fff;
      text-align: center;
      border-radius: 0 14px 14px 14px;
      width: 100px;
      color: #999;
      }
      .loading-img {
      width: 40px;
      height: 40px;
      }
    }
    .scroll-container {
      width: 38px;
      height: 38px;
      background-color: #fff;
      border-radius: 19px;
      display: flex;
      justify-content: center;
      align-items: center;
      position: fixed;
      left: calc((100% - 1201px) / 2 + 175px + 1061px);
    }
    .scroll-top {
      bottom: 200px;
    }
    .scroll-bottom {
      top: 53px;
    }
}
.input-container {
    position: fixed;
    left: calc((100% - 1061px) / 2 + 35px);
    bottom: 5%;
    width: 1061px;
    .stop-container {
      cursor: pointer;
      width: 104px;
      height: 36px;
      background-color: #fff;
      color: #5863ff;
      line-height: 36px;
      text-align: center;
      border-radius: 18px;
      position: absolute;
      top: -50px;
      font-size: 14px;
      font-style: normal;
      font-weight: 400;
      .stop-img {
      width: 22px;
      height: 22px;
      vertical-align: middle;
      margin-right: 3px;
      }
    }
    .input {
      .el-textarea__inner {
      padding: 15px;
      border-radius: 14px;
      box-shadow: 14px 27px 45px 0px rgba(112, 144, 176, 0.2);
      }
      .el-textarea__inner::-webkit-scrollbar {
      width: 6px;
      height: 6px;
      }
      .el-textarea__inner::-webkit-scrollbar-thumb {
      border-radius: 3px;
      -moz-border-radius: 3px;
      -webkit-border-radius: 3px;
      background-color: #c3c3c3;
      }
      .el-textarea__inner::-webkit-scrollbar-track {
      background-color: transparent;
      }
    }
    .icon-container {
      position: absolute;
      right: 10px;
      bottom: 35px;
      z-index: 999;
      .loading-icon {
      width: 40px;
      height: 40px;
      }
      .send-icon {
      width: 35px;
      height: 35px;
      }
    }
}
}
.markdown-body {
box-sizing: border-box;
max-width: 1021px !important;
hr {
    display: none !important;
}
}




免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 人工智能之web前端开发(deepSeek与文心一言结合版)