<route lang="json5" type="page">
{
  layout: 'default',
  style: {
    navigationBarHidden: true,
  },
}
</route>

<template>
  <div class="flex flex-col h-screen bg-#ffffff tops">
    <!-- Navigation Bar -->
    <div
      class="flex-none flex items-center justify-between px-5 py-3 bg-white shadow-md h-20 pt-10 z-999 fixed top-0 w-full box-border"
    >
      <image src="/static/aichat/black.png" class="w-3 h-4.5" @click="goBack" />
      <div class="text-lg font-medium ml-12">小墨</div>
      <div class="flex items-center space-x-3">
        <!-- v-if="rawList.length > 0" -->
        <image src="/static/aichat/time.png" class="w-5 h-5 mr-4" @click="openPopup" />
        <image src="/static/aichat/new.png" class="w-5 h-5" @click="newChat" />
      </div>
    </div>

    <!-- 消息区 -->
    <div
      :class="[
        'flex fixed top-0 w-full  p-b-10 box-border mt-20',
        showActions ? (uploadList.length ? 'h-118' : 'h-151') : 'h-171',
      ]"
    >
      <!-- 背景层 -->
      <div
        v-if="!messages.length"
        class="absolute inset-0 flex flex-col items-center justify-center pointer-events-none bg-#ffffff"
      >
        <image src="/static/aichat/logo.png" class="w-20 h-24 mb-4" @click="newChat" />
        <view class="text-xl font-medium mb-1">嗨！ 我是小墨</view>
        <view class="text-gray-400">开启新的聊天吧</view>
      </div>

      <div
        ref="scrollEl"
        class="flex-1 overflow-y-auto bg-#ffffff"
        :class="showActions ? 'pb-44' : 'pb-16'"
      >
        <div :class="['relative z-10 px-4 py-6', showActions ? 'mb--11 h-105' : 'mb--21']">
          <template v-for="(msg, idx) in messages" :key="idx">
            <view v-if="shouldShowTimestamp(idx)" class="text-center text-xs text-gray-500 mt--3">
              {{ formatDayGroup(msg.timestamp) }}
            </view>
            <view
              class="flex items-start"
              :class="msg.role === 'assistant' ? 'justify-start  mt-0.3' : 'justify-end  mt-2 '"
            >
              <image
                v-if="msg.role === 'assistant'"
                src="/static/aichat/logo-message.png"
                class="w-8 h-8 rounded-full mr-2 mt-1"
              />
              <view
                class="relative max-w-[76.5%] mt-5"
                :class="idx === messages.length - 1 ? 'mb-20' : 'mb-3'"
              >
                <view
                  :class="[
                    'absolute -top-5 text-xs text-gray-400 w-20 text-right ',
                    msg.role === 'assistant' ? 'left--4' : 'right-0',
                  ]"
                >
                  {{ formatTimeShort(msg.timestamp) }}
                </view>
                <view
                  :class="[
                    'py-3 pl-3 rounded-md break-words mb-3 tracking-[2rpx] min-w-10 min-h-2 ',
                    msg.role === 'assistant'
                      ? 'bg-[#f9f8fd] text-black shadow '
                      : 'bg-[#45299e] text-white ',
                    msg.type === 'text' ? 'pr-0' : 'pr-3',
                  ]"
                >
                  <wd-loading
                    v-show="msg.role === 'assistant' && msgLoading && idx === messages.length - 1"
                    :size="20"
                    color="#e3e3e3"
                    custom-class="loading-black"
                    :class="[
                      'absolute top-1.5 text-xs text-gray-400 w-20 text-right',
                      msg.role === 'assistant' ? 'left-1' : 'right-0',
                    ]"
                  />
                  <!-- 图片消息 -->
                  <scroll-view
                    scroll-x
                    v-if="msg.type === 'image'"
                    vif="msg.type === uploadFileTypeEm.image"
                    @scroll="onScroll"
                  >
                    <view class="flex pr-1">
                      <view
                        v-for="(file, fileIdx) in msg.content.slice(0, 4)"
                        :key="fileIdx"
                        class="relative rounded-md overflow-hidden ml-2"
                        :class="{
                          'w-60 h-60': msg.content.length == 1,
                          'w-15 h-15': msg.content.length == 2,
                          'w-30 h-15 ': msg.content.length == 3,
                          'w-80 h-15 ': msg.content.length > 3,
                        }"
                      >
                        <view
                          v-if="msg.content.length > 4 && fileIdx >= 3"
                          class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-70 z-80"
                          @click="previewMoreImg(msg.content)"
                        >
                          + {{ msg.content.length - 4 }}
                        </view>
                        <image
                          vif="file.uploadFileType === uploadFileTypeEm.image"
                          :src="file?.image_url?.url"
                          class="w-full h-full object-cover"
                          @click="handleImageClick(file.image_url.url)"
                        />
                      </view>
                    </view>
                  </scroll-view>

                  <!-- 文件消息 -->

                  <scroll-view
                    scroll-x
                    v-else-if="msg.type === uploadFileTypeEm.file"
                    @scroll="onScroll"
                  >
                    <view class="flex pr-1">
                      <view
                        v-for="(file, fileIdx) in msg.content"
                        :key="fileIdx"
                        style="flex: 0 0 6rem"
                        class="relative text-xs h-12 px-2 py-2 rounded-md overflow-hidden mr-1 bg-white c-black"
                        :class="{
                          'w-25 h-15': msg.content.length == 1,
                          'w-50 h-15': msg.content.length == 2,
                          'w-30 h-15 ': msg.content.length == 3,
                          'h-15 flex-grow-0 flex-shrink-0 basis-10 mr-1': msg.content.length >= 4,
                        }"
                        @click="previewFile(file.content)"
                      >
                        <view>{{ file.name ? file.name : file.content }}</view>
                        <!-- <view>--{{ file }}--</view> -->
                        <!-- <view>{{ file }}</view> -->
                        <view>{{ calcFileSize(file.size) }}</view>
                      </view>
                    </view>
                  </scroll-view>

                  <!-- 视频 -->
                  <view v-else-if="msg.type === uploadFileTypeEm.video">
                    <view
                      v-for="(file, fileIdx) in msg.content"
                      :key="fileIdx"
                      style="flex: 0 0 6rem"
                      class="relative text-xs h-32 w-80 rounded-md overflow-hidden mr-1 c-black"
                    >
                      <!-- @click="previewVideo(msg.content)"-->
                      <video
                        :src="file?.video_url?.url"
                        class="w-60 h-30"
                        controls
                        :poster="file?.video_url?.poster"
                      ></video>
                      <!-- <video
                        :src="file.tempFilePath"
                        class="w-60 h-30"
                        controls
                        :poster="file.url"
                      ></video> -->
                    </view>
                  </view>

                  <!-- 文本消息 -->
                  <view v-else class="pr-2">
                    {{ msg.content }}
                  </view>
                </view>
                <view
                  v-if="msg.role === 'assistant' && msg.type === 'text'"
                  class="absolute bottom--3.5 flex space-x-3 ml-1"
                >
                  <image src="/static/aichat/copy.png" class="w-4 h-4" @click="copyText(msg)" />
                  <image src="/static/aichat/resect.png" class="w-4.3 h-4" @click="refreshText()" />
                </view>
              </view>
              <image
                v-if="msg.role === 'user'"
                :src="userAvatar"
                class="w-8 h-8 rounded-full ml-2 mt-1 bg-gray"
              />
            </view>
          </template>
        </div>
        <!-- 聊天信息区域 -->
        <!-- <view class="" style="height: 72px"></view> -->
      </div>
    </div>

    <!-- 底部上传预览 + 输入区 -->
    <div
      :class="[
        'fixed bottom-0 left-0 right-0 bg-white z-[80] overflow-hidden transition-all duration-300',
        showActions ? (uploadList.length ? 'h-40' : 'h-40') : 'h-20',
      ]"
    >
      <!-- 上传列表 -->
      <div
        v-if="uploadList.length"
        :class="[
          'flex px-4 py-2 overflow-x-auto space-x-3 bg-transparent',
          showActions
            ? uploadList.length
              ? ' fixed bottom-40'
              : 'fixed bottom-19'
            : 'fixed bottom-19',
        ]"
        :style="{
          transform: `rotate(0deg)`,
          transition: 'transform 0.3s ease',
        }"
      >
        <div class="flex overflow-x-auto space-x-3 py-2 w-88 flex-nowrap">
          <div
            v-for="item in uploadList"
            :key="item.id"
            class="relative w-20 h-16 rounded overflow-hidden flex-shrink-0"
          >
            <!-- 预览图，成功后用后端返回的 URL；上传中可以先用本地预览 -->

            <!-- <img
              v-if="videoFileType.includes(isFileType(item.name))"
              src="/static/aichat/ic_video.png"
              class="w-full h-full object-cover"
              @click="handleImageClick(item.ori_url)"
            />
            <img
              v-else-if="officeFileTypeList.includes(isFileType(item.name))"
              src="/static/aichat/word.png"
              class="w-full h-full object-cover"
              @click="handleImageClick(item.url)"
            />
            <img
              v-else-if="picFileType.includes(isFileType(item.name))"
              :src="item.tempFilePath"
              class="w-full h-full object-cover"
              @click="handleImageClick(item.url)"
            /> -->
            <img
              v-if="picFileType.includes(isFileType(item.name))"
              :src="item.tempFilePath || item.url"
              class="w-full h-full object-cover"
              @click="handleImageClick(item.ori_url)"
            />
            <view v-else class="text-xs text-gray-400 mt-1" @click="previewFile(item.ori_url)">
              {{ item.name }}
            </view>

            <!-- 关闭按钮 -->
            <div
              class="absolute top-1 right-1 w-4 h-4 rounded-full bg-black bg-opacity-50 flex items-center justify-center cursor-pointer text-white text-xs z-5"
              @click="removeImage(item.id)"
            >
              ×
            </div>

            <!-- 重试 -->
            <span
              v-if="item.status === 'error'"
              class="absolute w-full h-full bg-black bg-opacity-40 pt-1 left-0 top-0 text-center color-white text-3xl"
              @click.stop="retry(item)"
            >
              ↻
            </span>

            <!-- 进度 / 成功 / 失败 -->
            <div
              class="absolute bottom-0 left-0 w-full text-xs text-center py-1 bg-black bg-opacity-50"
              :class="{
                'text-black': item.status === 'uploading',
                'text-green': item.status === 'success',
                'text-red': item.status === 'error',
              }"
            >
              <template v-if="item.status === 'uploading'">
                <view class="text-white">{{ item.progress }}%</view>
              </template>
              <template v-else-if="item.status === 'success'">✔ 成功</template>
              <template v-else>✖ 失败</template>
            </div>
          </div>
        </div>
      </div>

      <!-- 知识库-->
      <div
        @click="toggleKnowledge"
        v-if="!uploadList.length"
        class="fixed left-[32rpx] right-0 z-[90] h-8 w-23 flex items-center justify-between px-3 box-border rounded-1 transition-all duration-300"
        :class="[
          knowledgeOpen ? 'bg-#eee9f8' : 'bg-[#F9F9F9]',
          showActions ? (uploadList.length ? 'bottom-31' : 'bottom-41') : 'bottom-21',
        ]"
      >
        <image
          :src="
            knowledgeOpen
              ? '/static/aichat/Knowledge-open.png'
              : '/static/aichat/Knowledge-close.png'
          "
          class="w-4 h-3.5 mt-1"
        />
        <div
          :class="['text-[26rpx] transition-colors', knowledgeOpen ? 'text-#46299D' : 'text-black']"
        >
          知识库
        </div>
        <!-- <button @click="reload()">refresh</button> -->
      </div>

      <!-- 输入 + 切换 -->
      <view class="flex items-center px-4 py-2.5 border-t border-t-solid border-[#E7E7E7]">
        <input
          v-model="inputText"
          @focus="onFocus"
          @confirm="sendText"
          placeholder="想对我说点什么～"
          class="flex-1 h-10 px-3 border border-gray-100 bg-[#f9f9f9] rounded-1 focus:outline-none"
        />
        <!-- 将keyup替换为confirm -->
        <image
          v-show="!knowledgeOpen"
          src="/static/aichat/add-circle.png"
          class="w-7 h-7 mx-3"
          @click="toggleActions"
          :style="{
            transform: `rotate(${rotation}deg)`,
            transition: 'transform 0.3s ease',
          }"
        />
        <image
          v-if="sendTextLoading && inputText.length <= 0"
          src="/static/aichat/enter-no.png"
          class="w-7 h-7"
          @click="sendText()"
          :disabled="loading"
          :class="[knowledgeOpen ? 'ml-2' : 'ml-0']"
        />
        <image
          v-else-if="sendTextLoading"
          src="/static/aichat/enter.png"
          class="w-7 h-7"
          @click="sendText()"
          :disabled="loading"
          :class="[knowledgeOpen ? 'ml-2' : 'ml-0']"
        />
        <image
          v-else
          src="/static/aichat/stop.png"
          class="w-7 h-7"
          @click="stopMsg()"
          :class="[knowledgeOpen ? 'ml-2' : 'ml-0']"
        />
      </view>

      <!-- 操作面板 -->
      <transition name="slide-up">
        <view
          v-show="showActions"
          :class="[
            'flex justify-around items-center h-10 bg-white border-t border-t-solid border-[#E7E7E7]',
            showActions ? (uploadList.length ? 'pt-10' : 'pt-10') : 'pt-0',
          ]"
        >
          <view class="flex flex-col items-center">
            <image src="/static/aichat/phone-img.png" class="w-13 h-13" @click="onPickImage" />
            <span class="text-xs mt-1 text-gray-500">照片</span>
          </view>
          <view class="flex flex-col items-center">
            <image src="/static/aichat/photo.png" class="w-13 h-13" @click="onPickVideo" />
            <span class="text-xs mt-1 text-gray-500">视频</span>
          </view>
          <view class="flex flex-col items-center">
            <image src="/static/aichat/files.png" class="w-13 h-13" @click="onPickFile" />
            <span class="text-xs mt-1 text-gray-500">文件</span>
          </view>
        </view>
      </transition>
    </div>

    <!-- 弹窗遮罩 + 容器 -->
    <view v-if="showPopup" class="overlay" @click.self="closePopup">
      <view :class="['popup', { fullscreen }]">
        <!-- Header -->
        <view class="popup-header">
          <view>
            <image src="/static/aichat/close.png" class="w4 h4" @click="closePopup" />
            <image
              :src="fullscreen ? '/static/aichat/shirk-cl.png' : '/static/aichat/shrink.png'"
              class="w5 h5 ml-4"
              @click="toggleFullscreen"
            />
          </view>

          <text class="title ml--14">历史记录</text>
        </view>
        <!-- 内容区 -->
        <view v-if="!rawList.length" class="flex-1 flex flex-col items-center justify-center">
          <image
            src="/static/aichat/empty.png"
            class="w-[400rpx] h-[400rpx] mb-[20rpx] opacity-50"
            mode="widthFix"
          />
          <text class="text-[28rpx] text-gray-500">暂无数据</text>
        </view>
        <scroll-view
          v-else
          scroll-y
          class="popup-body"
          @scrolltolower="scrolltolowerLoadData"
          :scroll-top="scrollTop"
        >
          <template v-for="group in groups" :key="group.date">
            <view class="date-label">{{ group.label }}</view>
            <view
              v-for="item in group.items"
              :key="item.id"
              class="history-item"
              @click="goChat(item.listUuid)"
            >
              <text class="history-text">{{ item.title }}</text>
            </view>
          </template>
        </scroll-view>
      </view>
    </view>

    <view
      v-if="showPreview"
      class="fixed inset-0 bg-black bg-opacity-90 z-999 flex justify-center items-start pt-10"
      @click="closePreview"
    >
      <view
        class="absolute w-full max-w-full h-full mt-8 p-5 box-border flex justify-center items-start"
      >
        <image
          v-if="previewUrl"
          class="w-full max-h-[calc(100vh-90px)] object-contain"
          :src="previewUrl"
          mode="widthFix"
          @click.stop
        />
        <video
          class="w-full max-h-[calc(100vh-90px)] object-contain"
          v-else-if="previewVideoUrl"
          :src="previewVideoUrl"
        ></video>
        <view
          class="absolute top--8 right-5 text-white text-3xl z-1000 w-10 h-10 text-center leading-10"
          @click.stop="closePreview"
        >
          ×
        </view>
      </view>
    </view>
  </div>
