|
|
@@ -0,0 +1,1083 @@
|
|
|
+<template>
|
|
|
+ <div class="ai-chat-popup" :style="popupStyle" ref="popupRef">
|
|
|
+ <div class="fullscreen-spin mask2" v-if="agentlistloading">
|
|
|
+ <div class="spin">
|
|
|
+ <div class="spinner"></div>
|
|
|
+ <div class="text">加载中...</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 弹窗头部 -->
|
|
|
+ <div class="ai-chat-header">
|
|
|
+ <h3>{{ lang.ssClassroomAiAssistant }}</h3>
|
|
|
+ <button class="close-btn" @click="$emit('close')">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round">
|
|
|
+ <line x1="18" y1="6" x2="6" y2="18"></line>
|
|
|
+ <line x1="6" y1="6" x2="18" y2="18"></line>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <!-- 聊天区域 v-if="messages.length > 0" -->
|
|
|
+ <div class="chat-section" ref="chatSection">
|
|
|
+ <!-- 消息列表 -->
|
|
|
+ <div v-for="(message, index) in messages" :key="index" class="chat-message">
|
|
|
+ <div class="message-content user-message chat" v-if="message.content">
|
|
|
+ <div v-html="message.content"></div>
|
|
|
+ <!-- 显示上传的文件 -->
|
|
|
+ <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
|
|
|
+ <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
|
|
|
+ <span>{{ file.title }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="message-content ai-message chat" v-if="message.aiContent || message.loading">
|
|
|
+ <div v-if="message.aiContent" v-html="message.aiContent"></div>
|
|
|
+ <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32"
|
|
|
+ viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
|
|
|
+ <circle cx="4" cy="12" r="3" fill="currentColor">
|
|
|
+ <animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s"
|
|
|
+ calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
|
|
|
+ </circle>
|
|
|
+ <circle cx="12" cy="12" r="3" fill="currentColor">
|
|
|
+ <animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline" dur="0.6s"
|
|
|
+ keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
|
|
|
+ </circle>
|
|
|
+ <circle cx="20" cy="12" r="3" fill="currentColor">
|
|
|
+ <animate id="svgSpinners3DotsBounce1" attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.2s"
|
|
|
+ calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
|
|
|
+ </circle>
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 输入区域 -->
|
|
|
+ <div class="input-section">
|
|
|
+ <div class="input-wrapper">
|
|
|
+ <div class="file-box" v-show="files.length">
|
|
|
+ <div v-for="(file, index) in files" :key="index" class="file-item">
|
|
|
+ <span class="file-name">{{ file.title }}</span>
|
|
|
+ <button class="remove-file-btn" @click="removeFile(index)">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round">
|
|
|
+ <line x1="18" y1="6" x2="6" y2="18"></line>
|
|
|
+ <line x1="6" y1="6" x2="18" y2="18"></line>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <textarea class="ai-input" placeholder="请输入你的问题..." v-model="inputText" @keyup.enter.exact="sendMessage"
|
|
|
+ />
|
|
|
+
|
|
|
+ </div>
|
|
|
+ <div class="input-actions">
|
|
|
+ <!-- <FileInput accept="*" @change="handleFileUpload" >
|
|
|
+ <button class="attach-btn">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
|
+ <path
|
|
|
+ d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48">
|
|
|
+ </path>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </FileInput> -->
|
|
|
+ <button class="send-btn" @click="sendMessage" v-if="!chatLoading">
|
|
|
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
|
|
+ stroke-linejoin="round">
|
|
|
+ <line x1="22" y1="2" x2="11" y2="13"></line>
|
|
|
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ <button class="send-btn stop" @click="stopMessage" v-if="chatLoading">
|
|
|
+ <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
|
+ <rect width="32" height="32" rx="16" fill="black" fill-opacity="0.4"></rect>
|
|
|
+ <path
|
|
|
+ d="M11.3333 12.333C11.3333 11.7807 11.781 11.333 12.3333 11.333H19.6666C20.2189 11.333 20.6666 11.7807 20.6666 12.333V19.6663C20.6666 20.2186 20.2189 20.6663 19.6666 20.6663H12.3333C11.781 20.6663 11.3333 20.2186 11.3333 19.6663V12.333Z"
|
|
|
+ fill="white" fill-opacity="0.9"></path>
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <!-- 输入时的快捷操作弹出 -->
|
|
|
+ <div class="quick-actions-popup" v-if="showQuickActions">
|
|
|
+ <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
|
|
|
+ @click="sendQuickAction(action)">{{ action }}</button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { ref, onMounted, useTemplateRef, nextTick, watch, computed } from 'vue'
|
|
|
+import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2, getAgentChatList, agentlistloading, insertChat } from '@/tools/aiChat'
|
|
|
+import { useSlidesStore } from '@/store'
|
|
|
+import { lang } from '@/main'
|
|
|
+import MarkdownIt from 'markdown-it'
|
|
|
+import { getWorkPageId } from '@/services/course'
|
|
|
+import FileInput from '@/components/FileInput2.vue'
|
|
|
+import axios from '@/services/config'
|
|
|
+import message from '@/utils/message'
|
|
|
+import { getClassById } from '@/services/course'
|
|
|
+
|
|
|
+interface ChatMessage {
|
|
|
+ uid?: string
|
|
|
+ role: 'ai' | 'user'
|
|
|
+ content?: string
|
|
|
+ aiContent?: string
|
|
|
+ oldContent?: string
|
|
|
+ loading?: boolean
|
|
|
+ chatloading?: boolean
|
|
|
+ gLoading?: boolean
|
|
|
+ rawContent?: string
|
|
|
+ timestamp?: Date
|
|
|
+ like?: boolean
|
|
|
+ unlike?: boolean
|
|
|
+ isTyping?: boolean
|
|
|
+ AI?: string
|
|
|
+ isShowSynchronization?: boolean
|
|
|
+ filename?: string
|
|
|
+ is_mind_map?: boolean
|
|
|
+ sourceFiles?: Array<{
|
|
|
+ title: string
|
|
|
+ id?: string
|
|
|
+ url?: string
|
|
|
+ }>
|
|
|
+ jsonData?: {
|
|
|
+ gType?: string
|
|
|
+ isGenerate?: boolean
|
|
|
+ headUrl?: string
|
|
|
+ assistantName?: string
|
|
|
+ files?: Array<{
|
|
|
+ title: string
|
|
|
+ id?: string
|
|
|
+ url?: string
|
|
|
+ }>
|
|
|
+ sourceArray?: Array<{
|
|
|
+ text?: string
|
|
|
+ id?: string
|
|
|
+ title?: string
|
|
|
+ }>
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const props = withDefaults(defineProps<{
|
|
|
+ userid?: string | null
|
|
|
+ position?: { x: number; y: number }
|
|
|
+ workJson?: any
|
|
|
+ visible?: boolean
|
|
|
+ cid?: string | null
|
|
|
+}>(), {
|
|
|
+ userid: null,
|
|
|
+ position: () => ({ x: 0, y: 0 }),
|
|
|
+ workJson: () => ({}),
|
|
|
+ visible: false,
|
|
|
+ cid: null
|
|
|
+})
|
|
|
+
|
|
|
+const emit = defineEmits(['close'])
|
|
|
+
|
|
|
+const grade = ref('')
|
|
|
+
|
|
|
+watch(() => props.visible, (newVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ session_name.value = props.workJson.id || ''
|
|
|
+ console.log('workJson', props.workJson)
|
|
|
+ getAgentChatList(props.workJson.id, props.userid || '').then((res) => {
|
|
|
+ console.log('res', res)
|
|
|
+ messages.value = res
|
|
|
+ if (messages.value.length === 0) {
|
|
|
+ messages.value.push({
|
|
|
+ role: 'user',
|
|
|
+ content: '',
|
|
|
+ })
|
|
|
+ messages.value.at(-1).loading = true
|
|
|
+ messages.value.at(-1).chatloading = true
|
|
|
+ messages.value.at(-1).sourceFiles = files.value.filter(file => file.id !== null).map(file => ({
|
|
|
+ title: file.title,
|
|
|
+ id: file.id
|
|
|
+ }))
|
|
|
+ chatLoading.value = true
|
|
|
+ sendAction('')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+watch(() => props.cid, (newVal) => {
|
|
|
+ if (newVal) {
|
|
|
+ getClassById({
|
|
|
+ id: newVal
|
|
|
+ }).then(res => {
|
|
|
+ console.log('res年级', res)
|
|
|
+ grade.value = res[0][0].name || ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 计算弹窗样式
|
|
|
+// 弹窗引用
|
|
|
+const popupRef = useTemplateRef<HTMLElement>('popupRef')
|
|
|
+
|
|
|
+// 计算弹窗样式
|
|
|
+const popupStyle = computed(() => {
|
|
|
+ // 获取slideListWrap的尺寸和位置
|
|
|
+ const slideListWrap = document.querySelector('.slide-list-wrap')
|
|
|
+ if (!slideListWrap) {
|
|
|
+ // 如果找不到slideListWrap,使用默认位置
|
|
|
+ return {
|
|
|
+ right: `${props.position.x + 10}px`,
|
|
|
+ bottom: `${props.position.y + 60}px`
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const wrapRect = slideListWrap.getBoundingClientRect()
|
|
|
+ // 使用实际的弹窗宽高,考虑边距
|
|
|
+ const popupWidth = popupRef.value?.clientWidth || 320 // 实际弹窗宽度
|
|
|
+ const popupHeight = popupRef.value?.clientHeight || 400 // 实际弹窗高度
|
|
|
+ const buttonWidth = 105 // 按钮宽度
|
|
|
+ const buttonHeight = 50 // 按钮高度
|
|
|
+ const margin = 10 // 弹窗与屏幕边缘的最小距离
|
|
|
+
|
|
|
+ // 计算按钮在slideListWrap内的实际位置
|
|
|
+ const buttonRight = props.position.x
|
|
|
+ const buttonBottom = props.position.y
|
|
|
+ const buttonLeft = wrapRect.width - buttonRight - buttonWidth
|
|
|
+ const buttonTop = wrapRect.height - buttonBottom - buttonHeight
|
|
|
+
|
|
|
+ console.log('按钮位置:', buttonTop, buttonLeft)
|
|
|
+ console.log('弹窗尺寸:', popupHeight, popupWidth)
|
|
|
+ // 判断弹窗显示方向
|
|
|
+ const style: any = {}
|
|
|
+
|
|
|
+ // 判断垂直方向
|
|
|
+ if (buttonTop < popupHeight + margin + buttonHeight) {
|
|
|
+ // 按钮在上方,弹窗向下显示
|
|
|
+ style.top = `${wrapRect.top + wrapRect.height - buttonBottom - margin}px`
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // 按钮在下方,弹窗向上显示
|
|
|
+ style.bottom = `${(buttonBottom) + buttonHeight + margin}px`
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断水平方向
|
|
|
+ if (buttonRight < popupWidth + margin) {
|
|
|
+ // 按钮在右侧,弹窗向左显示
|
|
|
+ style.right = `${buttonRight}px`
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ // 按钮在左侧,弹窗向右显示
|
|
|
+ style.left = `${buttonLeft}px`
|
|
|
+ }
|
|
|
+
|
|
|
+ return style
|
|
|
+})
|
|
|
+
|
|
|
+const inputText = ref('')
|
|
|
+const messages = ref<ChatMessage[]>([])
|
|
|
+const chatSection = useTemplateRef<HTMLElement>('chatSection')
|
|
|
+const chatLoading = ref(false)
|
|
|
+const showQuickActions = ref(false)
|
|
|
+const streamController = ref<{ abort: () => void } | null>(null)
|
|
|
+const noStreamController = ref<{ promise: Promise<string>; abort: () => void } | null>(null)
|
|
|
+const files = ref<Array<{ title: string; id?: string | null; url?: string; isProcessing?: boolean; cancel?: () => void }>>([])
|
|
|
+
|
|
|
+// 快捷操作短语数组
|
|
|
+const quickActions = [
|
|
|
+ lang.ssAiChatQuickAction1,
|
|
|
+ lang.ssAiChatQuickAction2,
|
|
|
+]
|
|
|
+
|
|
|
+// 监听输入变化,当输入"/"时显示快捷操作
|
|
|
+watch(inputText, (newValue) => {
|
|
|
+ // if (messages.value.length > 0 && newValue === '/') {
|
|
|
+ // showQuickActions.value = true
|
|
|
+ // }
|
|
|
+ // else if (newValue !== '/') {
|
|
|
+ // showQuickActions.value = false
|
|
|
+ // }
|
|
|
+})
|
|
|
+
|
|
|
+const sendMessage = () => {
|
|
|
+ if (chatLoading.value) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 检查是否有文件正在处理中
|
|
|
+ const hasProcessingFile = files.value.some(file => file.isProcessing)
|
|
|
+ if (hasProcessingFile) {
|
|
|
+ message.error(lang.ssAiChatWaitUpload)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if (inputText.value.trim() || files.value.length > 0) {
|
|
|
+ // 添加用户消息
|
|
|
+ messages.value.push({
|
|
|
+ role: 'user',
|
|
|
+ content: inputText.value,
|
|
|
+ })
|
|
|
+ // 模拟AI回复
|
|
|
+ // setTimeout(() => {
|
|
|
+
|
|
|
+ // setTimeout(() => {
|
|
|
+ // messages.value.at(-1).aiContent = '课程生成完成!为您创建了5个内容页面和3个互动工具。您可以查看底部课程大纲,或在中央区域开始编辑。',
|
|
|
+ // messages.value.at(-1).jsonData = {
|
|
|
+ // isChoice: true
|
|
|
+ // }
|
|
|
+ // }, 1000)
|
|
|
+ // }, 500)
|
|
|
+ prevChatResult()
|
|
|
+ messages.value.at(-1).loading = true
|
|
|
+ messages.value.at(-1).chatloading = true
|
|
|
+ messages.value.at(-1).sourceFiles = files.value.filter(file => file.id !== null).map(file => ({
|
|
|
+ title: file.title,
|
|
|
+ id: file.id
|
|
|
+ }))
|
|
|
+ chatLoading.value = true
|
|
|
+ sendAction(inputText.value)
|
|
|
+ inputText.value = ''
|
|
|
+ files.value = []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const stopMessage = () => {
|
|
|
+ if (streamController.value) {
|
|
|
+ streamController.value.abort()
|
|
|
+ streamController.value = null
|
|
|
+ }
|
|
|
+ if (noStreamController.value) {
|
|
|
+ noStreamController.value.abort()
|
|
|
+ noStreamController.value = null
|
|
|
+ }
|
|
|
+ chatLoading.value = false
|
|
|
+ if (messages.value.length > 0) {
|
|
|
+ messages.value.at(-1).chatloading = false
|
|
|
+ messages.value.at(-1).loading = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 处理文件上传
|
|
|
+const handleFileUpload = async (files2: File[]) => {
|
|
|
+ const maxSize = 10 * 1024 * 1024 // 10MB
|
|
|
+ const uploadPromises = []
|
|
|
+
|
|
|
+ for (let i = 0; i < files2.length; i++) {
|
|
|
+ const file = files2[i]
|
|
|
+ if (file.size > maxSize) {
|
|
|
+ message.error(lang.ssAiChatFileSizeLimit)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ // 先添加文件到列表,显示解析中状态
|
|
|
+ const fileIndex = files.value.length
|
|
|
+ files.value.push({
|
|
|
+ title: file.name + ' (' + lang.ssAiChatParsing + ')',
|
|
|
+ id: null,
|
|
|
+ isProcessing: true,
|
|
|
+ cancel: null
|
|
|
+ })
|
|
|
+ // 创建取消控制器
|
|
|
+ const controller = new AbortController()
|
|
|
+ files.value[fileIndex].cancel = () => {
|
|
|
+ controller.abort()
|
|
|
+ files.value.splice(fileIndex, 1)
|
|
|
+ }
|
|
|
+ // 创建上传Promise并添加到数组
|
|
|
+ const uploadPromise = uploadFile2(file, controller.signal).then(res => {
|
|
|
+ if (!res) {
|
|
|
+ files.value.splice(fileIndex, 1)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 上传成功,更新文件状态
|
|
|
+ files.value[fileIndex] = {
|
|
|
+ title: file.name,
|
|
|
+ id: res.results.document_id,
|
|
|
+ isProcessing: false
|
|
|
+ }
|
|
|
+ }).catch(error => {
|
|
|
+ if (error.name !== 'AbortError') {
|
|
|
+ console.error(lang.ssAiChatUploadFailed, error)
|
|
|
+ files.value.splice(fileIndex, 1)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ uploadPromises.push(uploadPromise)
|
|
|
+ }
|
|
|
+
|
|
|
+ // 等待所有文件上传完成
|
|
|
+ await Promise.allSettled(uploadPromises)
|
|
|
+}
|
|
|
+
|
|
|
+// 移除文件
|
|
|
+const removeFile = (index: number) => {
|
|
|
+ const file = files.value[index]
|
|
|
+ if (file && file.isProcessing && file.cancel) {
|
|
|
+ file.cancel()
|
|
|
+ }
|
|
|
+ files.value.splice(index, 1)
|
|
|
+}
|
|
|
+
|
|
|
+const prevChatResult = () => {
|
|
|
+ nextTick(() => {
|
|
|
+ if (chatSection.value) {
|
|
|
+ chatSection.value.scrollTop = chatSection.value.scrollHeight
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const sendQuickAction = (action: string) => {
|
|
|
+ inputText.value = action
|
|
|
+ sendMessage()
|
|
|
+}
|
|
|
+
|
|
|
+import { v4 as uuidv4 } from 'uuid'
|
|
|
+const session_name = ref('')
|
|
|
+const slidesStore = useSlidesStore()
|
|
|
+const gType = ref('chat')
|
|
|
+
|
|
|
+const sendAction = async (action: string) => {
|
|
|
+ const md = new MarkdownIt()
|
|
|
+
|
|
|
+ let choice = ''
|
|
|
+ switch (props.workJson.atool) {
|
|
|
+ case '45':
|
|
|
+ choice = '选择题'
|
|
|
+ break
|
|
|
+ case '15':
|
|
|
+ choice = '问答题'
|
|
|
+ break
|
|
|
+ default:
|
|
|
+ choice = ''
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ let question_content = ''
|
|
|
+ let correct_answer = ''
|
|
|
+ let student_answer = ''
|
|
|
+ const workContent = JSON.parse(decodeURIComponent(props.workJson.content))
|
|
|
+ if (props.workJson.atool === '45') {
|
|
|
+ const testJson = workContent.testJson || []
|
|
|
+ for (let i = 0; i < testJson.length; i++) {
|
|
|
+ question_content += `第${i + 1}题:${testJson[i].teststitle}\n 选项:${testJson[i].checkList}`
|
|
|
+ correct_answer += `第${i + 1}题:${testJson[i].answer}` + '\n'
|
|
|
+ student_answer += `第${i + 1}题:${testJson[i].userAnswer}` + '\n'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if (props.workJson.atool === '15') {
|
|
|
+ question_content = workContent.answerQ || ''
|
|
|
+ correct_answer = workContent.evaluationCriteria || '无'
|
|
|
+ student_answer = workContent.answer || ''
|
|
|
+ }
|
|
|
+
|
|
|
+ let promptText = ``
|
|
|
+ if (props.workJson.atool === '45') {
|
|
|
+ promptText += `
|
|
|
+ 当前题目信息:
|
|
|
+ - 题目类型:${choice}
|
|
|
+ - 题目内容:${question_content}
|
|
|
+ - 正确答案:${correct_answer}(0,1,3是选项的下标,学生的答案也是)
|
|
|
+ - 学生提交的回答:${student_answer}
|
|
|
+ `
|
|
|
+ }
|
|
|
+ else if (props.workJson.atool === '15') {
|
|
|
+ promptText += `
|
|
|
+ 当前题目信息:
|
|
|
+ - 题目类型:${choice}
|
|
|
+ - 题目标题:${question_content}
|
|
|
+ - 评分要点:${correct_answer}
|
|
|
+ - 学生提交的回答:${student_answer}
|
|
|
+ `
|
|
|
+ }
|
|
|
+
|
|
|
+ const prompt = `
|
|
|
+ 当前课程信息:
|
|
|
+ - 课程年级:${grade.value}}
|
|
|
+
|
|
|
+ ${promptText}
|
|
|
+ query:${action}
|
|
|
+ `
|
|
|
+ chat_stream(prompt, agentid2.value, props.userid || '', lang.lang, (event) => {
|
|
|
+ if (event.type === 'message') {
|
|
|
+ messages.value.at(-1).aiContent = md.render(event.data)
|
|
|
+
|
|
|
+ messages.value.at(-1).loading = false
|
|
|
+ prevChatResult()
|
|
|
+ }
|
|
|
+ else if (event.type === 'messageEnd') {
|
|
|
+ messages.value.at(-1).aiContent = md.render(event.data)
|
|
|
+ messages.value.at(-1).chatloading = false
|
|
|
+ chatLoading.value = false
|
|
|
+ prevChatResult()
|
|
|
+ insertChat({
|
|
|
+ answer: messages.value.at(-1).aiContent,
|
|
|
+ problem: messages.value.at(-1).content,
|
|
|
+ type: 'chat',
|
|
|
+ alltext: messages.value.at(-1).aiContent,
|
|
|
+ assistant_id: props.workJson.id
|
|
|
+ })
|
|
|
+ }
|
|
|
+ }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
|
|
|
+ streamController.value = controller
|
|
|
+ }).catch(err => {
|
|
|
+ chatLoading.value = false
|
|
|
+ console.log('err', err)
|
|
|
+ stopMessage()
|
|
|
+ })
|
|
|
+}
|
|
|
+import useCreateElement from '@/hooks/useCreateElement'
|
|
|
+import useSlideHandler from '@/hooks/useSlideHandler'
|
|
|
+const { createSlide } = useSlideHandler()
|
|
|
+const { createFrameElement } = useCreateElement()
|
|
|
+
|
|
|
+
|
|
|
+const setPageId = async (tool: any, json: any) => {
|
|
|
+ const res = await getWorkPageId({
|
|
|
+ userid: props.userid || '',
|
|
|
+ type: tool,
|
|
|
+ json: json
|
|
|
+ })
|
|
|
+ return res[0][0].id
|
|
|
+}
|
|
|
+
|
|
|
+// 上传文件
|
|
|
+const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
|
|
|
+ try {
|
|
|
+ const uuid = uuidv4()
|
|
|
+ const formData = new FormData()
|
|
|
+ const timestamp = Date.now()
|
|
|
+ const finalExtension = file.name.split('.').pop()?.toLowerCase() || ''
|
|
|
+ const baseName = file.name.slice(0, -(finalExtension.length + 1))
|
|
|
+
|
|
|
+ formData.append(
|
|
|
+ 'file',
|
|
|
+ new File([file], `${baseName}${timestamp}.${finalExtension}`)
|
|
|
+ )
|
|
|
+ formData.append('collection_ids', JSON.stringify([]))
|
|
|
+ formData.append('id', uuid)
|
|
|
+ formData.append('metadata', JSON.stringify({ title: file.name }))
|
|
|
+ formData.append('ingestion_mode', 'fast')
|
|
|
+ formData.append('run_with_orchestration', 'true')
|
|
|
+
|
|
|
+ // 同步知识库
|
|
|
+ const res = await axios.post(
|
|
|
+ 'https://r2rserver.cocorobo.cn/v3/documents',
|
|
|
+ formData,
|
|
|
+ {
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'multipart/form-data',
|
|
|
+ },
|
|
|
+ signal: signal
|
|
|
+ }
|
|
|
+ )
|
|
|
+
|
|
|
+ console.log(res)
|
|
|
+ return res
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ console.log('err', error)
|
|
|
+ if (error.name === 'AbortError') {
|
|
|
+ throw error
|
|
|
+ }
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+// const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
|
|
|
+const agentid2 = ref('bdcb2d5b-9dd6-4b1b-8cef-b34ce5579c5e')// 生成内容
|
|
|
+
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ // session_name.value = uuidv4()
|
|
|
+ // getAgentModel(agentid1.value)
|
|
|
+ getAgentModel(agentid2.value)
|
|
|
+ if (props.cid) {
|
|
|
+ getClassById({
|
|
|
+ id: props.cid
|
|
|
+ }).then(res => {
|
|
|
+ console.log('res年级', props.cid, res)
|
|
|
+ grade.value = res[0][0].name || ''
|
|
|
+ })
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.ai-chat-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ padding: 16px;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-chat-popup {
|
|
|
+ position: absolute;
|
|
|
+ width: 400px;
|
|
|
+ height: 500px;
|
|
|
+ background: #fffefa;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ // padding: 16px;
|
|
|
+ gap: 16px;
|
|
|
+ z-index: 9999;
|
|
|
+ border-radius: 10px;
|
|
|
+ border: 2px solid #fcefc6;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-chat-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ height: 60px;
|
|
|
+ width: 100%;
|
|
|
+ padding: 0 16px;
|
|
|
+ border-bottom: 2px solid #fcefc6;
|
|
|
+ box-sizing: border-box;
|
|
|
+
|
|
|
+ .close-btn {
|
|
|
+ margin-left: auto;
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 4px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ color: #000;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 5px;
|
|
|
+ border: 2px solid #fcefc6;
|
|
|
+ box-sizing: border-box;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.chat-section {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+
|
|
|
+.input-section {
|
|
|
+ display: flex;
|
|
|
+ gap: 10px;
|
|
|
+ padding: 16px;
|
|
|
+ width: 100%;
|
|
|
+ border-top: 2px solid #fcefc6;
|
|
|
+}
|
|
|
+
|
|
|
+.input-wrapper {
|
|
|
+ position: relative;
|
|
|
+ display: flex;
|
|
|
+ background: #fff;
|
|
|
+ border: 2px solid #fcefc6;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ min-height: 70px;
|
|
|
+ flex: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.ai-input {
|
|
|
+ flex: 1;
|
|
|
+ border: none;
|
|
|
+ background: transparent;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #374151;
|
|
|
+ outline: none;
|
|
|
+ resize: none;
|
|
|
+ // min-height: 80px;
|
|
|
+
|
|
|
+ &::placeholder {
|
|
|
+ color: #9CA3AF;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.input-actions {
|
|
|
+ display: flex;
|
|
|
+ // justify-content: space-between;
|
|
|
+ // align-items: center;
|
|
|
+ margin-top: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.attach-btn {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ padding: 4px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ color: #6b7280;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #FFF4E5;
|
|
|
+ color: #F78B22;
|
|
|
+ border-radius: 4px;
|
|
|
+ }
|
|
|
+
|
|
|
+ input[type="file"] {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.file-box {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ min-height: 24px;
|
|
|
+ max-height: 70px;
|
|
|
+ overflow-y: auto;
|
|
|
+
|
|
|
+ .file-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background: #f5f5f5;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+
|
|
|
+ .file-name {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #374151;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .remove-file-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #9CA3AF;
|
|
|
+ font-size: 12px;
|
|
|
+ padding: 2px;
|
|
|
+ margin-left: 8px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ width: 14px;
|
|
|
+ height: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #EF4444;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.send-btn {
|
|
|
+ margin-left: auto;
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ border: none;
|
|
|
+ background: #FF9300;
|
|
|
+ color: #fff;
|
|
|
+ border-radius: 50%;
|
|
|
+ cursor: pointer;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #E68A00;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.stop {
|
|
|
+ background: unset;
|
|
|
+ width: auto;
|
|
|
+
|
|
|
+ svg {
|
|
|
+ width: 32px;
|
|
|
+ height: 32px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.quick-actions {
|
|
|
+ // display: flex;
|
|
|
+ // flex-direction: column;
|
|
|
+ // gap: 8px;
|
|
|
+
|
|
|
+ .quick-action-btn {
|
|
|
+ padding: 5px 10px;
|
|
|
+ border: 1px solid #f7c58f;
|
|
|
+ background: #FFF9F2;
|
|
|
+ color: #6b4a1f;
|
|
|
+ border-radius: 16px;
|
|
|
+ font-size: 12px;
|
|
|
+ cursor: pointer;
|
|
|
+ text-align: left;
|
|
|
+ display: block;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ border-color: #F78B22;
|
|
|
+ background: #FFF4E5;
|
|
|
+ color: #111827;
|
|
|
+ }
|
|
|
+
|
|
|
+ +.quick-action-btn {
|
|
|
+ margin-top: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+.chat-section {
|
|
|
+ // flex: 1;
|
|
|
+ height: calc(100% - 155px);
|
|
|
+ overflow-y: auto;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ // gap: 16px;
|
|
|
+ padding: 0 10px;
|
|
|
+
|
|
|
+ &::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &::-webkit-scrollbar-track {
|
|
|
+ background: #F3F4F6;
|
|
|
+ border-radius: 3px;
|
|
|
+ }
|
|
|
+
|
|
|
+ &::-webkit-scrollbar-thumb {
|
|
|
+ background: #D1D5DB;
|
|
|
+ border-radius: 3px;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #9CA3AF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.chat-message {
|
|
|
+ max-width: 100%;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+
|
|
|
+ .message-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 8px 10px;
|
|
|
+ font-size: 14px;
|
|
|
+ line-height: 1.5;
|
|
|
+ color: #374151;
|
|
|
+ width: fit-content;
|
|
|
+
|
|
|
+ +.message-content {
|
|
|
+ margin-top: 10px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.message-content {
|
|
|
+ &.ai-message {
|
|
|
+ align-self: flex-start;
|
|
|
+ background: #fff;
|
|
|
+ border: 1.5px solid #fcefc6;
|
|
|
+ border-bottom-left-radius: 2px;
|
|
|
+
|
|
|
+ &>svg {
|
|
|
+ width: 17px;
|
|
|
+ height: 17px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.message-content {
|
|
|
+ &.user-message {
|
|
|
+ align-self: flex-end;
|
|
|
+ background: #FFF4E5;
|
|
|
+ border: 1.5px solid #F78B22;
|
|
|
+ border-bottom-right-radius: 2px;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.initial-state {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: flex-start;
|
|
|
+ align-items: flex-start;
|
|
|
+ // padding: 24px;
|
|
|
+ gap: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+.confirm-btn {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding: 6px 15px;
|
|
|
+ background: #FF9300;
|
|
|
+ color: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-left: auto;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #E68A00;
|
|
|
+ }
|
|
|
+
|
|
|
+ &.disabled {
|
|
|
+ background: #9CA3AF;
|
|
|
+ cursor: not-allowed;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+ul {
|
|
|
+ margin: 8px 0;
|
|
|
+ padding-left: 20px;
|
|
|
+
|
|
|
+ li {
|
|
|
+ margin: 4px 0;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.quick-actions-popup {
|
|
|
+ position: absolute;
|
|
|
+ bottom: 100%;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ background: white;
|
|
|
+ border: 1px solid #E5E7EB;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
|
+ padding: 8px;
|
|
|
+ z-index: 100;
|
|
|
+ margin-bottom: 8px;
|
|
|
+
|
|
|
+ .quick-action-btn {
|
|
|
+ width: 100%;
|
|
|
+ text-align: left;
|
|
|
+ padding: 10px 12px;
|
|
|
+ background: white;
|
|
|
+ border: none;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #374151;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ background: #fff4e5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.fullscreen-spin {
|
|
|
+ position: absolute;
|
|
|
+ top: 0;
|
|
|
+ bottom: 0;
|
|
|
+ left: 0;
|
|
|
+ right: 0;
|
|
|
+ z-index: 100;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+
|
|
|
+ &.mask2 {
|
|
|
+ background-color: rgba($color: #f1f1f1, $alpha: .7);
|
|
|
+ }
|
|
|
+}
|
|
|
+.spin {
|
|
|
+ width: 200px;
|
|
|
+ height: 200px;
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ margin-top: -100px;
|
|
|
+ margin-left: -100px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+.spinner {
|
|
|
+ width: 36px;
|
|
|
+ height: 36px;
|
|
|
+ border: 3px solid $themeColor;
|
|
|
+ border-top-color: transparent;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spinner .8s linear infinite;
|
|
|
+}
|
|
|
+.text {
|
|
|
+ margin-top: 20px;
|
|
|
+ color: $themeColor;
|
|
|
+}
|
|
|
+@keyframes spinner {
|
|
|
+ 0% {
|
|
|
+ transform: rotate(0deg);
|
|
|
+ }
|
|
|
+ 100% {
|
|
|
+ transform: rotate(360deg);
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|
|
|
+<style>
|
|
|
+.chat table {
|
|
|
+ text-align: center;
|
|
|
+ border-spacing: 0;
|
|
|
+ border-left: 1px solid #000;
|
|
|
+ border-bottom: 1px solid #000;
|
|
|
+}
|
|
|
+
|
|
|
+.chat table td,
|
|
|
+.chat table th {
|
|
|
+ border-top: 1px solid #000;
|
|
|
+ border-right: 1px solid #000;
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-files {
|
|
|
+ margin-top: 8px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.message-file-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 6px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #6b7280;
|
|
|
+ background: #f3f4f6;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ max-width: 200px;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.message-file-item span {
|
|
|
+ flex: 1;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+}
|
|
|
+</style>
|