dongyukun
21 小时以前 e295922209fb87c6dcd68ea1560fd16c3e6d808c
src/views/ai/dashboard/components/conversation/CommonConversation.vue
@@ -18,6 +18,9 @@
      ref="sidebarRef">
      <ConversationList
        :active-id="activeConversationId"
        :quick-access="quickAccessFlag"
        :model-name="modelName"
        :default-message="defaultMessage"
        ref="conversationListRef"
        @on-conversation-create="handleConversationCreateSuccess"
        @on-conversation-click="handleConversationClick"
@@ -55,9 +58,8 @@
          <!-- 情况一:消息加载中 -->
          <MessageLoading v-if="activeMessageListLoading" />
          <!-- 情况二:无聊天对话时 -->
          <MessageNewConversation
          <MessageListEmpty
            v-if="!activeConversation"
            @on-new-conversation="handleConversationCreate"
          />
          <!-- 情况三:消息列表为空 -->
          <MessageListEmpty
@@ -89,7 +91,7 @@
              @input="handlePromptInput"
              @compositionstart="onCompositionstart"
              @compositionend="onCompositionend"
              placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
              placeholder="请问我问题...(Shift+Enter 换行,按下 Enter 发送)"
            ></textarea>
            <div class="prompt-btns">
              <div class="content">
@@ -141,13 +143,21 @@
import MessageList from '../message/MessageList.vue'
import MessageListEmpty from '../message/MessageListEmpty.vue'
import MessageLoading from '../message/MessageLoading.vue'
import MessageNewConversation from '../message/MessageNewConversation.vue'
import { onClickOutside } from '@vueuse/core'
import * as authUtil from "@/utils/auth";
import {refreshToken} from "@/api/login";
import {formatToDateTime} from "@/utils/dateUtil";
import { formatReasoningContent } from '@/views/ai/utils/utils'
/** AI 聊天对话 列表 */
defineOptions({ name: 'NormalConversation' })
const props = defineProps({
  data: {
    type: Object,
    default: () => null
  }
})
const route = useRoute() // 路由
const message = useMessage() // 消息弹窗
@@ -175,8 +185,12 @@
  isCollapsed.value = !isCollapsed.value
}
const modelName = ref<string>('common') // 对话搜索
// 聊天对话
const conversationListRef = ref()
const quickAccessFlag = ref(false)
const defaultMessage = ref<ChatMessageVO>()
const activeConversationId = ref<number | null>(null) // 选中的对话编号
const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation
const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
@@ -199,7 +213,6 @@
// 接收 Stream 消息
const receiveMessageFullText = ref('')
const receiveMessageDisplayedText = ref('')
// =========== 【聊天对话】相关 ===========
@@ -298,7 +311,6 @@
    activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
      activeConversationId.value
    )
    // 滚动到最下面
    await nextTick()
    await scrollToBottom()