</template>

<script lang="ts" setup>
import { ref, reactive, nextTick, watchEffect, watch } from 'vue'
import dayjs from 'dayjs'
import { useUserStore } from '@/store'
import { getEnvBaseUrl } from '@/utils'
import guid from '@/utils/guid.js'
import type { IGptRequestBody } from '@/service/index/foo'
import {
  calcFileSize,
  formatParams,
  formatData,
  readFile,
  fileSuffix,
  officeFileTypeList as fileType,
  videoFileType as videoType,
  picFileType as picType,
} from './utils/index'
import 'dayjs/locale/zh-cn'
import { showToastErr, showToastOk, time_format3 } from '@/utils/tools'
import { uploadFileChunk } from './utils/api.js'
import { TOKEN, AVATAR } from './utils/test'
import { deepClone } from 'wot-design-uni/components/common/util'
import { log } from 'console'

dayjs.locale('zh-cn')

interface UploadFile {
  id: string
  formName: string
  name: string
  size: number
  type: string
  progress: number // 上传进度 0-100
  status: 'uploading' | 'success' | 'error'
  tempFilePath: string // 本地临时路径，用于预览
  url?: string // 本地预览URL或服务器返回的URL
  uploadFileType: 'image' | 'video' | 'file'
  ori_url?: string // 本地视频
  suffix: string
}

