Просмотр исходного кода

Merge branch 'beta' of https://git.cocorobo.cn/jack/PPT into beta

jack 2 недель назад
Родитель
Сommit
aee4c57cb9

+ 1 - 0
.gitignore

@@ -10,6 +10,7 @@ lerna-debug.log*
 node_modules
 .DS_Store
 dist
+package-lock.json
 dist-ssr
 coverage
 *.local

+ 25 - 7
package-lock.json

@@ -22,7 +22,7 @@
         "html2canvas": "^1.4.1",
         "katex": "^0.16.22",
         "lodash": "^4.17.21",
-        "markdown-it": "^14.1.0",
+        "markdown-it": "^14.1.1",
         "mitt": "^3.0.1",
         "nanoid": "^5.0.7",
         "number-precision": "^1.6.0",
@@ -45,6 +45,7 @@
         "svg-pathdata": "^7.1.0",
         "tinycolor2": "^1.6.0",
         "tippy.js": "^6.3.7",
+        "uuid": "^13.0.0",
         "vue": "^3.5.17",
         "vuedraggable": "^4.1.0",
         "wangeditor": "^4.7.15",
@@ -4162,9 +4163,9 @@
       }
     },
     "node_modules/markdown-it": {
-      "version": "14.1.0",
-      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
-      "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+      "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
       "dependencies": {
         "argparse": "^2.0.1",
         "entities": "^4.4.0",
@@ -5888,6 +5889,18 @@
         "base64-arraybuffer": "^1.0.2"
       }
     },
+    "node_modules/uuid": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+      "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==",
+      "funding": [
+        "https://github.com/sponsors/broofa",
+        "https://github.com/sponsors/ctavan"
+      ],
+      "bin": {
+        "uuid": "dist-node/bin/uuid"
+      }
+    },
     "node_modules/validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -9250,9 +9263,9 @@
       "dev": true
     },
     "markdown-it": {
-      "version": "14.1.0",
-      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
-      "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+      "version": "14.1.1",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+      "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
       "requires": {
         "argparse": "^2.0.1",
         "entities": "^4.4.0",
@@ -10584,6 +10597,11 @@
         "base64-arraybuffer": "^1.0.2"
       }
     },
+    "uuid": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz",
+      "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="
+    },
     "validate-npm-package-license": {
       "version": "3.0.4",
       "resolved": "https://registry.npmmirror.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",

+ 2 - 1
package.json

@@ -29,7 +29,7 @@
     "html2canvas": "^1.4.1",
     "katex": "^0.16.22",
     "lodash": "^4.17.21",
-    "markdown-it": "^14.1.0",
+    "markdown-it": "^14.1.1",
     "mitt": "^3.0.1",
     "nanoid": "^5.0.7",
     "number-precision": "^1.6.0",
@@ -52,6 +52,7 @@
     "svg-pathdata": "^7.1.0",
     "tinycolor2": "^1.6.0",
     "tippy.js": "^6.3.7",
+    "uuid": "^13.0.0",
     "vue": "^3.5.17",
     "vuedraggable": "^4.1.0",
     "wangeditor": "^4.7.15",

+ 30 - 23
src/App.vue

@@ -1,28 +1,35 @@
 <template>
   <template v-if="slides.length">
-    <Screen v-if="viewMode !== 'student' && screening" />
-    <Editor
-      v-if="viewMode === 'editor' && _isPC && !screening"
-      :courseid="urlParams.courseid"
-    />
-    <Editor2
-      v-else-if="viewMode === 'editor2' && _isPC && !screening"
-      :courseid="urlParams.courseid"
-    />
-    <Editor3
-      v-else-if="viewMode === 'editor3' && _isPC && !screening"
-      :courseid="urlParams.courseid"
-    />
-    <Student
-      v-else-if="viewMode === 'student'"
-      :courseid="urlParams.courseid"
-      :type="urlParams.type"
-      :userid="urlParams.userid"
-      :oid="urlParams.oid"
-      :org="urlParams.org"
-      :cid="urlParams.cid"
-    />
-    <Mobile v-else />
+    <Screen v-if="viewMode !== 'student' && screening" key="screen" />
+    <KeepAlive>
+      <Editor
+        v-if="viewMode === 'editor' && _isPC && !screening"
+        :courseid="urlParams.courseid"
+        key="editor"
+      />
+      <Editor2
+        v-else-if="viewMode === 'editor2' && _isPC && !screening"
+        :courseid="urlParams.courseid"
+        key="editor2"
+      />
+      <Editor3
+        v-else-if="viewMode === 'editor3' && _isPC && !screening"
+        :courseid="urlParams.courseid"
+        :userid="urlParams.userid"
+        key="editor3"
+      />
+      <Student
+        v-else-if="viewMode === 'student'"
+        :courseid="urlParams.courseid"
+        :type="urlParams.type"
+        :userid="urlParams.userid"
+        :oid="urlParams.oid"
+        :org="urlParams.org"
+        :cid="urlParams.cid"
+        key="student"
+      />
+      <Mobile v-else key="mobile" />
+    </KeepAlive>
   </template>
   <FullscreenSpin :tip="lang.ssInitDataWait" v-else loading :mask="false" />
 </template>

+ 430 - 0
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -0,0 +1,430 @@
+<template>
+    <div class="ai-chat-container">
+        <!-- 聊天区域 -->
+        <div class="chat-section" v-if="messages.length > 0" 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>
+                <div class="message-content ai-message chat" v-if="message.aiContent">
+                    <div v-html="message.aiContent"></div>
+                    <button class="confirm-btn" v-if="message.jsonData?.isChoice">确定</button>
+                </div>
+            </div>
+        </div>
+        <!-- 输入区域 -->
+        <div class="input-section">
+            <div class="input-wrapper">
+                <textarea class="ai-input"
+                    :placeholder="messages.length === 0 ? '例如:创建45分钟的四年级教学内容为水的三态变化的课程' : '输入 / 获取快捷操作短语'"
+                    v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
+                <div class="input-actions">
+                    <button class="attach-btn" v-show="false">
+                        <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>
+                    <button class="send-btn" @click="sendMessage">
+                        <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>
+                </div>
+            </div>
+        </div>
+        <!-- 初始状态 -->
+        <div class="initial-state" v-if="messages.length === 0">
+            <!-- 快捷操作 -->
+            <div class="quick-actions">
+                <button class="quick-action-btn" @click="sendQuickAction('为当前页面内容生成2道选择题')">为当前页面内容生成2道选择题</button>
+                <button class="quick-action-btn" @click="sendQuickAction('为当前页面生成2页内容页面')">为当前页面生成2页内容页面</button>
+            </div>
+        </div>
+    </div>
+</template>
+
+<script lang="ts" setup>
+import { ref, onMounted, useTemplateRef, nextTick } from 'vue'
+import { chat_no_stream, chat_stream, getAgentModel } from '@/tools/aiChat'
+import MarkdownIt from 'markdown-it'
+
+interface ChatMessage {
+    uid?: string
+    role: 'ai' | 'user'
+    content?: string
+    aiContent?: string
+    oldContent?: string
+    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?: {
+        isChoice?: boolean
+        headUrl?: string
+        assistantName?: string
+        sourceArray?: Array<{
+            text?: string
+            id?: string
+            title?: string
+        }>
+    }
+}
+
+const props = withDefaults(defineProps<{
+    userid?: string | null
+}>(), {
+  userid: null,
+})
+
+
+
+const inputText = ref('')
+const messages = ref<ChatMessage[]>([])
+const chatSection = useTemplateRef<HTMLElement>('chatSection')
+const chatLoading = ref(false)
+
+const sendMessage = () => {
+  if (inputText.value.trim()) {
+    // 添加用户消息
+    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
+    chatLoading.value = true
+    sendAction(inputText.value)
+    inputText.value = ''
+  }
+}
+
+const prevChatResult = () => {
+  nextTick(() => {
+    if (chatSection.value) {
+      chatSection.value.scrollTop = chatSection.value.scrollHeight
+    }
+  })
+}
+
+const sendQuickAction = (action: string) => {
+  // 添加用户消息
+  messages.value.push({
+    role: 'user',
+    content: action
+  })
+
+  // 模拟AI回复
+  //   setTimeout(() => {
+  //     messages.value.at(-1).aiContent = '请问您想聚焦哪一重点?'
+  //   }, 500)
+  sendAction(action)
+}
+const sendAction = async (action: string) => {
+  const content = await chat_no_stream(action, agentid1.value, props.userid || '', 'zh-CN')
+  console.log(content)
+  // 渲染 Markdown 格式
+  const md = new MarkdownIt()
+  const html = md.render(content)
+  messages.value.at(-1).aiContent = html
+
+  chat_stream(action, agentid1.value, props.userid || '', 'zh-CN', (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)
+      chatLoading.value = false
+      prevChatResult()
+    }
+  }).catch(err => {
+    chatLoading.value = false
+    console.log('err', err)
+  })
+}
+const agentid1 = ref('4535eb8a-851b-4c47-a059-234f702d89c4')
+const agentid2 = ref('6c5b2386-f305-4062-bf53-125c0058fafa')
+
+
+onMounted(() => {
+  getAgentModel(agentid1.value)
+  getAgentModel(agentid2.value)
+})
+</script>
+
+<style lang="scss" scoped>
+.ai-chat-container {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    padding: 16px;
+    gap: 16px;
+}
+
+.input-section {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+}
+
+.input-wrapper {
+    position: relative;
+    display: flex;
+    flex-direction: column;
+    background: #fafbfc;
+    border: 1.5px solid #e5e7eb;
+    border-radius: 8px;
+    padding: 8px 12px;
+    min-height: 120px;
+}
+
+.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: 8px;
+}
+
+.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;
+    }
+}
+
+.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;
+    }
+}
+
+.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-right: 8px;
+
+    &::-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: #fafbfc;
+        border: 1.5px solid #e5e7eb;
+        border-bottom-left-radius: 2px;
+    }
+}
+
+.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;
+    }
+}
+
+ul {
+    margin: 8px 0;
+    padding-left: 20px;
+
+    li {
+        margin: 4px 0;
+    }
+}
+</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;
+}
+</style>