@@ -335,19 +347,22 @@
  return []
})
//处理调度推理结论
const dealResult = (conversations: any) => {
  const regex = /<think>(\n*)([\s\S]*?)(\n*)<\/think>(\n*)([\s\S]*)/;
  conversations.forEach((conversation) => {
    if(conversation.content.includes('<\/think>')) {
      conversation.thinkingFlag = false
    } else {
      conversation.thinkingFlag = true
    }
    const match = conversation.content.match(regex);
    if(match) {
      conversation.thinking = match[2];
      conversation.conclusion = match[5]
//处理调度推理结论(微调大模型)
const dealResult = (messages: any) => {
  messages.forEach((message) => {
    if(message.type === 'assistant') {
      const spliceText = message.content.includes("总结:") ? "总结:" : "结论:";
      // 创建同时捕获前后内容的正则表达式
      const regex = new RegExp(`^([\\s\\S]*?)${spliceText}([\\s\\S]*)$`);
      const match = message.content.match(regex);
      if(match) {
        message.thinking = match[1];
        message.conclusion = match[2]
      } else {
        message.thinking = message.content
      }
      // 处理推理思路内容
      message.thinking = formatReasoningContent(message.thinking);
    }
  })
}
@@ -455,19 +470,20 @@
    message.error('发送失败,原因:内容为空!')
    return
  }
  // 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token
  authUtil.setToken(await refreshToken())
  if (activeConversationId.value == null) {
    message.error('还没创建对话,不能发送!')
    return
    await conversationListRef.value.createConversation(props.data?formatToDateTime(new Date(props.data.createTime)):null)
  }
  // 清空输入框
  prompt.value = ''
  // 发送请求时如果accessToken过期,无法中断请求,暂时增加请求前刷新token
  authUtil.setToken(await refreshToken())
  // 执行发送
  await doSendMessageStream({
    conversationId: activeConversationId.value,
    content: content
  } as ChatMessageVO)
  setTimeout(() => {
    // 执行发送
    doSendMessageStream({
      conversationId: activeConversationId.value,
      content: content
    } as ChatMessageVO)
  }, 400)
}
/** 真正执行【发送】消息操作 */
@@ -478,7 +494,6 @@
  conversationInProgress.value = true
  // 设置为空
  receiveMessageFullText.value = ''
  try {
    // 1.1 先添加两个假数据,等 stream 返回再替换
    activeMessageList.value.push({
@@ -499,8 +514,7 @@
    await nextTick()
    await scrollToBottom() // 底部
    // 1.3 开始滚动
    textRoll()
    await textRoll()
    // 2. 发送 event stream
    let isFirstChunk = true // 是否是第一个 chunk 消息段
    await ChatMessageApi.sendChatMessageStream(
@@ -542,7 +556,9 @@
        stopStream()
      }
    )
  } catch {}
  } catch {
    console.log('sendStream Exception')
  }
}
/** 停止 stream 流式调用 */
@@ -632,16 +648,19 @@
/** 初始化 **/
onMounted(async () => {
  // 如果有 conversationId 参数,则默认选中
  if (route.query.conversationId) {
    const id = route.query.conversationId as unknown as number
    activeConversationId.value = id
    await getConversation(id)
  defaultMessage.value = props.data
  if(defaultMessage.value) {
    prompt.value = defaultMessage.value.content
    quickAccessFlag.value = true
  } else {
    // 获取列表数据
    activeMessageListLoading.value = true
    await getMessageList()
  }
})
  // 获取列表数据
  activeMessageListLoading.value = true
  await getMessageList()
onUnmounted(() => {
  stopStream()
})
</script>
@@ -655,9 +674,9 @@
.sidebar-toggle {
  position: absolute;
  left: 300px;  // 初始展开位置
  left: 320px;  // 初始展开位置
  top: 40%;
  z-index: 1000;
  z-index: 1;
  width: 20px;
  height: 80px;
  background: rgba(115, 196, 255, 0.5);
@@ -682,12 +701,12 @@
  left: 0;
  top: 0;
  bottom: 0;
  width: 300px;
  width: 320px;
  background: rgba(13,28,58,0.9);
  box-shadow: 2px 0 8px rgba(0,0,0,0.1);
  transition: transform 0.3s ease, opacity 0.2s ease;
  z-index: 999;
  overflow: hidden;
  overflow-x: hidden;
  &.collapsed {
    transform: translateX(-100%);
@@ -699,7 +718,7 @@
// 头部
.detail-container {
  width: 100%;
  height: 885px;
  height: 910px;
  margin-left: 5px;
  background-color: rgba(0, 0, 0, 0); /* 透明背景 */
  transition: margin 0.3s cubic-bezier(0.4, 0, 0.2, 1);
@@ -796,7 +815,7 @@
.footer-container {
  display: flex;
  flex-direction: column;
  height: 114px;
  height: 205px;
  margin-left: 10px;
  padding: 0;
@@ -831,7 +850,7 @@
      flex-direction: column;
      padding: 9px 10px;
      width: 876px;
      height: 114px;
      height: 205px;
      background: rgba(115,196,255,0.05);
      border-radius: 4px 4px 4px 4px;
      border: 1px solid #73C4FF;
@@ -842,8 +861,8 @@
    }
    .prompt-input {
      width: 876px;
      height: 113.55px;
      width: 860px;
      height: 203px;
      font-weight: 400;
      font-size: 14px;
      background-color: rgba(219,238,255,0);