interface IMessage {
  role: 'user' | 'assistant'
  type: 'text' | 'image' | 'video' | 'file'
  content: string | any[]
  timestamp: Date
}
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 5mb属于大文件
const FILE_SLICE_SIZE = 10 * 1024 * 1024 // 分片大小
const userAvatar = ref()
const chatMode = ref('qwen-vl-plus')
let isUserOk = false // 用户确认使用 tongyi-app

const baseUrl = getEnvBaseUrl()
const messages = reactive([])
//获取用户上下文
const historyUserMsgs = reactive<any[]>([])
const inputText = ref('')
const loading = ref(false)
const showActions = ref(false)
const scrollEl = ref<HTMLElement>()
//显示图片更多遮罩层
const showImageMask = ref(false)
//已上传文件列表
const uploadList = reactive<UploadFile[]>([])
// 上传队列
const uploadQueue = reactive([])
// 上传中文件数量
const uploadingCount = ref(0)
// 最大并行上传数
const MAX_CONCURRENT_UPLOADS = 5 // 6

// ------------------
// 控制弹窗显隐 & 全屏/底部模式
// ------------------
const showPopup = ref(false)
const fullscreen = ref(false)

const officeFileTypeList = ref(fileType)
const otherFileType = ref(['.txt']) // 不在使用
const videoFileType = ref(videoType)
const picFileType = ref(picType)

// ------------------
// 历史记录数据 & 分组逻辑
// ------------------
interface HistoryRecord {
  id: number
  listUuid: string
  title: string
  createdAt: number // Unix 时间戳（秒）
}
const rawList = ref<HistoryRecord[]>([])

const groups = computed(() => {
  const map: Record<string, HistoryRecord[]> = {}
  rawList.value.length > 0 &&
    rawList.value.forEach((item) => {
      const key = dayjs.unix(item.createdAt).format('YYYY-MM-DD')
      ;(map[key] ||= []).push(item)
    })
  return Object.keys(map)
    .sort((a, b) => dayjs(b).valueOf() - dayjs(a).valueOf())
    .map((key) => {
      const today = dayjs().format('YYYY-MM-DD')
      const yesterday = dayjs().add(-1, 'day').format('YYYY-MM-DD')
      let label = ''
      if (key === today) label = '今天'
      else if (key === yesterday) label = '昨天'
      else label = dayjs(key).format('YYYY/MM/DD')
      return { date: key, label, items: map[key] }
    })
})

// 点击历史记录条目，跳转聊天页面
async function goChat(listUuid: string) {
  await fetchHistoryDiets(listUuid)
  showPopup.value = false
}
// qwen-vl-plus
/** 1. 创建聊天会话 */
const listUuid = ref('')

async function createChatSession() {
  try {
    const createResp: any = await uni.request({
      url: `${baseUrl}/chat/app/create`,
      method: 'POST',
      data: {
        gptModel: chatMode.value,
      },
      header: {
        Authorization: token.value,
      },
    })
    // 如果后台返回新的会话信息，可以在这里处理，比如拿到 listUuid 等
    listUuid.value = createResp.data.data.listUuid
  } catch (err) {
    console.log(err)
  }
}
// ------------------
// 拉取历史记录接口（替换为你自己的 API）
// ------------------

/** 2. 拉取历史记录列表 */
const state = reactive({
  page: 0,
  pageSize: 20,
  total: null,
  loading: false,
})
const scrollTop = ref(0)
async function fetchHistoryList() {
  // if(state.page*state.pageSize>state.total && state.total!==null){
  // 	return
  // }
  if (state.loading) {
    return
  }
  state.loading = true
  try {
    const resp: any = await uni.request({
      url: `${baseUrl}/chat/app/list`,
      method: 'POST',
      data: {
        page: state.page,
        pageSize: state.pageSize,
      },
      header: {
        Authorization: token.value,
      },
    })
    if (resp.data && resp.data.data) {
      rawList.value = rawList.value.concat(resp.data.data.data)
      state.total = resp.data.data.count //Math.ceil(resp.data.count/state.page)
      // scrollTop.value+=60;
    }
  } catch (err) {
    console.log('err: ', err)
  } finally {
    state.loading = false
  }
}
const scrolltolowerLoadData = (e) => {
  state.page++
}
watch(
  () => state.page,
  async () => {
    await fetchHistoryList()
  },
  { deep: true },
)
async function openPopup() {
  state.page++
  showPopup.value = true
}
function closePopup() {
  showPopup.value = false
}
function toggleFullscreen() {
  fullscreen.value = !fullscreen.value
}
/** 3. 拉取历史记录详情 */