+ 40 - 2
src/components/CollapsibleToolbar/index2.vue

@@ -2,6 +2,14 @@
   <div class="collapsible-toolbar" :class="{ collapsed: isCollapsed }">
     <div class="toolbar-content" v-show="!isCollapsed">
       <div class="sidebar-content">
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'cocoai' }" @click="toggleSubmenu('cocoai')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 2L2 7l10 5 10-5-10-5z"></path>
+            <path d="M2 17l10 5 10-5"></path>
+            <path d="M2 12l10 5 10-5"></path>
+          </svg>
+          <span class="item-label">Coco AI</span>
+        </div>
         <div class="sidebar-item" :class="{ active: activeSubmenu === 'page' }" @click="toggleSubmenu('page')">
           <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
             <g id="Component 1">
@@ -72,6 +80,23 @@
         </div>
       </div>
     </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'cocoai' }">
+      <div class="submenu-title" style="margin-bottom: 0;">
+        <div class="title">Coco AI</div>
+        <div class="close-icon" @click="toggleSubmenu('cocoai')">
+          <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <g id="Component 3">
+              <g id="Component 1">
+                <path id="Vector" d="M16 18L12 14L16 10" stroke="#9CA3AF" stroke-width="1.33333" />
+              </g>
+            </g>
+          </svg>
+        </div>
+      </div>
+      <div class="submenu-content">
+        <AiChat :userid="props.userid" />
+      </div>
+    </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'page' }">
       <div class="submenu-title">
         <div class="title">{{ lang.ssAddTemplatePage }}</div>
@@ -495,6 +520,7 @@ import useCreateElement from '@/hooks/useCreateElement'
 import useSlideHandler from '@/hooks/useSlideHandler'
 import { useSlidesStore } from '@/store'
 import FileInput from '@/components/FileInput.vue'
+import AiChat from './componets/aiChat.vue'
 import { lang } from '@/main'
 import toolChoice from '@/assets/img/tool_choice.jpeg'
 import toolAnswer from '@/assets/img/tool_answer.png'
@@ -506,10 +532,14 @@ interface ContentItem {
   id?: string
 }
 