async function fetchHistoryDiets(value) {
  try {
    const resp: any = await uni.request({
      url: `${baseUrl}/chat/app/detail`,
      method: 'POST',
      data: {
        listUuid: value,
        gptModel: chatMode.value,
      },
      header: {
        Authorization: token.value,
      },
    })
    if (resp && resp.data && resp.data.data) {
      const rawList = resp.data.data.detail // 假设后端直接返回消息数组
      listUuid.value = resp.data.data.listUuid
      // const newMessages = parseBackendMessages(JSON.parse(rawList))
      // 用解析后的消息替换当前消息列表
      messages.splice(0, messages.length, ...JSON.parse(rawList))
    }
  } catch (err) {
    console.log('err: ', err)
  }
}
function parseBackendMessages(chatList: any[]): IMessage[] {
  console.log('chatList: ', chatList)
  const chatHistory = []
  const requestArrData = [] // 请求数组
  chatList.forEach((item) => {
    const arr = JSON.parse(item.message)
    console.log('item.CreatedAt: ', item.UpdatedAt)
    const date = time_format3(item.CreatedAt)

    if (item.role === 'user') {
      const videoContent = []
      const imageContent = []
      let textContent = {}
      const requestData = {
        // 单个请求对象
        content: [],
        role: item.user,
      }
      arr.forEach((chat) => {
        if (chat.type === 'image_url') {
          imageContent.push(chat)
          requestData.content.push(chat)
        } else if (chat.type === 'video_url') {
          videoContent.push(chat)
          requestData.content.push(chat)
        } else {
          // if(item.type==="text")
          textContent.type = 'text'
          textContent.text = chat.text
          requestArrData.push({
            content: chat.text,
            role: 'user',
          })
        }
      })
      if (requestData.content.length > 0) {
        requestArrData.push({
          role: item.role,
          content: requestData,
        })
      }

      if (videoContent.length > 0) {
        const videoMessage = {
          role: item.role,
          type: 'video',
          content: videoContent, // string []
          timestamp: date,
        }
        chatHistory.push(videoMessage)
      }
      if (imageContent.length > 0) {
        const imageMessage = {
          role: item.role,
          type: 'image',
          content: imageContent, // string []
          timestamp: date,
        }
        chatHistory.push(imageMessage)
      }
      if (textContent.type === 'text') {
        const msg = {
          role: item.role,
          type: 'text',
          content: textContent.text, // string []
          timestamp: date,
        }
        chatHistory.push(msg)
      }
    } else if (item.role === 'system') {
      const content = []
      const requestData = {
        // 单个请求对象
        content: '', // 文件链接
        role: item.user,
      }
      arr.forEach((chat) => {
        const isFile = officeFileTypeList.value.includes(fileSuffix(chat.text)) //检测是否属于文档类
        if (chat.type === 'text' && !isFile) {
          const textContent = {
            role: 'user',
            type: 'text',
            text: chat.text,
          }
          chatHistory.push(textContent)
          requestArrData.push({
            content: chat.text,
            role: 'user',
          })
        } else {
          // 当content为文件链接时
          content.push({
            role: chat.role,
            type: 'file',
            content: chat.text, // string []
            timestamp: date,
          })
          requestArrData.push({
            content: chat.text,
            name: chat.text,
            role: 'system',
          })
        }
      })
      const message = {
        role: item.role,
        type: 'file',
        content: content, // string []
        timestamp: date,
      }
      chatHistory.push(message)

      console.log('-----chatHistory: ', chatHistory)
    } else if (item.role === 'assistant') {
      arr.forEach((chat) => {
        chatHistory.push({
          role: 'assistant',
          type: 'text',
          content: chat.text,
          timestamp: date,
        })
        requestArrData.push({
          content: chat.text,
          role: 'assistant',
          type: 'text',
        })
      })
    }
  })
  historyUserMsgs.splice(0, historyUserMsgs.length, ...requestArrData)
  console.log('chatHistory: ', chatHistory)
  return chatHistory
}
function parseBackendMessages1(rawList: any[]): IMessage[] {
  const messageList: IMessage[] = []

  rawList.forEach((item) => {
    const rawContent: string = item.text ?? item.content ?? item.message ?? ''
    // 先尝试 map[...] 的老逻辑
    let parts: ParsedPart[] | null = tryParseMapFormat(rawContent)

    // 如果没命中 map[...]，再看看是不是一个标准的 JSON 数组字符串
    if (!parts && rawContent.startsWith('[') && rawContent.endsWith(']')) {
      try {
        const arr = JSON.parse(rawContent) as Array<{ text: string; type: string }>
        if (Array.isArray(arr)) {
          parts = arr.map((el) => {
            if (el.type === 'file_url') {
              return {
                type: 'video',
                content: [{ url: el.text, uploadFileType: 'video' }],
              }
            } else if (el.type === 'image_url' || el.type === 'image') {
              return {
                type: 'image',
                content: [{ url: el.text, uploadFileType: 'image' }],
              }
            } else if (el.type === 'text') {
              return { type: 'text', content: el.text }
            }

            // 其他类型按需扩展
            return { type: el.type, content: el.text }
          })
        }
      } catch {
        // 解析失败就继续下面的 fallback
      }
    }

    if (parts) {
      // 拆分成多条消息推入
      parts.forEach((part) => {
        messageList.push({
          role: item.role,
          type: part.type as any, // 'text' 或 'image'
          content: part.content,
          timestamp: (item.CreatedAt ?? 0) * 1000,
        })
      })
    } else {
      // 你原来的 fallback 分支（单条，直接塞 content，type 也补成 'text'）
      messageList.push({
        role: item.role,
        type: 'text',
        content: rawContent,
        timestamp: (item.CreatedAt ?? 0) * 1000,
      })
    }
  })

  return messageList
}
interface ParsedPart {
  type: string
  content: string | UploadFile[]
}

function tryParseMapFormat(str: string): ParsedPart[] | null {
  if (!str || !str.startsWith('[') || !str.endsWith(']') || !str.includes('map[')) {
    return null // 不符合 [map[...] ...] 格式
  }

  const result: ParsedPart[] = []
  // 提取所有 map[...] 子串
  const pattern = /map\[([^\]]+)\]/g
  let match: RegExpExecArray | null
  while ((match = pattern.exec(str)) !== null) {
    const contentBlock = match[1] // 如 "text:这种 type:text"
    const typeIndex = contentBlock.indexOf(' type:')
    if (typeIndex === -1) {
      continue // 安全检查，正常情况下一定有 ' type:' 分隔
    }
    // 拆分内容部分和类型部分
    const contentPart = contentBlock.substring(0, typeIndex).trim() // "text:这种"
    const typeValue = contentBlock.substring(typeIndex + 6).trim() // 跳过 " type:" 得到 "text" 或 "image_url"
    // 解析内容键和值
    const colonIndex = contentPart.indexOf(':')
    if (colonIndex === -1) {
      continue
    }
    const key = contentPart.substring(0, colonIndex).trim() // 键，比如 "text" 或 "image_url"
    let value = contentPart.substring(colonIndex + 1).trim() // 值，比如 "这种" 或 "https://cdn.xx/xxx.jpg"
    // 去掉包裹在值两端的引号（如果有的话）
    if (
      (value.startsWith('"') && value.endsWith('"')) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      value = value.substring(1, value.length - 1)
    }

    // 根据类型构造 ParsedPart 对象
    if (typeValue === 'text' && key === 'text') {
      result.push({
        type: 'text',
        content: value,
      })
    } else if (typeValue === 'image_url' && key === 'image_url') {
      // 将图片URL封装为 UploadFile 对象数组
      result.push({
        type: 'image', // 前端统一用 'image' 作为图片消息类型
        content: [{ url: value, uploadFileType: 'image' }],
      })
    } else {
      // 其他类型的内容，可根据需要扩展处理
      result.push({
        type: typeValue,
        content: value,
      })
    }
  }

  // 如果成功解析出至少一个片段，则返回数组；否则返回 null
  return result.length > 0 ? result : null
}

const token = ref<string>()
const userInfo = ref<any>({})
const refreshToken = ref<string>('')
const statusBarHeight = ref<number>(0)
const mask = ref('')
// ---- 页面初始化 ----

onMounted(async () => {
  // 1. 定义一个 init 函数，拿 Extras 并依次调用接口
  try {
    const init = async () => {
      const wv = plus.webview.currentWebview() // 获取当前页面所属的 Webview 对象。
      token.value = wv.token || uni.getStorageSync('token') || import.meta.env.VITE_DEV_TOKEN
      userInfo.value = JSON.parse(wv.userInfo) || {}
      refreshToken.value = wv.refreshToken || uni.getStorageSync('refreshToken')
      statusBarHeight.value = wv.statusBarHeight || uni.getSystemInfoSync().statusBarHeight
      userAvatar.value = userInfo.value.Avatar
      mask.value = userInfo.value.ID
      await createChatSession()
    }
    init()
  } catch (e) {
    console.error('onMounted e: ', e)
  } finally {
  }
})

function scrollToBottom() {
  const el = scrollEl.value!
  nextTick(() => {
    // el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
    el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
  })
}

//图片滚动事件
const onScroll = (e) => {
  showImageMask.value = e.detail.scrollLeft <= 0
}

//查看更多图片
const previewMoreImg = (files) => {
  uni.removeStorageSync('previewImages')
  uni.removeStorageSync('previewVideos')

  uni.setStorageSync('previewImages', files)

  uni.navigateTo({
    url: `/pages/preview/index`,
  })
}

//查看视频
const previewVideo = (files) => {
  uni.removeStorageSync('previewVideos')
  uni.removeStorageSync('previewImages')

  uni.setStorageSync('previewVideos', files)

  uni.navigateTo({
    url: `/pages/preview/index`,
  })
}

function addMessage(msg) {
  console.log('msg: ', msg)
  messages.push(msg)
  scrollToBottom()
}

const shouldShowTimestamp = (i: number) => {
  if (i === 0) return true
  return !dayjs(messages[i].timestamp).isSame(messages[i - 1].timestamp, 'day')
}
const formatDayGroup = (d: Date) => dayjs(d).format('YYYY/MM/DD HH:mm')
const formatTimeShort = (d: Date) => dayjs(d).format('MM/DD HH:mm')

function goBack() {
  const wv = plus.webview.currentWebview()
  wv.close('slide-out-right', 300) // 或者直接 wv.close()
}
function viewHistory() {
  uni.navigateTo({ url: '/pages/history/history' })
}
async function newChat() {
  messages.splice(0)
  await createChatSession()
  inputText.value = ''
}
const rotation = ref(0)
function toggleActions() {
  uni.hideKeyboard()

  rotation.value += 45

  console.log(rotation.value, '2')
  showActions.value = !showActions.value

  scrollToBottom()
}

// 上传文件formData类型
const uploadFileTypeEm = {
  image: 'image',
  video: 'video',
  file: 'file',
  text: 'text',
}
// const url=baseUrl
// 上传配置
const uploadConfig = reactive({
  url: `${baseUrl}/upload/img`,
  formData: {
    source: 'chat',
    mask: mask.value,
    type: uploadFileTypeEm.image,
  },
  image: {
    maxSize: 10 * 1024 * 1024, // 图片最大10MB
    limitMsg: '图片大小不能超过10MB',
    supportType: ['.jpeg', '.png', '.jpg'],
  },
  video: {
    maxSize: 100 * 1024 * 1024, // 视频最大100MB
    limitMsg: '视频大小不能超过100MB',
    supportType: ['.mp4', '.mov', '.wmv'],
  },
  file: {
    maxSize: 100 * 1024 * 1024, // 文件最大100MB
    limitMsg: '文件大小不能超过300MB',
    supportType: ['.pdf', '.doc', '.xlsx', '.txt'],
  },
})

//监听上传文件状态更新loading
watchEffect(() => {
  const allSuccess = uploadList.every((item) => item.status === 'success')
  loading.value = !allSuccess
})

//添加上传队列
const addUploadQueue = (tempFiles: any[], uploadFileType: string) => {
  // 添加到上传队列
  const files = tempFiles.map((item: any, index: number) => ({
    id: guid.getGuid(),
    tempFilePath: item.path,
    progress: 0,
    status: 'pending',
    name: item.name,
    size: item.size,
    type: item.type,
    uploadFileType: uploadFileType,
    formName: 'file',
  }))

  //剔除掉大小超过限制的文件
  const _files = []
  files.forEach((file: any) => {
    const size = file.size
    const fileMaxSize = uploadConfig[uploadFileType].maxSize
    const fileLimitMsg = uploadConfig[uploadFileType].limitMsg

    if (size > fileMaxSize) {
      uni.showToast({
        title: fileLimitMsg,
        icon: 'none',
      })
      return
    }

    _files.push(file)
  })

  // 添加到上传文件列表
  uploadList.push(..._files)

  // 添加到队列
  uploadQueue.push(..._files)

  // 更新上传文件类型
  uploadConfig.formData.type = uploadFileType

  // 开始上传
  startUploads()
}

// 开始上传文件
const startUploads = () => {
  // 获取待上传文件
  const pendingFiles: Array<any> = uploadQueue.filter((file) => file.status === 'pending')

  // 限制最大并行上传数
  const filesToUpload = pendingFiles.slice(0, MAX_CONCURRENT_UPLOADS - uploadingCount.value)

  filesToUpload.forEach((file) => {
    // uploadFile(file);
    let url = null
    if (file.size > MAX_FILE_SIZE) {
      bigFileUpload(file)
        .then((res) => {
          console.log('全部成功 res: ', res)
          // const length=res.length;
          url = res.FullFileUrl
          file.status = 'success'
          uploadingCount.value--
        })
        .catch((rej) => {
          console.log('失败rej: ', rej)
          file.status = 'error'
          // uploadingCount.value--
        })
    } else {
      uploadFile(file)
    }
  })
}

// 上传单个文件
const uploadFile = (file: UploadFile) => {
  // 更新状态为上传中
  file.status = 'uploading'
  uploadingCount.value++

  // 创建上传任务
  const uploadTask = uni.uploadFile({
    url: uploadConfig.url,
    filePath: file.tempFilePath,
    name: file.formName,
    formData: uploadConfig.formData,
    success: (res) => {
      const result = JSON.parse(res.data)

      if (res.statusCode === 200 && result.code === 0) {
        // 更新文件状态
        file.status = 'success'
        file.progress = 100
        file.url = result.data.cover_url || result.data.ori_url || file.tempFilePath
        file.ori_url = result.data.ori_url || ''
        uni.showToast({
          title: res.msg || '文件上传成功',
          icon: 'none',
        })
      } else {
        file.status = 'error'
      }
    },
    fail: (err) => {
      file.status = 'error'
      if (err.errMsg === 'uploadFile:fail timeout') {
        uni.showToast({
          title: '上传文件超时',
          icon: 'none',
          duration: 3000,
        })
      }
    },
    complete: () => {
      // 更新上传文件列表状态
      uploadList.forEach((item) => {
        if (item.id === file.id) {
          item.status = file.status
          item.url = file.url
          item.progress = file.progress
        }
      })

      uploadingCount.value--

      // 检查是否还有待上传的文件
      const remainingPendingFiles = uploadQueue.filter((file) => file.status === 'pending')
      if (remainingPendingFiles.length > 0) {
        startUploads()
      } else if (uploadingCount.value === 0) {
        // 所有文件上传完成
        // uni.showToast({
        //   title: '全部上传完成',
        //   icon: 'success',
        // })
        loading.value = false
      }
    },
  })

  // 监听上传进度
  uploadTask.onProgressUpdate((res) => {
    uploadList.forEach((item) => {
      if (item.id === file.id) {
        item.progress = res.progress === 100 ? 99 : res.progress
      }
    })
  })
}
function bigFileUpload(file) {
  file.status = 'uploading'
  uploadingCount.value++

  console.log('file: ', file) //progress
  return new Promise(async (resolve, reject) => {
    const md5 = file.id
    const fileName = file.name
    const tempFilePath = file.tempFilePath
    const maxSize = FILE_SLICE_SIZE
    const chunksList = await readFile(file, maxSize)
    const chunksLength = chunksList.length
    const requestList = []
    const maxConcurrency = 3 // 一次最多3个
    let currentRunning = 0
    let index = 0
    let success = null
    const errors = []
    let isOver = false
    const next = () => {
      while (currentRunning < maxConcurrency && index < chunksLength) {
        if (isOver) {
          // 有一个失败就立刻取消之后的请求
          return
        }
        const chunkIndex = index++
        const chunk = chunksList[chunkIndex]
        const params = {
          FileMd5: md5,
          Chunk: chunk,
          ChunkFileName: `${fileName}_${chunkIndex}`,
          Total: chunksLength,
          UseType: 100,
          FileName: fileName,
          Source: 'aiChat',
        }
        currentRunning++
        // send request
        uploadFileChunk({ formData: params, tempFilePath })
          .then((res) => {
            if (res.statusCode === 200) {
              file.progress = Math.ceil(((chunkIndex + 1) / chunksLength) * 100)
              if (file.progress > 100) {
                file.progress = 100
              }
              const data = JSON.parse(res.data)
              if (data.status === 0) {
                // success.push(data.data)
                success = data.data
                if (data.data && data.data.FullFileUrl) {
                  file.url = data.data.FullFileUrl
                  file.ori_url = data.data.FullFileUrl
                } else if (data.data && data.data.CoverUrl) {
                  file.url = data.data.CoverUrl
                  file.ori_url = data.data.CoverUrl
                }
              } else if (data.status === 1 && data.code === 401) {
                showToastErr(data.msg)
                reject(data)
                // throw Error(data.msg);
              }
              // console.log('res: ',data);
            } else {
              errors.push(res.data)
              throw new Error('上传失败')
            }
          })
          .catch((err) => {
            reject(err)
            isOver = true
            file.status = 'error'
            // console.error(`第 ${chunkIndex} 片上传失败`, err)
            errors.push(err)
          })
          .finally(() => {
            currentRunning--
            if (chunkIndex === chunksLength - 1) {
              if (errors.length === 0) {
                file.status = 'success'
                resolve(success)
              } else {
                file.status = 'error'
                reject(errors)
              }
            } else if (index < chunksLength) {
              next()
            }
          })
      }
    }
    next()
    // const request = (chunks, i) => {
    // 	const chunk = chunks.shift()
    // 	const params = {
    // 		FileMd5: md5,
    // 		Chunk: chunk,
    // 		ChunkFileName: `${fileName}_${i}`,
    // 		Total: chunksLength,
    // 		UseType: 100,
    // 		FileName: fileName,
    // 		Source: 'aiChat',
    // 	}

    // 	uploadFileChunk({ formData: params, tempFilePath: tempFilePath })
    // 		.then((res) => {
    // 			// requestList.push(Promise.resolve(res))
    // 			if (res.status === 0) {
    // 				file.progress = Math.ceil(((i + 1) / chunksLength) * 100)
    // 				Promise.resolve(JSON.parse(res.data))
    // 			} else {
    // 				Promise.reject(res)
    // 			}
    // 		})
    // 		.catch((rej) => {
    // 			// requestList.push(Promise.resolve(rej))
    // 			Promise.reject(rej)
    // 		})
    // 		.finally(() => {
    // 			if (chunks.length > 0) {
    // 				request(chunks, ++i)
    // 			}
    // 		})
    // }
    // console.log('success,next: ', success, errors);
    // const promiseList=chunksList.map(request);
    // request(chunksList, 0)
    // return Promise.all(chunksList)
    // 	.then((res) => {
    // 		console.log('all res: ', res)
    // 	})
    // 	.catch((rej) => {
    // 		console.log('all rej: ', rej)
    // 	})
  })
}

// 照片
const onPickImage = () => {
  loading.value = true
  uni.chooseImage({
    count: 9, // 最多选择9张
    sizeType: ['original', 'compressed'],
    sourceType: ['album'],
    // extension: ['.rar', '.png', '.jpg'],
    // extension: ['jpeg', 'png', 'jpg'],
    success: (res: any) => {
      console.log(res)

      // 开始上传
      addUploadQueue(res.tempFiles, uploadFileTypeEm.image)
    },
    fail: (err) => {
      uni.showToast({
        title: '选择照片失败',
        icon: 'none',
      })
      loading.value = false
    },
  })
}

// 视频
const onPickVideo = () => {
  uni.chooseVideo({
    sourceType: ['album', 'camera'],
    compressed: true,
    // extension: uploadConfig.video.supportType,
    success: (res: any) => {
      console.log(res)

      const tempFile = res.tempFile
      tempFile.path = res.tempFilePath

      // 开始上传
      addUploadQueue([tempFile], uploadFileTypeEm.video)
    },
    fail: (err) => {
      uni.showToast({
        title: '选取视频失败',
        icon: 'none',
      })
    },
  })
}
const fileInput = ref(null)
// extension: [
//    'txt', // 文本
//    'pdf', // PDF
//    'doc',
//    'docx', // Word
//    'xls',
//    'xlsx', // Excel
//    'ppt',
//    'pptx', // PowerPoint
//  ],
// 文件
const onPickFile = () => {
  uni.chooseFile({
    count: 9,
    type: 'all',

    success: (res: any) => {
      console.log(res)
      // 开始上传
      addUploadQueue(res.tempFiles, uploadFileTypeEm.file)
    },
    fail: (err) => {
      uni.showToast({
        title: '选择文件失败',
      })
    },
  })
}

// 点击重试
function retry(item: UploadFile) {
  item.status = 'uploading'
  item.progress = 0
  if (item.size > MAX_FILE_SIZE) {
    bigFileUpload(item)
      .then((res) => {
        // const length=res.length;
        // item.url=res.FullFileUrl;
        // item.ori_url=res.FullFileUrl;
        item.status = 'success'
      })
      .catch((rej) => {
        item.status = 'error'
      })
  } else {
    uploadFile(item) // 如果需要 detail，可缓存后传入
  }
}

// 删除
function removeImage(id: string) {
  const findIndex = uploadList.findIndex((item) => item.id === id)
  if (findIndex > -1) {
    uploadList.splice(findIndex, 1)
  }
}

// 预览（可自行实现 uni.previewImage 等）
function previewImage(url: string) {
  uni.previewImage({ urls: [url] })
}

// 预览文件
// 统一替换掉原来的 previewFile
const previewFile = (url: string) => {
  if (typeof url !== 'string') {
    return
  }
  if (typeof plus !== 'undefined') {
    // 在 App 里直接用原生下载 + 打开
    downloadAndOpenFile(url)
  } else {
    // H5 或者调试环境回退到 WebView
    uni.navigateTo({
      url: '/pages/webview/index?link=' + encodeURIComponent(url),
    })
  }
}

// 下载并打开文件
const downloadAndOpenFile = (downloadUrl: string) => {
  uni.showLoading({ title: '加载中...', mask: true })

  if (!downloadUrl) {
    uni.hideLoading()
    return uni.showToast({ title: '文件路径无效', icon: 'none' })
  }

  // 将文件存放到应用私有下载目录，保证权限可读可写
  const options = {
    // “_downloads/” 会自动映射到应用的 Documents/download 目录
    filename: '_downloads/',
  }

  const dtask = plus.downloader.createDownload(downloadUrl, options, (d, status) => {
    uni.hideLoading()
    if (status === 200) {
      const savedPath = d.filename
      if (savedPath) {
        // 用系统默认的方式打开任意类型文件（PDF/Word/Excel/图片/视频 都通用）
        plus.runtime.openFile(savedPath, {}, () => {
          // 打开失败的回调可选
        })
      } else {
        uni.showToast({ title: '文件保存失败', icon: 'none' })
      }
    } else {
      uni.showToast({ title: '下载失败', icon: 'error' })
    }
  })

  dtask.start()
}
const msgLoading = ref(true)
interface EventTargetSendText {
  detail: { value: string }
}
type EventTypeTarget = string | EventTargetSendText
const sendTextLoading = ref(true) // false可以发送消息，true不可以发送，因为上一次发送未结束
const refreshSend = ref(false)
let stopStreamMsg = false
const stopMsg = () => {
  stopStreamMsg = true
}
async function sendText() {
  console.log('uploadList: ', uploadList)
  if (uploadList.length > 0) {
    const isUpLoading = uploadList.some((file) => {
      // return file.status==="error" || file.status==="pending"
      return file.status === 'uploading' || file.status === 'pending'
    })
    const isError = uploadList.some((file) => {
      // return file.status==="error" || file.status==="pending"
      return file.status === 'error'
    })
    if (isUpLoading) {
      return showToastErr('有文件正在上传')
    } else if (isError) {
      return showToastErr('有文件上传失败')
    }
  }
  const msg = inputText.value.trim()
  if (!msg && !refreshSend.value) {
    return showToastErr('不可以发送空消息!')
  }
  // if (uploadList.length > 0) {
  // 	return showToastErr('请等待文件上传完成!')
  // }
  if (!sendTextLoading.value) {
    sendTextLoading.value = true
    return showToastErr('正在接收消息请稍后')
  }
  // 开启加载状态
  sendTextLoading.value = false
  // 文本消息

  // 先判断是否上传文件，若有文件在判断文件类型，视频+图片不能与文档类文件同时上传

  // let body: IGptRequestBody;
  let body = {}
  let aiMsg = {}
  if (uploadList.length > 0 && !isUserOk) {
    let photo = [] // 媒体文件 参数中的content
    let video = [] // 媒体文件 参数中的content
    let fileList = [] // 文档文件
    // 格式化请求参数
    const { photoList, videoList, file } = formatParams(uploadList)
    if ((photoList.length > 0 || videoList.length > 0) && file.length > 0) {
      sendTextLoading.value = true
      // 媒体文件与文档同时上传时
      return showToastErr('视频或图片不能与文件同时上传')
    }
    addMessage({
      role: 'user',
      type: 'text',
      content: msg,
      timestamp: new Date(),
      mask: 'new',
    })

    if (photoList.length > 0 && file.length <= 0) {
      // 仅上传了媒体文件时
      chatMode.value = 'qwen-vl-max'
      aiMsg = {
        role: 'assistant',
        // role:"user",
        type: 'text',
        content: msg,
        timestamp: new Date(),
      }
      addMessage({
        role: 'user',
        // role:"user",
        type: 'image',
        content: deepClone(photoList),
        timestamp: new Date(),
        mask: 'new',
      })

      photoList.push({
        type: 'text',
        text: msg,
      })
      const lastMsg = {
        role: 'user',
        // role: 'assistant',
        type: 'image',
        content: photoList,
        mask: 'new',
      }
      historyUserMsgs.push(lastMsg)
      body = {
        model: chatMode.value, // 模型选择
        max_tokens: 1000,
        top_p: 1,
        presence_penalty: 0,
        frequency_penalty: 0,
        messages: deepClone(historyUserMsgs), // text ? [aiMsg] : historyUserMsgs,
        stream: true,
        listUuid: listUuid.value, // "eff18a10-1719-4528-ad63-ee5c01d0a412"
      }
      // 开始发请求 api
    }
    if (videoList.length > 0 && file.length <= 0) {
      chatMode.value = 'qwen-vl-max'
      aiMsg = {
        role: 'assistant',
        // role:"user",
        type: 'text',
        content: msg,
        timestamp: new Date(),
      }
      // let content = video;
      addMessage({
        role: 'user',
        type: 'video',
        content: deepClone(videoList),
        timestamp: new Date(),
        mask: 'new',
      })

      videoList.forEach((item) => {
        // item.video_url.poster
        if (item.type == 'video_url') {
          delete item.video_url.poster
        }
      })
      videoList.push({
        type: 'text',
        text: msg,
      })
      const lastMsg = {
        role: 'user',
        // role: 'assistant',
        content: videoList,
        mask: 'new',
      }
      historyUserMsgs.push(lastMsg)
      body = {
        model: chatMode.value, // 模型选择
        max_tokens: 1000,
        top_p: 1,
        presence_penalty: 0,
        frequency_penalty: 0,
        messages: deepClone(historyUserMsgs), // text ? [aiMsg] : historyUserMsgs,
        stream: true,
        listUuid: listUuid.value,
        // listUuid:"eff18a10-1719-4528-ad63-ee5c01d0a412"
      }
    } else if (photoList.length <= 0 && videoList.length <= 0 && file.length > 0) {
      // 仅上传了文档类文件时
      chatMode.value = 'qwen-long'
      aiMsg = {
        role: 'assistant',
        // role:"user",
        type: 'text',
        content: msg,
        timestamp: new Date(),
      }
      addMessage({
        role: 'user',
        type: 'file',
        content: deepClone(file),
        mask: 'new',
        timestamp: new Date(),
      })
      file.push({
        role: 'user',
        content: msg,
        mask: 'new',
      })
      historyUserMsgs.push(...file)
      body = {
        model: chatMode.value, // 模型选择
        // max_tokens: 1000,
        // top_p: 1,
        // presence_penalty: 0,
        // frequency_penalty: 0,
        messages: deepClone(historyUserMsgs), // text ? [aiMsg] : historyUserMsgs,
        stream: true,
        listUuid: listUuid.value,
        // listUuid:"eff18a10-1719-4528-ad63-ee5c01d0a412"
      }
    }
  } else {
    if (!refreshSend.value) {
      //不重发时触发
      addMessage({
        role: 'user',
        type: 'text',
        content: msg,
        timestamp: new Date(),
        mask: 'new',
      })
    }
    // 纯文本时发送
    chatMode.value = 'tongyi-app' //'tongyi-app'; qwen-long

    //更新上下文消息
    historyUserMsgs.push({
      role: 'user',
      content: msg,
      timestamp: new Date(),
    })
    aiMsg = {
      role: 'assistant',
      type: 'text',
      content: msg,
      timestamp: new Date(),
    }

    body = {
      model: chatMode.value, // 模型选择
      max_tokens: 1000,
      top_p: 1,
      presence_penalty: 0,
      frequency_penalty: 0,
      messages: deepClone(historyUserMsgs), // text ? [aiMsg] : historyUserMsgs,
      stream: true,
      listUuid: listUuid.value,
      // listUuid:"eff18a10-1719-4528-ad63-ee5c01d0a412"
    }
  }
  // 第一次发送纯文本消息，第二次发送图片+视频，第三次发送文档，此时因为历史消息都要一起发送给后端，
  // 所以要想办法在遇到这种情况时，截断历史记录，主动为用户建立一个新的回话，但是不需要清空历史记录

  console.log('message: ', messages)
  uploadList.splice(0, uploadList.length) // 清空上传的文件
  const list = formatData(messages)

  body.messages = list
  body.detail = JSON.stringify(messages)
  aiMsg.content = ''
  addMessage(aiMsg)
  // return
  // return
  // 没有上传文件，仅文字消息
  try {
    // aiMsg.content = ''
    // 发送问题到后端
    inputText.value = ''
    const controller = new AbortController()
    const signal = controller.signal
    const resp = await fetch(baseUrl + '/chat/app/completion', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Authorization: token.value },
      body: JSON.stringify(body),
      signal: signal,
    })

    const reader = resp.body!.getReader()
    const decoder = new TextDecoder()
    let buffer = ''
    let done = false

    while (!done) {
      if (stopStreamMsg) {
        // 立刻停下
        // reader.cancel();
        controller.abort()
        stopStreamMsg = false
      }
      const { value, done: streamDone } = await reader.read()
      done = streamDone
      if (value) {
        buffer += decoder.decode(value, { stream: true })
        const lines = buffer.split(/\r?\n/)
        buffer = lines.pop()!

        for (const line of lines) {
          // 只处理以 "data: " 开头的行
          if (!line.startsWith('data: ')) continue
          const chunk = line.slice(6).trim()

          if (chunk === '[DONE]') {
            done = true
            console.log('sss')
            break
          }
          try {
            const json = JSON.parse(chunk)
            const delta = json.choices?.[0]?.delta?.content
            if (delta) {
              msgLoading.value = false
              aiMsg.content += delta
              //每次更新messages消息，实现流式输出
              messages[messages.length - 1] = { ...aiMsg }
              scrollToBottom()
            }
          } catch {}
        }

        //更新上下文消息
        done && historyUserMsgs.push(aiMsg)
        historyUserMsgs.forEach((item) => {
          if (Array.isArray(item)) {
            item.forEach((ele) => {
              if (ele.mask === 'new') {
                ele.mask = ''
              }
            })
          } else if (item.role === 'system') {
            item.mask = ''
          } else {
            item.mask = ''
          }
        })
        messages.forEach((item) => {
          if (item.mask === 'new') {
            item.mask = ''
          }
        })
        console.log('chunk------------------: ')
      }
    }
    scrollToBottom()
  } catch (err) {
    // aiMsg.content = '请重新发送'
    // //更新messages消息
    // messages[messages.length - 1] = { ...aiMsg }
    console.error(err)
  } finally {
    sendTextLoading.value = true
    showActions.value = false
    refreshSend.value = false // 重发已经结束 关闭重发
    msgLoading.value = false
  }
}
function copyText(msg: IMessage) {
  if (typeof msg.content === 'string') {
    uni.setClipboardData({
      data: msg.content,
      success() {
        uni.showToast({ title: '已复制', icon: 'success' })
      },
      fail(err) {
        console.error('复制失败', err)
        uni.showToast({ title: '复制失败', icon: 'error' })
      },
    })
  }
}

function refreshText() {
  if (!sendTextLoading.value) {
    // 正在接收消息，不可以重发
    return
  }
  if (uploadList.length > 0) {
    return
  }
  // 1. 找到最后两条用户消息（用于处理图文混合场景）
  const userMessages = messages.filter((msg) => msg.role === 'user')
  // const lastTwoUserMsgs = userMessages.slice(-2)
  const [msg1, msg2] = deepClone(userMessages.slice(-2))
  // let text=lastTwoUserMsgs[0] // lastTwoUserMsgs.every((msg)=>msg.type==="text" || msg.type==="image" || msg.type==="video" || msg.type==="file")
  // let file=lastTwoUserMsgs[1] // lastTwoUserMsgs.every((msg)=>msg.type==="text" || msg.type==="image" || msg.type==="video" || msg.type==="file")

  // 2. 提取文本内容和文件列表
  let refreshText = null
  const refreshFiles: UploadFile[] = []
  if (msg1 && msg1.type === 'text' && msg2 && msg2.type !== 'text') {
    msg1.mask = 'new'
    msg2.mask = 'new'
    refreshFiles.push(msg1)
    refreshFiles.push(msg2)
  } else if (msg1.type === 'text' && msg1.role === 'user' && !msg2) {
    msg1.mask = 'new'
    refreshFiles.push(msg1)
  } else if (msg2.type === 'text' && msg2.role === 'user' && !msg1) {
    msg2.mask = 'new'
    refreshFiles.push(msg2)
  } else {
    msg2.mask = 'new'
    refreshFiles.push(msg2)
  }

  // lastTwoUserMsgs.forEach((msg,i) => {
  // 	console.log('msg: ',msg);
  // 	if (msg.type === 'text' && msg.role==="user") {
  // 		refreshText = msg.content // 总是取最新的文本
  // 		refreshFiles.push({
  // 			content:msg.content,
  // 			type:"text",
  // 			role:"user",
  // 			timestamp:new Date(),
  // 			mask:"new"
  // 		})
  // 	} else if(msg.type==="video"){
  // 		msg.mask="new"
  // 		refreshFiles.push(msg)
  // 		// msg.content.forEach((file : any) => {
  // 		// 	console.log('lastTwoUserMsgs file: ',file);

  // 		// 	refreshFiles.push({
  // 		// 		id: guid.getGuid(),
  // 		// 		url: file.video_url.url,
  // 		// 		status: 'success',
  // 		// 		name: file.name || '未命名文件',
  // 		// 		size: file.size || 0,
  // 		// 		uploadFileType: file.uploadFileType || detectFileType(file.video_url.url),
  // 		// 	})
  // 		// })
  // 	}else if(msg.type==="image"){
  // 		msg.mask="new"
  // 		refreshFiles.push(msg)
  // 		// msg.content.forEach((file : any) => {
  // 		// 	console.log('lastTwoUserMsgs file: ',file);
  // 		// 	refreshFiles.push({
  // 		// 		id: guid.getGuid(),
  // 		// 		url: file.image_url.url,
  // 		// 		status: 'success',
  // 		// 		name: file.name || '未命名文件',
  // 		// 		size: file.size || 0,
  // 		// 		uploadFileType: file.uploadFileType || detectFileType(file.image_url.url),
  // 		// 	})
  // 		// })
  // 	}else{
  // 		msg.mask="new"
  // 		refreshFiles.push(msg)
  // 		// msg.content.forEach((file : any) => {
  // 		// 	console.log('lastTwoUserMsgs file: ',file);
  // 		// 	refreshFiles.push({
  // 		// 		id: guid.getGuid(),
  // 		// 		url: file.content,
  // 		// 		status: 'success',
  // 		// 		name: file.name || '未命名文件',
  // 		// 		size: file.size || 0,
  // 		// 		uploadFileType: file.uploadFileType || detectFileType(file.content),
  // 		// 	})
  // 		// })
  // 	}
  // })

  // 3. 更新输入框和上传列表
  // inputText.value = refreshText
  // uploadList.splice(0, uploadList.length, ...refreshFiles)
  refreshFiles.forEach((ele) => {
    messages.push(ele)
  })
  refreshSend.value = true
  // inputText.value = refreshText
  // 4. 自动触发发送（模拟用户点击发送按钮）
  setTimeout(() => {
    sendText()
  }, 100)
}

// 文件类型检测函数（根据URL后缀）
function detectFileType(url: string) {
  console.log('url: ', url)
  const ext = url.split('.').pop()?.toLowerCase()
  if (['jpg', 'png', 'jpeg', 'gif'].includes(ext!)) return uploadFileTypeEm.image
  if (['mp4', 'mov', 'avi'].includes(ext!)) return uploadFileTypeEm.video
  return uploadFileTypeEm.file
}
const knowledgeOpen = ref(false)
async function toggleKnowledge() {
  if (chatMode.value !== 'tongyi-app') {
    chatMode.value = 'tongyi-app'
    isUserOk = true // 用户确认 tongyi-app
    await createChatSession()
    messages.splice(0, messages.length)
  } else {
    chatMode.value = 'qwen-vl-plus'
    isUserOk = false // 取消确认 tongyi-app
  }
  knowledgeOpen.value = !knowledgeOpen.value
  showActions.value = false
  rotation.value = 0
}

//收起键盘
const closeKeyboard = () => {
  uni.hideKeyboard()
  rotation.value = 0
}

//输入框聚焦事件
const onFocus = () => {
  showActions.value = false
  rotation.value = 0
}

const showPreview = ref(false)
const previewUrl = ref('') // 预览图片
const previewVideoUrl = ref('') // 预览视频
const previewTop = ref(30) // 距离顶部30px
const handleImageClick = (src) => {
  // let fileList = officeFileTypeList.value
  // officeFileTypeList=ref(['.docx','.doc','.excel','.pdf'])
  // const otherFileType=ref(['.txt'])
  // const otherFileType=ref(['.txt'])
  if (picFileType.value.includes(isFileType(src))) {
    previewUrl.value = src
    showPreview.value = true
  } else if (videoFileType.value.includes(isFileType(src))) {
    previewVideoUrl.value = src
    showPreview.value = true
  } else if (officeFileTypeList.value.includes(isFileType(src))) {
    previewFile(src)
  }
}

const closePreview = () => {
  showPreview.value = false
  previewUrl.value = ''
  previewVideoUrl.value = ''
}
const reload = () => {
  const currentWebview = plus.webview.currentWebview()
  currentWebview.reload(true)
}

const isFileType = (name: string) => {
  let reg = /\.[\w\d]+$/
  let match = name.match(reg)
  return match ? match[0] : null
}
</script>

<style lang="scss" scoped>
.slide-up-enter-active,
.slide-up-leave-active {
  transition: transform 0.3s ease-out;
}

.slide-up-enter-from,
.slide-up-leave-to {
  transform: translateY(100%);
}

.slide-up-enter-to,
.slide-up-leave-from {
  transform: translateY(0%);
}

.font-pf {
  font-family: PingFangSC-Medium, sans-serif;
}

.page {
  position: relative;
  width: 100%;
  height: 100%;
  background: #f8f8f8;
}
/* 顶部导航 */
.nav-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 88rpx;
  padding: 0 24rpx;
  background: #fff;
}

.nav-title {
  font-size: 32rpx;
  color: #333;
}

.nav-icon {
  width: 40rpx;
  height: 40rpx;
}
/* 遮罩 */
.overlay {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  background: rgba(0, 0, 0, 0.4);
}
/* 弹窗容器 */
.popup {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 60%;
  overflow: hidden;
  background: #fff;
  border-radius: 12rpx 12rpx 0 0;
}

.popup.fullscreen {
  height: 90%;
  border-radius: 0;
}
/* Header */
.popup-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 88rpx;
  padding: 0 24rpx;
  border-bottom: 1rpx solid #f0f0f0;
}

.popup-header .icon {
  width: 32rpx;
  height: 32rpx;
}

.popup-header .title {
  flex: 1;
  font-size: 32rpx;
  color: #333;
  text-align: center;
}
/* 内容区 */
.popup-body {
  flex: 1;
  padding: 24rpx 0;
}

.date-label {
  padding: 0 32rpx;
  margin-top: 24rpx;
  font-size: 24rpx;
  color: #999;
}

.history-item {
  padding: 24rpx 32rpx;
  border-bottom: 1rpx solid #f0f0f0;
}

.history-text {
  font-size: 28rpx;
  color: #333;
}

.tops {
  padding-top: var(--status-bar-height);
}
.flex-i {
  display: flex !important;
}
</style>