+
+
 const props = withDefaults(defineProps<{
   defaultCollapsed?: boolean
+  userid?: string | null
 }>(), {
-  defaultCollapsed: false
+  defaultCollapsed: false,
+  userid: null,
 })
 
 const emit = defineEmits<{
@@ -1063,8 +1093,8 @@ const handleParsingClose = () => {
   display: flex;
   justify-content: space-between;
   align-items: center;
-  margin-bottom: 20px;
   border-bottom: 1px solid #f0f0f0;
+  margin-bottom: 20px;
   width: 100%;
   box-sizing: border-box;
   padding: 12px 15px;
@@ -1102,6 +1132,14 @@ const handleParsingClose = () => {
   }
 }
 
+.submenu-content {
+  width: 100%;
+  height: calc(100% - 50px);
+  box-sizing: border-box;
+  overflow: hidden;
+}
+
+
 .submenu-img {
   width: calc(100% - 30px);
   margin: 0 auto;

+ 27 - 1
src/hooks/useSlideHandler.ts

@@ -16,7 +16,7 @@ export default () => {
   const mainStore = useMainStore()
   const slidesStore = useSlidesStore()
   const { selectedSlidesIndex: _selectedSlidesIndex, activeElementIdList } = storeToRefs(mainStore)
-  const { currentSlide, slides, theme, slideIndex } = storeToRefs(slidesStore)
+  const { currentSlide, slides, theme, slideIndex, viewportSize, viewportRatio } = storeToRefs(slidesStore)
 
   const selectedSlidesIndex = computed(() => [..._selectedSlidesIndex.value, slideIndex.value])
   const selectedSlides = computed(() => slides.value.filter((item, index) => selectedSlidesIndex.value.includes(index)))
@@ -93,9 +93,35 @@ export default () => {
   // 根据模板创建新页面
   const createSlideByTemplate = (slide: Slide) => {
     const { groupIdMap, elIdMap } = createElementIdMap(slide.elements)
+    const slideWidth = viewportSize.value
+    const slideHeight = viewportSize.value * viewportRatio.value
+    
+    // 模板原始宽高(16:9比例)
+    const templateWidth = 1280
+    const templateHeight = 720
+    
+    // 计算缩放因子
+    const scaleX = slideWidth / templateWidth
+    const scaleY = slideHeight / templateHeight
 
     for (const element of slide.elements) {
       element.id = elIdMap[element.id]
+      element.top = (element.top || 0) * scaleY
+      element.left = (element.left || 0) * scaleX
+      element.width = (element.width || 0) * scaleX
+      if (element.type === 'text' || element.type === 'shape') {
+        element.height = (element.height || 0) * scaleY
+      }
+      
+      // 处理 content 中的字体大小
+      if (element.type === 'text') {
+        // 匹配 style 中的 font-size 属性
+        element.content = element.content.replace(/font-size:\s*(\d+)px/g, (match, fontSize) => {
+          const newFontSize = Math.round(parseInt(fontSize) * scaleY)
+          return `font-size: ${newFontSize}px`
+        })
+      }
+      
       if (element.groupId) element.groupId = groupIdMap[element.groupId]
     }
     const newSlide = {

+ 2 - 0
src/plugins/icon.ts

@@ -98,6 +98,7 @@ import {
   Power,
   ListView,
   Magic,
+  Tips,
   HighLight,
   Download,
   IndentLeft,
@@ -232,6 +233,7 @@ export const icons: Icons = {
   IconPower: Power,
   IconListView: ListView,
   IconMagic: Magic,
+  IconTips: Tips,
   IconHighLight: HighLight,
   IconDownload: Download,
   IconIndentLeft: IndentLeft,

+ 151 - 0
src/tools/aiChat.ts

@@ -0,0 +1,151 @@
+import axios from '@/services/config'
+import { v4 as uuidv4 } from 'uuid'
+import { fetchEventSource } from '@microsoft/fetch-event-source'
+
+const model = {}
+
+interface ChatParams {
+  id: string;
+  message: string;
+  userId: string;
+  model: string;
+  file_ids: string[];
+  sound_url: string;
+  temperature: number;
+  top_p: number;
+  max_completion_tokens: number;
+  stream: boolean;
+  uid: string;
+  session_name: string;
+  tts_language: string;
+}
+
+const DEFAULT_PARAMS: Omit<ChatParams, 'message' | 'uid' | 'stream'> = {
+  id: 'a7741704-ba56-40b7-a6b8-62a423ef9376',
+  userId: '6c56ec0e-2c74-11ef-bee5-005056b86db5',
+  model: 'open-doubao',
+  file_ids: [],
+  sound_url: '',
+  temperature: 0.2,
+  top_p: 1,
+  max_completion_tokens: 4096,
+  session_name: 'pptSession_name',
+  tts_language: 'zh-CN'
+}
+
+export const chat_no_stream = async (msg: string, agentId: string, userId: string, language: string): Promise<string> => {
+  const agentData = await getAgentModel(agentId)
+  const params: ChatParams = {
+    ...DEFAULT_PARAMS,
+    id: agentId,
+    message: msg,
+    uid: uuidv4(),
+    stream: false,
+    model: agentData?.modelType || 'open-doubao',
+    userId: userId,
+    tts_language: language,
+    session_name: uuidv4()
+  }
+
+  const res = await axios.post('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', params)
+  let content = res?.message || ''
+  console.log(content)
+  
+  // 清理可能的 markdown 格式
+  if (content.includes('```json')) {
+    // 提取 ```json 和 ``` 之间的内容
+    const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/)
+    if (jsonMatch) {
+      content = jsonMatch[1].trim()
+    }
+  }
+  else if (content.includes('```')) {
+    // 提取 ``` 和 ``` 之间的内容
+    const codeMatch = content.match(/```\s*([\s\S]*?)\s*```/)
+    if (codeMatch) {
+      content = codeMatch[1].trim()
+    }
+  }
+
+  return content
+
+}
+
+export const chat_stream = async (
+  msg: string,
+  agentId: string,
+  userId: string,
+  language: string,
+  onMessage: (event: { type: 'message' | 'close' | 'error' | 'messageEnd'; data: string }) => void
+): Promise<void> => {
+  const agentData = await getAgentModel(agentId)
+  const params: ChatParams = {
+    ...DEFAULT_PARAMS,
+    id: agentId,
+    message: msg,
+    uid: uuidv4(),
+    stream: true,
+    model: agentData?.modelType || 'open-doubao',
+    userId: userId,
+    tts_language: language,
+    session_name: uuidv4()
+  }
+
+  const ctrl = new AbortController()
+  let content = ''
+  try {
+    await fetchEventSource('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', {
+      method: 'POST',
+      body: JSON.stringify(params),
+      signal: ctrl.signal,
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      onmessage(event) {
+        const data = JSON.parse(event.data)
+        if (data.content) {
+          if (data.content != '[DONE]') {
+            content += data.content
+            onMessage({
+              type: 'message',
+              data: content
+            })
+          }
+          else {
+            onMessage({
+              type: 'messageEnd',
+              data: content
+            })
+          }
+
+        }
+      },
+      onclose() {
+        onMessage({
+          type: 'close',
+          data: 'SSE Connection closed'
+        })
+      },
+      onerror(err) {
+        onMessage({
+          type: 'error',
+          data: err.message || 'Unknown error'
+        })
+        // 返回 undefined 以阻止自动重连,如需重连则删除此行
+        throw err
+      },
+    })
+  }
+  finally {
+    ctrl.abort()
+  }
+}
+
+export const getAgentModel = async (agentId: string) => {
+  if (model[agentId]) {
+    return model[agentId]
+  }
+  const res = await axios.get(`https://appapi.cocorobo.cn/api/agents/agent/${agentId}`)
+  model[agentId] = res
+  return model[agentId]
+}

+ 3 - 1
src/views/Editor/index3.vue

@@ -142,7 +142,7 @@
     </div>
     <!-- <EditorHeader class="layout-header" /> -->
     <div class="layout-content">
-      <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" />
+      <CollapsibleToolbar class="layout-sidebar" @toggle="handleToolbarToggle" :userid="props.userid" />
       <div class="layout-content-center">
         <CanvasTool class="center-top" />
         <Canvas class="center-body" :style="{ height: `calc(100% - ${remarkHeight + 60}px  - 120px)` }"
@@ -217,10 +217,12 @@ const parentWindow = window.parent as ParentWindowWithToolList
 
 interface Props {
   courseid?: string | null
+  userid?: string | null
 }
 
 const props = withDefaults(defineProps<Props>(), {
   courseid: null,
+  userid: null,
 })
 
 const mainStore = useMainStore()

+ 330 - 49
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -4,7 +4,7 @@
       width: slideWidth + 'px',
       height: slideHeight + 'px',
     }">
-      <span class="closeIcon" @click="closeSlideIndex()">
+      <span v-show="false" class="closeIcon" @click="closeSlideIndex()">
         <img src="../../../assets/img/close.png" />
       </span>
 
@@ -43,18 +43,22 @@
         }">
           <div id="echartsArea1" ref="echartsArea1"></div>
         </div>
-        <!-- <div class="c_t45_aiAnalysis">
-          <div class="c_t45_aa_header">
-            <div class="c_t45_aa_h_title">
-              <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 170.666667C323.477333 170.666667 170.666667 323.477333 170.666667 512s152.810667 341.333333 341.333333 341.333333 341.333333-152.810667 341.333333-341.333333S700.522667 170.666667 512 170.666667zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z"></path><path d="M693.013333 330.986667a42.666667 42.666667 0 0 1 10.304 43.648l-75.413333 226.282666a42.666667 42.666667 0 0 1-26.986667 26.986667l-226.282666 75.413333a42.666667 42.666667 0 0 1-53.973334-53.973333l75.434667-226.261333a42.666667 42.666667 0 0 1 26.986667-26.986667l226.282666-75.413333a42.666667 42.666667 0 0 1 43.648 10.304z m-222.72 139.306666l-41.685333 125.098667 125.077333-41.706667 41.706667-125.077333-125.077333 41.706667z"></path></svg>分析
+        <div class="aiAnalysis">
+          <div class="ai_header">
+            <div class="ai_title">
+              <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 170.666667C323.477333 170.666667 170.666667 323.477333 170.666667 512s152.810667 341.333333 341.333333 341.333333 341.333333-152.810667 341.333333-341.333333S700.522667 170.666667 512 170.666667zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z"></path><path d="M693.013333 330.986667a42.666667 42.666667 0 0 1 10.304 43.648l-75.413333 226.282666a42.666667 42.666667 0 0 1-26.986667 26.986667l-226.282666 75.413333a42.666667 42.666667 0 0 1-53.973334-53.973333l75.434667-226.261333a42.666667 42.666667 0 0 1 26.986667-26.986667l226.282666-75.413333a42.666667 42.666667 0 0 1 43.648 10.304z m-222.72 139.306666l-41.685333 125.098667 125.077333-41.706667 41.706667-125.077333-125.077333 41.706667z"></path></svg>{{ lang.ssAnalysis }}
             </div>
-            <div class="c_t45_aa_h_refresh">
-              AI生成
+            <div class="ai_refresh" :class="{'disabled': currentAnalysis && currentAnalysis.loading}" @click="aiAnalysisRefresh45()">
+              {{ lang.ssAIGenerate }}
              <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M875 483c-33.4 0-60.5 27.1-60.5 60.5v0.1C814.4 710.3 678.8 846 512 846S209.5 710.3 209.5 543.5 345.2 241 512 241c36.8 0 71.7 7.6 104.4 19.7-32 3-57.4 29.1-57.4 61.9 0 34.8 28.2 63 63 63h201.9c34.8 0 63-28.2 63-63V120c0-34.8-28.2-63-63-63s-63 28.2-63 63v81.4C691 150.5 605.2 120 512 120 278.1 120 88.5 309.6 88.5 543.5S278.1 967 512 967s423.5-189.6 423.5-423.5c0-33.4-27.1-60.5-60.5-60.5z"></path></svg>
 
             </div>
           </div>
-        </div> -->
+          <div class="ai_content" v-if="currentAnalysis">
+            {{ currentAnalysis.json }}
+          </div>
+          <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}</div>
+        </div>
         <div class="cq_changeBtn" v-if="props.showData.choiceQuestionListData.length > 1">
           <div :class="{ cq_cb_disabled: props.showData.workIndex <= 0 }" @click="changeWorkIndex(0)">
             <svg style="transform: rotate(-90deg);" viewBox="0 0 1024 1024" version="1.1" width="200" height="200">
@@ -91,6 +95,24 @@
           </div>
         </div>
 
+       <div class="aiAnalysis" style="margin-top:1rem ;">
+          <div class="ai_header">
+            <div class="ai_title">
+              <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M512 170.666667C323.477333 170.666667 170.666667 323.477333 170.666667 512s152.810667 341.333333 341.333333 341.333333 341.333333-152.810667 341.333333-341.333333S700.522667 170.666667 512 170.666667zM85.333333 512C85.333333 276.352 276.352 85.333333 512 85.333333s426.666667 191.018667 426.666667 426.666667-191.018667 426.666667-426.666667 426.666667S85.333333 747.648 85.333333 512z"></path><path d="M693.013333 330.986667a42.666667 42.666667 0 0 1 10.304 43.648l-75.413333 226.282666a42.666667 42.666667 0 0 1-26.986667 26.986667l-226.282666 75.413333a42.666667 42.666667 0 0 1-53.973334-53.973333l75.434667-226.261333a42.666667 42.666667 0 0 1 26.986667-26.986667l226.282666-75.413333a42.666667 42.666667 0 0 1 43.648 10.304z m-222.72 139.306666l-41.685333 125.098667 125.077333-41.706667 41.706667-125.077333-125.077333 41.706667z"></path></svg>{{ lang.ssAnalysis }}
+            </div>
+            <div class="ai_refresh" :class="{'disabled': currentAnalysis && currentAnalysis.loading}" @click="aiAnalysisRefresh15()">
+              {{ lang.ssAIGenerate }}
+             <svg viewBox="0 0 1024 1024" width="200" height="200"><path d="M875 483c-33.4 0-60.5 27.1-60.5 60.5v0.1C814.4 710.3 678.8 846 512 846S209.5 710.3 209.5 543.5 345.2 241 512 241c36.8 0 71.7 7.6 104.4 19.7-32 3-57.4 29.1-57.4 61.9 0 34.8 28.2 63 63 63h201.9c34.8 0 63-28.2 63-63V120c0-34.8-28.2-63-63-63s-63 28.2-63 63v81.4C691 150.5 605.2 120 512 120 278.1 120 88.5 309.6 88.5 543.5S278.1 967 512 967s423.5-189.6 423.5-423.5c0-33.4-27.1-60.5-60.5-60.5z"></path></svg>
+
+            </div>
+          </div>
+          <div class="ai_content" v-if="currentAnalysis">
+            {{ currentAnalysis.json }}
+          </div>
+          <div class="ai_updateTime" v-if="currentAnalysis">{{ lang.ssUpdateTime }}:{{ currentAnalysis.update_at }}</div>
+        </div>
+
+
         <div class="c_t15_workDetail" v-if="lookWorkData">
           <div class="c_t15_wd_top">
             <img src="../../../assets/img/arrow_left.png" @click="lookWork('')" />
@@ -201,13 +223,15 @@
 </template>
 
 <script setup lang="ts">
-import { computed, ref, watch, onUnmounted, nextTick } from 'vue'
+import { computed, ref, watch, onUnmounted, nextTick, onMounted } from 'vue'
 import * as echarts from 'echarts'
 import previewImageTool from '../../components/tool/previewImageTool.vue'
 import MarkdownIt from 'markdown-it'
 import useImport from '@/hooks/useImport'
 import { lang } from '@/main'
 import selectUserDialog from './selectUserDialog.vue'
+import { chat_stream } from '@/tools/aiChat'
+import axios from '@/services/config'
 const props = defineProps<{
   visible: number[];
   workIndex: number;
@@ -216,6 +240,8 @@ const props = defineProps<{
   slideIndex: number;
   showData: any;
   workArray: any[];
+  courseDetail: any;
+  userId: string;
 }>()
 
 const emit = defineEmits<{
@@ -240,6 +266,9 @@ const previewImageToolRef = ref<any>(null)
 // 选择用户组件
 const selectUserDialogRef = ref<any>(null)
 
+// ai分析数据
+const aiAnalysisData = ref<Array<any>>([])
+
 const md = new MarkdownIt()
 const { getFile } = useImport()
 
@@ -455,6 +484,7 @@ const setEchartsArea1 = () => {
   if (myChart.value) {
     const _work =
       props.showData.choiceQuestionListData[props.showData.workIndex]
+    console.log('_work', _work)
     // 修正版,处理xAxis.data内为图片对象的case,formatter始终只拿到src或自定义label,保证无[object Object]问题
     const option = {
       tooltip: {
@@ -471,7 +501,7 @@ const setEchartsArea1 = () => {
           fontSize: 17,
           lineHeight: 20,
           interval: 0,
-          formatter: function (value: any, idx: number) {
+          formatter: function(value: any, idx: number) {
             // 如果是字符串且格式为JSON(图片),则解析处理
             if (typeof value === 'string') {
               try {
@@ -580,7 +610,7 @@ const setEchartsArea1 = () => {
       (option.series[0].data as any[]).push(i.user.length)
     })
 
-    console.log(option)
+    // console.log(option)
     // {
     //   title: {
     //     text: 'ECharts 入门示例'
@@ -606,13 +636,34 @@ const setEchartsArea1 = () => {
       const selectedOption = _work.choiceUser[idx]
       if (selectedOption && selectUserDialogRef.value) {
         // console.log(selectedOption)
-        console.log("selectedOption",selectedOption)
-        selectUserDialogRef.value.open(`${lang.ssSelectUser.replace("{a}","<span>"+selectedOption.index+"</span>")}`,selectedOption)
+        console.log('selectedOption', selectedOption)
+        selectUserDialogRef.value.open(`${lang.ssSelectUser.replace('{a}', '<span>' + selectedOption.index + '</span>')}`, selectedOption)
       }
     })
   }
 }
 
+// 获取分析
+const getAnalysis = () => {
+  if (!props.showData.workDetail.id) {
+    return
+  }
+  const params = {
+    pid: props.showData.workDetail.id,
+  }
+  axios.get('https://pbl.cocorobo.cn/api/pbl/select_pptAnalysisByPid?pid=' + params.pid).then(res => {
+    const data = res[0]
+    if (data.length) {
+      aiAnalysisData.value = data
+    }
+    else {
+      aiAnalysisData.value = []
+    }
+  }).catch(err => {
+    console.log('get_pptAnalysis_err', err)
+  })
+}
+
 // 监听选择题数据变化
 watch(
   () => props.showData,
@@ -639,6 +690,7 @@ watch(
     else {
       myChart.value = null
     }
+    getAnalysis()
   },
   { immediate: true, deep: true }
 )
@@ -719,12 +771,207 @@ watch(
 
 // 查看未提交学生
 const viewUnsubmittedStudents = () => {
-  selectUserDialogRef.value.open(lang.ssUnsubmittedStudents,{user:props.showData.unsubmittedStudents.map((item: any) => item.name)})
+  selectUserDialogRef.value.open(lang.ssUnsubmittedStudents, {user: props.showData.unsubmittedStudents.map((item: any) => item.name)})
   // if (props.unsubmittedStudents.length > 0) {
-    // unsubmittedStudentsDialogRef.value.open(props.unsubmittedStudents)
+  // unsubmittedStudentsDialogRef.value.open(props.unsubmittedStudents)
   // }
 }
 
+// ai生成选择题分析 选择题
+const aiAnalysisRefresh45 = () => {
+  const _work = props.showData.choiceQuestionListData[props.showData.workIndex]
+  const msg = `# CONTEXT #
+你是K-12阶段的AI教育课堂分析助手,基于上传的课件、逐字稿,以及当页的学生答题数据(选择题/问答题/智能体对话)进行智能分析。
+
+# OBJECTIVE #
+输出当前学生答题的分析报告:整体表现、核心发现(共性问题/误区)、教学优化(改进方向),为教师提供教学策略的建议和支持。
+
+#INPUT#
+课程数据:
+- 课程名称:${props.courseDetail.title}
+- 课程学科:${props.courseDetail.name}
+当前页面答题数据(选择题):【分析重点】
+- 选择题题目:${_work.teststitle}
+- 选项数据:${JSON.stringify(_work.choiceUser)}
+- 题目图片:${_work.timuList.lenght ? _work.timuList[0].src : ''}
+- 未提交学生:${JSON.stringify(props.showData.unsubmittedStudents.map((item: any) => item.name))}
+
+# ANALYSIS RULES #
+1. **问题定位**:明确指出哪个知识点/概念/步骤掌握不佳
+2. **建议具体**:给出可执行的教学动作(如“增加XX实例演练”“补充XX对比图”)
+
+# RESPONSE #
+采用段落叙述,数据量化呈现,突出可操作建议,严格控制输出为80词内。采用三段式论述:
+1. 整体表现-1句话
+2. 核心发现-1-2句,指出共性问题+典型案例
+3. 改进建议-1句话,提出具体教学动作
+
+# EXAMPLES #
+样例:
+选择题正确率62%,核心概念“机器学习三步骤”(输入数据→训练模型→预测结果)掌握尚可,但“训练与预测区分”混淆率达38%;建议强化训练vs预测对比教学。`
+
+  if (!currentAnalysis.value) {
+    aiAnalysisData.value.push({
+      pid: props.showData.workDetail.id,
+      index: props.showData.workIndex,
+      loading: true,
+      json: '',
+      noEnd: true,
+      update_at: '',
+      create_at: '',
+    })
+  }
+  else {
+    aiAnalysisData.value.find((item:any) => {
+      return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+    }).loading = true
+    aiAnalysisData.value.find((item:any) => {
+      return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+    }).json = ''
+  }
+
+  chat_stream(msg, 'a7741704-ba56-40b7-a6b8-62a423ef9376', props.userId, 'zh-CN', (event) => {
+    if (event.type === 'message') { 
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).json = event.data
+
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).loading = false
+    }
+    else if (event.type === 'messageEnd') {
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).json = event.data
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).noEnd = false
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
+      saveAnalysis()
+    }
+  }).catch(err => {
+    console.log('err', err)
+  })
+}
+
+// 问答题
+const aiAnalysisRefresh15 = () => {
+  const msg = `# CONTEXT #
+你是K-12阶段的AI教育课堂分析助手,基于上传的课件、逐字稿,以及当页的学生答题数据(选择题/问答题/智能体对话)进行智能分析。
+
+# OBJECTIVE #
+输出当前学生答题的分析报告:整体表现、核心发现(共性问题/误区)、教学优化(改进方向),为教师提供教学策略的建议和支持。
+
+#INPUT#
+课程数据:
+- 课程名称:${props.courseDetail.title}
+- 课程学科:${props.courseDetail.name}
+当前页面答题数据(问答题):【分析重点】
+- 问答题题目:${props.showData.workDetail.json.answerQ}
+- 回答数据:${JSON.stringify(processedWorkArray.value.map((i) => ({user: i.name, answer: i.content.answer})))}
+- 未提交学生:${JSON.stringify(props.showData.unsubmittedStudents.map((item: any) => item.name))}
+
+# ANALYSIS RULES #
+1. **问题定位**:明确指出哪个知识点/概念/步骤掌握不佳
+2. **建议具体**:给出可执行的教学动作(如“增加XX实例演练”“补充XX对比图”)
+
+# RESPONSE #
+采用段落叙述,数据量化呈现,突出可操作建议,严格控制输出为80词内。采用三段式论述:
+1. 整体表现-1句话
+2. 核心发现-1-2句,指出共性问题+典型案例
+3. 改进建议-1句话,提出具体教学动作
+
+# EXAMPLES #
+样例:
+选择题正确率62%,核心概念“机器学习三步骤”(输入数据→训练模型→预测结果)掌握尚可,但“训练与预测区分”混淆率达38%;建议强化训练vs预测对比教学。`
+  if (!currentAnalysis.value) {
+    aiAnalysisData.value.push({
+      pid: props.showData.workDetail.id,
+      index: props.showData.workIndex,
+      loading: true,
+      json: '',
+      noEnd: true,
+      update_at: '',
+      create_at: '',
+    })
+  }
+  else {
+    aiAnalysisData.value.find((item:any) => {
+      return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+    }).loading = true
+    aiAnalysisData.value.find((item:any) => {
+      return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+    }).json = ''
+  }
+
+  chat_stream(msg, 'a7741704-ba56-40b7-a6b8-62a423ef9376', props.userId, 'zh-CN', (event) => {
+    if (event.type === 'message') { 
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).json = event.data
+
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).loading = false
+    }
+    else if (event.type === 'messageEnd') {
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).json = event.data
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).noEnd = false
+      aiAnalysisData.value.find((item:any) => {
+        return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+      }).update_at = new Date().toLocaleString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).replace(/\//g, '-')
+      saveAnalysis()
+    }
+  }).catch(err => {
+    console.log('err', err)
+  })
+}
+
+
+// 当前分析
+const currentAnalysis = computed(() => {
+  return aiAnalysisData.value.find((item:any) => {
+    return item.pid === props.showData.workDetail.id && item.index === props.showData.workIndex
+  })
+})
+
+// 保存分析
+const saveAnalysis = () => {
+  if (!currentAnalysis.value) {
+    return
+  }
+  const params = [{
+    pid: props.showData.workDetail.id,
+    idx: props.showData.workIndex,
+    json: currentAnalysis.value.json,
+  }]
+
+  axios.post('https://pbl.cocorobo.cn/api/pbl/insert_pptAnalysis', params).then(res => {
+    if (res == 1) {
+      console.log('保存成功')
+    }
+  }).catch(err => {
+    console.log('insert_pptAnalysis_err', err)
+  })
+}
+
+
+
+// watch(()=>props.slideIndex,()=>{
+//   getAnalysis()
+// })
+
+// onMounted(()=>{
+//   getAnalysis()
+// })
+
 // 组件卸载时清理ECharts实例
 onUnmounted(() => {
   // 清除定时器
@@ -897,40 +1144,7 @@ onUnmounted(() => {
         }
       }
 
-      .c_t45_aiAnalysis{
-        display: flex;
-        flex-direction: column;
-        padding: 1rem;
-        border: solid 1px #F6C82B;
-        border-left-width: 4px;
-        border-radius: 1rem;
-        &>.c_t45_aa_header{
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          gap: 1rem;
-          &>div{
-            display: flex;
-            align-items: center;
-            gap: .5rem;
-            &>svg{
-              width: 1rem;
-              height: 1rem;
-              
-            }
-          }
-          &>.c_t45_aa_h_title{
-            color: #F7CD49;
-            font-weight: 500;
-            &>svg{
-              fill: #F7CD49;
-            }
-          }
-          // &>.c_t45_aa_h_refresh{
-            
-          // }
-        }
-      }
+
     }
 
     .c_t15 {
@@ -1458,4 +1672,71 @@ onUnmounted(() => {
     }
   }
 }
+
+      .aiAnalysis{
+        width: 100%;
+        height: auto;
+        display: flex;
+        flex-direction: column;
+        padding: 1rem;
+        border: solid 1px #F6C82B;
+        border-left-width: 4px;
+        border-radius: 1rem;
+        gap: 1rem;
+        &>.ai_header{
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          gap: 1rem;
+          &>div{
+            display: flex;
+            align-items: center;
+            gap: .5rem;
+            &>svg{
+              width: 1rem;
+              height: 1rem;
+              
+            }
+          }
+          &>.ai_title{
+            color: #F7CD49;
+            font-weight: 500;
+            &>svg{
+              fill: #F7CD49;
+              width: 1.2rem;
+              height: 1.2rem;
+            }
+          }
+          &>.ai_refresh{
+            padding: .5rem 1rem;
+            border-radius: .5rem;
+            background: #F6C82B;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            cursor: pointer;
+            color: #000;
+            font-weight: bold;
+            font-size: .8rem;
+            gap: .5rem;
+            &>svg{
+              fill: #000;
+              width: .8rem;
+              height: .8rem;
+            }
+            &.disabled{
+              cursor: not-allowed !important;
+              background: #FEF8E9 !important;
+            }
+          }
+        }
+        &>.ai_content{
+          font-size: 1rem;
+          font-weight: 500;
+        }
+        &>.ai_updateTime{
+          font-size: .8rem;
+           font-weight: 500;
+        }
+      }
 </style>

+ 69 - 7
src/views/Student/index.vue

@@ -78,6 +78,16 @@
           left: isFullscreen ? '0' : `${(containerWidth - slideWidth * canvasScale) / 2}px`,
           top: isFullscreen ? '0' : `${(containerHeight - slideHeight * canvasScale) / 2}px`
         }" @mousemove="handleLaserMove">
+          <div class="homework-check-box" v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo" v-show="currentSlideHasIframe" :style="{
+            top: isFullscreen ? '0' : `15px`
+          }">
+            <div class="homework-check-box-item" @click="openChoiceQuestionDetail2(slideIndex)" :class="{'active': !choiceQuestionDetailDialogOpenList.includes(slideIndex)}">
+              <div class="homework-check-box-item-title">{{ lang.ssQuestion }}</div>
+            </div>
+            <div class="homework-check-box-item" @click="openChoiceQuestionDetail3(slideIndex)" :class="{'active': choiceQuestionDetailDialogOpenList.includes(slideIndex)}">
+              <div class="homework-check-box-item-title">{{ lang.ssAnswer }}</div>
+            </div>
+          </div>
           <div class="viewport" v-if="false">
             <div class="background" :style="backgroundStyle"></div>
 
@@ -89,7 +99,7 @@
           <ScreenSlideList :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"
             :animationIndex="0" :turnSlideToId="() => { }" :manualExitFullscreen="() => { }"  :slideIndex="slideIndex" v-show="!choiceQuestionDetailDialogOpenList.includes(slideIndex)"/>
 
-          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
+          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex)"  :userId="props.userid" :courseDetail="courseDetail" :workArray="workArray" @changeWorkIndex="changeWorkIndex" v-model:visible="choiceQuestionDetailDialogOpenList" :showData="answerTheResultRef" :slideIndex="slideIndex" :workIndex="0" :style="{ width: isFullscreen ? '100%' : slideWidth2 * canvasScale + 'px', height: isFullscreen ? '100%' : slideHeight2 * canvasScale + 'px', margin: '0 auto' }" :slideWidth="isFullscreen ? slideWidth * canvasScale : slideWidth2 * canvasScale" :slideHeight="isFullscreen ? slideHeight * canvasScale : slideHeight2 * canvasScale"/>
 
 
           <div class="slide-bottom" v-if="!isFullscreen">
@@ -110,7 +120,8 @@
                 <IconLoading v-else-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo" class="tool-btn loading" v-tooltip="lang.ssSubmitting"></IconLoading>
                 <IconStopwatchStart v-if="props.type == '1' && courseDetail.userid == props.userid && isFollowModeActive" class="tool-btn" v-tooltip="lang.ssTimer" @click="timerlVisible = !timerlVisible"  />
                 <IconWrite v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="lang.ssPenTool" @click="writingBoardToolVisible = true"  />
-                <IconMagic v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="lang.ssLaserPen" :class="{ 'active': laserPen }" @click="toggleLaserPen"  />
+                <!-- <IconMagic v-if="isFollowModeActive && props.type == '1' && courseDetail.userid == props.userid" class="tool-btn" v-tooltip="lang.ssLaserPen" :class="{ 'active': laserPen }" @click="toggleLaserPen"  /> -->
+                <IconTips v-if="props.type == '1'" class="tool-btn" v-tooltip="lang.ssAiHelper" :class="{ 'active': !workPanelCollapsed }" @click="workPanelCollapsed = !workPanelCollapsed"  />
                 <IconFullScreenOne class="tool-btn" v-tooltip="lang.ssOpenFull" @click="enterFullscreen" />
               </div>
           </div>
@@ -194,7 +205,7 @@
           </button>
           <!-- 标签页切换按钮 -->
           <div v-show="!workPanelCollapsed" class="tab-switcher">
-            <button 
+            <!-- <button 
               v-if="currentSlideHasIframe && !currentSlideHasBilibiliVideo"
               v-show="currentSlideHasIframe"
               class="tab-btn" 
@@ -203,7 +214,7 @@
               :title="lang.ssAnswerRes"
             >
               {{ lang.ssAnswerRes }}
-            </button>
+            </button> -->
             <button 
               class="tab-btn" 
               :class="{ active: rightPanelMode === 'dialogue' }"
@@ -649,9 +660,9 @@ const autoSwitchToAvailablePanel = () => {
     rightPanelMode.value = 'dialogue'
     console.log('自动切换到对话面板')
   }
-  else if (currentSlideHasIframe.value && rightPanelMode.value !== 'homework' && !currentSlideHasBilibiliVideo.value) {
-    rightPanelMode.value = 'homework'
-  }
+  // else if (currentSlideHasIframe.value && rightPanelMode.value !== 'homework' && !currentSlideHasBilibiliVideo.value) {
+  //   rightPanelMode.value = 'homework'
+  // }
 }
 
 // 移除定时器相关函数,改用socket监听
@@ -3216,6 +3227,24 @@ const openChoiceQuestionDetail = (index:number) => {
   }
 }
 
+// 打开作业查看详细
+const openChoiceQuestionDetail2 = (index:number) => {
+  if (!choiceQuestionDetailDialogOpenList.value.includes(index)) {
+  }
+  else {
+    choiceQuestionDetailDialogOpenList.value = choiceQuestionDetailDialogOpenList.value.filter(i => i !== index)
+  }
+}
+
+
+// 打开作业查看详细
+const openChoiceQuestionDetail3 = (index:number) => {
+  if (!choiceQuestionDetailDialogOpenList.value.includes(index)) {
+    choiceQuestionDetailDialogOpenList.value.push(index)
+  }
+}
+
+
 const handlePageUnload = () => {
   if (isCreator.value && timerIndicator.value.visible && props.type === '1') {
     sendMessage({ type: 'timer_stop', courseid: props.courseid })
@@ -5259,4 +5288,37 @@ const clearTimerState = () => {
     }
   }
 }
+
+.homework-check-box {
+  position: absolute;
+  top: 15px;
+  left: 50%;
+  transform: translate(-50%, 0);
+  display: flex;
+  align-items: center;
+  box-shadow: 0px 3px 4px 3px #f2f2f2;
+  padding: 8px;
+  border-radius: 5px;
+  background: #fff;
+  z-index: 999;
+
+  .homework-check-box-item{
+    padding: 10px 18px;
+    border-radius: 5px;
+    font-weight: 600;
+    cursor: pointer;
+    transition: all 0.3s ease;
+
+    &.active{
+      background: #f6c82b;
+    }
+    
+    &:hover{
+      background: #fff;
+      color: #f6c82b;
+    }
+  }
+
+  .homework-check-box-item-title{}
+}
 </style>

Разница между файлами не показана из-за своего большого размера
+ 4653 - 0
src/views/Student/index2.vue


+ 6 - 1
src/views/lang/cn.json

@@ -721,7 +721,12 @@
   "ssCocoLinkTip":"请添加 Cocorobo 同域、亚马逊或可访问的 HTML 链接。",
   "ssChoiceQuestion":"选择题",
   "ssAnswerCount":"回答人数",
+  "ssQuestion":"题目",
+  "ssAnswer":"回答",
   "ssViewUnsubmittedStudents":"点击查看未提交学生",
   "ssSelectUser":"选择{a}的成员",
-  "ssUnsubmittedStudents":"未提交学生"
+  "ssUnsubmittedStudents":"未提交学生",
+  "ssAnalysis":"分析",
+  "ssAIGenerate":"AI生成",
+  "ssUpdateTime":"最后更新"
 }

+ 6 - 1
src/views/lang/en.json

@@ -721,7 +721,12 @@
   "ssCocoLinkTip":"Please add Cocorobo, Amazon, or accessible HTML link.",
   "ssChoiceQuestion":"Choice Question",
   "ssAnswerCount":"Answer count",
+  "ssQuestion":"Question",
+  "ssAnswer":"Answer",
   "ssViewUnsubmittedStudents":"Click to view unsubmitted students",
   "ssSelectUser":"Select members of {a}",
-  "ssUnsubmittedStudents":"Unsubmitted students"
+  "ssUnsubmittedStudents":"Unsubmitted students",
+  "ssAnalysis":"Analysis",
+  "ssAIGenerate":"AI Generate",
+  "ssUpdateTime":"Last Updated"
 }

+ 6 - 1
src/views/lang/hk.json

@@ -721,7 +721,12 @@
   "ssCocoLinkTip":"請添加 Cocorobo 同域、亚马逊或可访问的 HTML 链接。",
   "ssChoiceQuestion":"選擇題",
   "ssAnswerCount":"回答人數",
+  "ssQuestion":"題目",
+  "ssAnswer":"回答",
   "ssViewUnsubmittedStudents":"點擊查看未提交學生",
   "ssSelectUser":"選擇{a}的成員",
-  "ssUnsubmittedStudents":"未提交學生"
+  "ssUnsubmittedStudents":"未提交學生",
+  "ssAnalysis":"分析",
+  "ssAIGenerate":"AI生成",
+  "ssUpdateTime":"最後更新"
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов