Explorar o código

Merge branch 'beta'

lsc hai 1 semana
pai
achega
105bab7d39

+ 680 - 407
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -1,100 +1,184 @@
 <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 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>
+  <div class="ai-chat-container">
+    <div class="message-quick-box">
+      <div class="message-quick-box-item" :class="{ 'active': isQuickActions.includes('quick') }">
+        <button type="button" class="coco-collapse-trigger" @click="toggleQuickActions('quick')">
+          <span class="coco-collapse-icon soft-orange">
+            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
+              stroke-linecap="round">
+              <path d="M4 7h16"></path>
+              <path d="M4 12h10"></path>
+              <path d="M4 17h7"></path>
+            </svg>
+          </span>
+          <span class="coco-collapse-copy">
+            <span class="coco-collapse-title">{{ lang.ssAiChatQuickTitle }}</span>
+            <span class="coco-collapse-subtitle" id="cocoQuickSectionHint">{{ lang.ssAiChatQuickSubtitle }}</span>
+          </span>
+          <span class="coco-collapse-chevron">
+            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <polyline points="6 9 12 15 18 9"></polyline>
+            </svg>
+          </span>
+        </button>
+        <div class="coco-collapse-body" v-if="isQuickActions.includes('quick')">
+          <div class="coco-quick-tabs">
+            <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'page' }"
+              @click="setCocoQuickTab('page')">{{ lang.ssAiChatQuickTabPage }}</button>
+            <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'course' }"
+              @click="setCocoQuickTab('course')">{{ lang.ssAiChatQuickTabCourse }}</button>
+          </div>
+          <div class="coco-quick-list">
+            <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'" @click="sendQuickAction(lang.ssAiChatQuickAction3)">
+              <span class="coco-quick-action-copy">
+                <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenChoicesTitle }}</span>
+                <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenChoicesDesc }}</span>
+              </span>
+              <span class="coco-quick-action-arrow">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+                  stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M5 12h14"></path>
+                  <path d="M13 5l7 7-7 7"></path>
+                </svg>
+              </span>
+            </button>
+
+            <!-- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'" @click="sendQuickAction(lang.ssAiChatQuickAction4)">
+              <span class="coco-quick-action-copy">
+                <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenWebTitle }}</span>
+                <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenWebDesc }}</span>
+              </span>
+              <span class="coco-quick-action-arrow">
+                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
+                  stroke-linecap="round" stroke-linejoin="round">
+                  <path d="M5 12h14"></path>
+                  <path d="M13 5l7 7-7 7"></path>
+                </svg>
+              </span>
+            </button> -->
+            <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'"  @click="sendQuickAction(lang.ssAiChatQuickAction5)">
+                <span class="coco-quick-action-copy">
+                    <span class="coco-quick-action-title">{{ lang.ssAiChatQuickRecommendToolsTitle }}</span>
+                    <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickRecommendToolsDesc }}</span>
+                </span>
+                <span class="coco-quick-action-arrow">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                        <path d="M5 12h14"></path>
+                        <path d="M13 5l7 7-7 7"></path>
                     </svg>
-                    <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }" v-if="message.jsonData?.gType !== 'chat' && !message.chatloading && message.aiContent"
-                        @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm}}</button>
-                </div>
-            </div>
+                </span>
+            </button>
+            <!-- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'">
+                <span class="coco-quick-action-copy">
+                    <span class="coco-quick-action-title">{{ lang.ssAiChatQuickBatchGenToolsTitle }}</span>
+                    <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickBatchGenToolsDesc }}</span>
+                </span>
+                <span class="coco-quick-action-arrow">
+                    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+                        <path d="M5 12h14"></path>
+                        <path d="M13 5l7 7-7 7"></path>
+                    </svg>
+                </span>
+            </button> -->
+          </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="messages.length === 0 ? lang.ssAiChatExample : lang.ssAiChatShortcut"
-                    v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
-                <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>
+    </div>
+    <!-- 聊天区域 -->
+    <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 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 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>
+          <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }"
+            v-if="message.jsonData?.gType !== 'chat' && !message.chatloading && message.aiContent"
+            @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm }}</button>
         </div>
-        <!-- 初始状态 -->
-        <div class="initial-state" v-if="messages.length === 0">
-            <!-- 快捷操作 -->
-            <div class="quick-actions">
-                <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn" @click="sendQuickAction(action)">{{ action }}</button>
-            </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="lang.ssAiChatShortcut"
+          v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
+        <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>
+      <!-- 输入时的快捷操作弹出 -->
+      <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 class="initial-state" v-if="false">
+      <!-- 快捷操作 -->
+      <div class="quick-actions">
+        <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>
@@ -109,48 +193,48 @@ import axios from '@/services/config'
 import message from '@/utils/message'
 
 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
+  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
     }>
-    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
-        }>
-    }
+    sourceArray?: Array<{
+      text?: string
+      id?: string
+      title?: string
+    }>
+  }
 }
 
 const props = withDefaults(defineProps<{
-    userid?: string | null
+  userid?: string | null
 }>(), {
   userid: null,
 })
@@ -170,11 +254,14 @@ const files = ref<Array<{ title: string; id?: string | null; url?: string; isPro
 const quickActions = [
   lang.ssAiChatQuickAction1,
   lang.ssAiChatQuickAction2,
+  lang.ssAiChatQuickAction6,
+  lang.ssAiChatQuickAction7,
 ]
 
 // 监听输入变化,当输入"/"时显示快捷操作
 watch(inputText, (newValue) => {
-  if (messages.value.length > 0 && newValue === '/') {
+  // messages.value.length > 0 && 
+  if (newValue === '/') {
     showQuickActions.value = true
   }
   else if (newValue !== '/') {
@@ -242,7 +329,7 @@ const stopMessage = () => {
 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) {
@@ -281,10 +368,10 @@ const handleFileUpload = async (files2: File[]) => {
         files.value.splice(fileIndex, 1)
       }
     })
-    
+
     uploadPromises.push(uploadPromise)
   }
-  
+
   // 等待所有文件上传完成
   await Promise.allSettled(uploadPromises)
 }
@@ -367,7 +454,7 @@ const sendAction = async (action: string) => {
       const tempElement = document.createElement('div')
       tempElement.innerHTML = textElement.content
       return tempElement.textContent || tempElement.innerText || ''
-      
+
 
     })
     .filter(content => content.trim() !== '') || []
@@ -482,7 +569,7 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
     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}`)
@@ -504,7 +591,7 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
         signal: signal
       }
     )
-    
+
     console.log(res)
     return res
   }
@@ -522,6 +609,22 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
 const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
 const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
 
+const isQuickActions = ref<string[]>([])
+const toggleQuickActions = (section: string) => {
+  if (isQuickActions.value.includes(section)) {
+    isQuickActions.value = isQuickActions.value.filter((item: string) => item !== section)
+  }
+  else {
+    isQuickActions.value.push(section)
+  }
+}
+
+const cocoQuickTab = ref('page')
+const setCocoQuickTab = (tab: string) => {
+  cocoQuickTab.value = tab
+}
+
+
 
 onMounted(() => {
   session_name.value = uuidv4()
@@ -532,379 +635,549 @@ onMounted(() => {
 
 <style lang="scss" scoped>
 .ai-chat-container {
-    width: 100%;
-    height: 100%;
-    display: flex;
-    flex-direction: column;
-    padding: 16px;
-    gap: 16px;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  padding: 16px;
+  gap: 16px;
 }
 
 .input-section {
-    display: flex;
-    flex-direction: column;
-    gap: 12px;
-    position: relative;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  position: relative;
 }
 
 .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;
+  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;
-    }
+  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;
+  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;
-    }
+  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;
-    }
+  &:hover {
+    background: #FFF4E5;
+    color: #F78B22;
+    border-radius: 4px;
+  }
 
-    input[type="file"] {
-        display: none;
-    }
+  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;
-        }
+  margin-bottom: 8px;
+  min-height: 24px;
+  max-height: 70px;
+  overflow-y: auto;
 
-        .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;
+  .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;
+    }
 
-            svg {
-                width: 14px;
-                height: 14px;
-            }
+    .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;
-            }
-        }
+      &: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;
+  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;
+  }
 
-    svg {
-        width: 20px;
-        height: 20px;
-    }
+  &:hover {
+    background: #E68A00;
+  }
 
-    &:hover {
-        background: #E68A00;
-    }
+  &.stop {
+    background: unset;
+    width: auto;
 
-    &.stop {
-        background: unset;
-        width: auto;
-        svg {
-            width: 32px;
-            height: 32px;
-        }
+    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;
-        }
+  // 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;
 
-        +.quick-action-btn {
-            margin-top: 8px;
-        }
+    &: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;
-    }
+  // 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-track {
+    background: #F3F4F6;
+    border-radius: 3px;
+  }
 
-    &::-webkit-scrollbar-thumb {
-        background: #D1D5DB;
-        border-radius: 3px;
+  &::-webkit-scrollbar-thumb {
+    background: #D1D5DB;
+    border-radius: 3px;
 
-        &:hover {
-            background: #9CA3AF;
-        }
+    &:hover {
+      background: #9CA3AF;
     }
+  }
 }
 
 .chat-message {
-    max-width: 100%;
-    margin-bottom: 10px;
+  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 {
-        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 {
+      margin-top: 10px;
     }
+  }
 }
 
 .message-content {
-    word-break: break-word;
+  word-break: break-word;
 
-    &.ai-message {
-        align-self: flex-start;
-        background: #fafbfc;
-        border: 1.5px solid #e5e7eb;
-        border-bottom-left-radius: 2px;
+  &.ai-message {
+    align-self: flex-start;
+    background: #fafbfc;
+    border: 1.5px solid #e5e7eb;
+    border-bottom-left-radius: 2px;
 
-        &>svg {
-            width: 17px;
-            height: 17px;
-        }
+    &>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;
-    }
+  &.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;
+  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;
+  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: 8px;
+    border-radius: 6px;
     font-size: 14px;
+    color: #374151;
     cursor: pointer;
-    margin-left: auto;
-    transition: all 0.3s ease;
+    transition: all 0.2s ease;
 
     &:hover {
-        background: #E68A00;
-    }
-    &.disabled {
-        background: #9CA3AF;
-        cursor: not-allowed;
+      background: #fff4e5;
     }
+  }
 }
 
-ul {
-    margin: 8px 0;
-    padding-left: 20px;
+.message-quick-box {
+  display: flex;
+  flex-direction: column;
+  flex-shrink: 0;
+  gap: 10px;
+
+  .message-quick-box-item {
+    box-shadow: rgba(17, 24, 39, 0.04) 0px 10px 30px;
+    border-width: 1px;
+    border-style: solid;
+    border-color: rgb(239, 229, 216);
+    border-image: initial;
+    border-radius: 18px;
+    background: rgba(255, 255, 255, 0.96);
+    overflow: hidden;
 
-    li {
-        margin: 4px 0;
+
+    .coco-collapse-trigger {
+      width: 100%;
+      display: flex;
+      align-items: center;
+      cursor: pointer;
+      text-align: left;
+      gap: 12px;
+      border-width: initial;
+      border-style: none;
+      border-color: initial;
+      border-image: initial;
+      background: transparent;
+      padding: 12px 14px;
+
+      &:hover {
+        background: rgb(255, 251, 245);
+      }
     }
-}
 
-.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;
+    .coco-collapse-icon.soft-orange {
+      color: rgb(154, 91, 17);
+      background: rgb(255, 240, 219);
+    }
+
+    .coco-collapse-icon {
+      width: 34px;
+      height: 34px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      flex-shrink: 0;
+      border-radius: 12px;
+    }
+
+    .coco-collapse-copy {
+      min-width: 0px;
+      display: flex;
+      flex-direction: column;
+      flex: 1 1 0%;
+      gap: 2px;
+
+      .coco-collapse-title {
         font-size: 14px;
-        color: #374151;
-        cursor: pointer;
-        transition: all 0.2s ease;
+        font-weight: 700;
+        color: rgb(17, 24, 39);
+      }
+
+      .coco-collapse-subtitle {
+        font-size: 12px;
+        color: rgb(139, 115, 86);
+        line-height: 1.5;
+      }
+    }
+
+    .coco-collapse-chevron {
+      color: rgb(156, 163, 175);
+      flex-shrink: 0;
+
+      svg {
+        transition: transform 0.2s;
+      }
+    }
+
+    &.active {
+      .coco-collapse-chevron svg {
+        transform: rotate(180deg);
+      }
+    }
 
-        &:hover {
-            background: #fff4e5;
+    .coco-collapse-body {
+      padding: 0px 14px 14px;
+      .coco-quick-tabs {
+        display: grid;
+        grid-template-columns: repeat(2, minmax(0px, 1fr));
+        margin-bottom: 12px;
+        gap: 8px;
+        .coco-quick-tab {
+          height: 34px;
+          color: rgb(124, 92, 56);
+          font-size: 12px;
+          font-weight: 600;
+          cursor: pointer;
+          border-radius: 12px;
+          border-width: 1px;
+          border-style: solid;
+          border-color: rgb(234, 223, 206);
+          border-image: initial;
+          background: rgb(250, 247, 242);
+          transition: 0.2s;
+          &.active {
+            color: rgb(154, 91, 17);
+            box-shadow: rgba(247, 139, 34, 0.12) 0px 8px 18px;
+            border-color: rgba(247, 139, 34, 0.48);
+            background: rgb(255, 244, 229);
+          }
         }
+      }
+
+      .coco-quick-list {
+        display: flex;
+        flex-direction: column;
+        gap: 8px;
+        .coco-quick-action {
+          width: 100%;
+          display: flex;
+          align-items: flex-start;
+          cursor: pointer;
+          text-align: left;
+          gap: 12px;
+          border-width: 1px;
+          border-style: solid;
+          border-color: rgb(239, 229, 216);
+          border-image: initial;
+          background: rgb(255, 253, 250);
+          border-radius: 14px;
+          padding: 12px;
+          transition: 0.2s;
+          .coco-quick-action-copy {
+            min-width: 0px;
+            display: flex;
+            flex-direction: column;
+            flex: 1 1 0%;
+            gap: 4px;
+            .coco-quick-action-title {
+                font-size: 13px;
+                font-weight: 700;
+                color: rgb(17, 24, 39);
+                line-height: 1.4;
+            }
+            
+
+            .coco-quick-action-desc {
+                font-size: 12px;
+                line-height: 1.6;
+                color: rgb(139, 115, 86);
+            }
+          }
+
+          .coco-quick-action-arrow {
+            color: rgb(212, 163, 115);
+            flex-shrink: 0;
+            padding-top: 2px;
+          }
+        }
+
+      }
+
     }
+  }
 }
 </style>
 
 <style>
 .chat table {
-    text-align: center;
-    border-spacing: 0;
-    border-left: 1px solid #000;
-    border-bottom: 1px solid #000;
+  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;
+  border-top: 1px solid #000;
+  border-right: 1px solid #000;
+  padding: 10px;
 }
 
 .message-files {
-    margin-top: 8px;
-    display: flex;
-    flex-direction: column;
-    gap: 4px;
+  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;
+  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;
+  flex: 1;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
 }
 </style>

+ 1 - 0
src/components/CollapsibleToolbar/index.vue

@@ -283,6 +283,7 @@ const getTypeLabel = (type?: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,

+ 894 - 130
src/components/CollapsibleToolbar/index2.vue

@@ -2,7 +2,8 @@
   <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')">
+        <div class="sidebar-item feature-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>
@@ -10,7 +11,18 @@
           </svg>
           <span class="item-label">Coco AI</span>
         </div>
-        <div class="sidebar-item" :class="{ active: activeSubmenu === 'page' }" @click="toggleSubmenu('page')">
+        <div class="sidebar-item feature-sidebar-item" :class="{ active: activeSubmenu === 'uploadFile' }"
+          @click="toggleSubmenu('uploadFile')">
+          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+            <path d="M12 16V4"></path>
+            <path d="M7 9l5-5 5 5"></path>
+            <path d="M4 16.5v1.5A2 2 0 006 20h12a2 2 0 002-2v-1.5"></path>
+            <path d="M5 14h14"></path>
+          </svg>
+          <span class="item-label">{{ lang.ssUploadFile }}</span>
+        </div>
+        <div class="sidebar-divider"></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">
               <path id="Vector"
@@ -20,7 +32,7 @@
             </g>
           </svg>
           <span class="item-label">{{ lang.ssPage }}</span>
-        </div>
+        </div> -->
         <div class="sidebar-item" :class="{ active: activeSubmenu === 'interactive' }"
           @click="toggleSubmenu('interactive')">
           <svg class="item-icon" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -78,10 +90,12 @@
           </svg>
           <span class="item-label">{{ lang.ssMultimedia }}</span>
         </div>
-        <div class="sidebar-item" :class="{ active: activeSubmenu === 'english' }"
-          @click="toggleSubmenu('english')">
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'english' }" @click="toggleSubmenu('english')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"></path><path d="M19 10v2a7 7 0 01-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line>
+            <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"></path>
+            <path d="M19 10v2a7 7 0 01-14 0v-2"></path>
+            <line x1="12" y1="19" x2="12" y2="23"></line>
+            <line x1="8" y1="23" x2="16" y2="23"></line>
           </svg>
           <span class="item-label">{{ lang.ssEnglish }}</span>
         </div>
@@ -104,6 +118,104 @@
         <AiChat :userid="props.userid" />
       </div>
     </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'uploadFile' }">
+      <div class="submenu-title" style="margin-bottom: 0;">
+        <div class="title">{{ lang.ssUploadFile }}</div>
+        <div class="close-icon" @click="toggleSubmenu('uploadFile')">
+          <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">
+        <template v-if="readingFile">
+          <div class="reading-file">
+            <div class="loading-spinner"></div>
+            <div class="reading-text">{{ lang.ssReadingFile }}</div>
+          </div>
+        </template>
+        <template v-else-if="!showFileConfirmModal && !exportingDialog">
+          <FileInput accept=".pptx" @change="handleFileUpload">
+            <div class="upload-dropzone">
+              <div class="upload-dropzone-icon">
+                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
+                  <path d="M12 15V4"></path>
+                  <path d="M7 9l5-5 5 5"></path>
+                  <path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2"></path>
+                </svg>
+              </div>
+              <div class="upload-dropzone-title">{{ lang.ssDragAndDrop }}</div>
+              <div class="upload-dropzone-subtitle">{{ lang.ssSupportPptx }}</div>
+              <!-- <div class="upload-dropzone-footnote">同类型文件支持批量导入,跨类型文件请分开处理</div> -->
+            </div>
+          </FileInput>
+        </template>
+
+        <template v-else-if="!exportingDialog">
+          <div class="file-confirm-inline">
+            <div class="file-info">
+              <div class="file-title">{{ lang.ssFileDetected }}{{ currentFileName }}</div>
+              <div class="file-subtitle">{{ currentFileName }}({{ lang.ssTotalPages.replace('{count}',
+                pageCount.toString()) }})</div>
+            </div>
+
+            <div class="import-options">
+              <div class="import-option" :class="{ active: selectedImportOption === 'page' }"
+                @click="selectedImportOption = 'page'">
+                <div class="option-icon">
+                </div>
+                <div class="option-text">
+                  <div class="option-title">{{ lang.ssImportAsSlide }}</div>
+                  <div class="option-desc">{{ lang.ssImportAsSlideDesc }}</div>
+                </div>
+              </div>
+
+              <div v-show="false" class="import-option" :class="{ active: selectedImportOption === 'library' }"
+                @click="selectedImportOption = 'library'">
+                <div class="option-icon">
+                </div>
+                <div class="option-text">
+                  <div class="option-title">{{ lang.ssImportAndSave }}</div>
+                  <div class="option-desc">{{ lang.ssImportAndSaveDesc }}</div>
+                </div>
+              </div>
+            </div>
+
+            <div class="modal-buttons">
+              <button class="cancel-btn" @click="cancelFileUpload">{{ lang.ssCancel }}</button>
+              <button class="confirm-btn" @click="confirmFileUpload">{{ lang.ssConfirm }}</button>
+            </div>
+          </div>
+        </template>
+
+        <template v-if="exportingDialog">
+          <div class="progress-inline">
+            <div class="progress-header">
+              <!-- <span class="file-name">{{ currentFileName }}</span>
+              <span class="progress-percent">{{ importProgress }}%</span> -->
+              <div class="upload-task-main">
+                <div class="upload-task-name">{{ currentFileName }}</div>
+                <div class="upload-task-meta">课件文件 · {{ formatFileSize(currentFileSize) }}</div>
+              </div>
+              <div class="upload-task-side">
+                <div class="upload-task-percent">{{ importProgress }}%</div>
+                <button type="button" class="upload-task-action" :class="{ 'upload-task-close': !exporting }"
+                  @click="handleParsingClose">{{ exporting ? '取消' : '×' }}</button>
+              </div>
+            </div>
+            <div class="progress-bar">
+              <div class="progress-fill" :style="{ width: importProgress + '%' }"></div>
+            </div>
+            <div class="progress-loading" v-if="exporting">上传中...</div>
+            <!-- <button class="close-btn" @click="handleParsingClose">{{ lang.ssClose }}</button> -->
+          </div>
+        </template>
+      </div>
+    </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'page' }">
       <div class="submenu-title">
         <div class="title">{{ lang.ssAddTemplatePage }}</div>
@@ -175,7 +287,7 @@
         </div>
       </div>
       <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
-        @change="handleFileUpload">
+        @change="handleFileUpload" v-if="false">
         <div class="submenu-upload">
           <div class="submenu-icon">
             <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -253,25 +365,25 @@
         <div class="submenu-item" @click="handleToolClick('vote')" @mouseenter="hoveredTool = 'vote'"
           @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <polyline points="9 11 12 14 22 4"></polyline>
-              <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+            <polyline points="9 11 12 14 22 4"></polyline>
+            <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
           </svg>
           <span class="submenu-label">{{ lang.ssVote }}</span>
         </div>
         <div class="submenu-item" @click="handleToolClick('photo')" @mouseenter="hoveredTool = 'photo'"
           @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
-              <circle cx="12" cy="13" r="4"></circle>
+            <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+            <circle cx="12" cy="13" r="4"></circle>
           </svg>
           <span class="submenu-label">{{ lang.ssPhoto }}</span>
         </div>
         <div class="submenu-item" @click="handleToolClick('creative')" @mouseenter="hoveredTool = null"
           @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <circle cx="12" cy="12" r="10"/>
-            <line x1="12" y1="8" x2="12" y2="16"/>
-            <line x1="8" y1="12" x2="16" y2="12"/>
+            <circle cx="12" cy="12" r="10" />
+            <line x1="12" y1="8" x2="12" y2="16" />
+            <line x1="8" y1="12" x2="16" y2="12" />
           </svg>
           <span class="submenu-label">{{ lang.ssCreative }}</span>
         </div>
@@ -376,7 +488,17 @@
           </div>
           <span class="submenu-label">{{ lang.ssUploadWebpageLink }}</span>
         </div>
-        <!-- <div class="submenu-item">
+        <div class="submenu-item" @click="handleToolClick('createWebpage')">
+          <div class="submenu-icon">
+            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
+              <rect x="3" y="3" width="18" height="14" rx="2"></rect>
+              <path d="M12 8v6"></path>
+              <path d="M9 11h6"></path>
+            </svg>
+          </div>
+          <span class="submenu-label">{{ lang.ssNewWebpage }}</span>
+        </div>
+        <div class="submenu-item" @click="handleToolClick('uploadCode')">
           <div class="submenu-icon">
             <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
               <g id="Component 1">
@@ -389,16 +511,6 @@
             </svg>
           </div>
           <span class="submenu-label">{{ lang.ssUploadWebpage }}</span>
-        </div> -->
-        <div class="submenu-item" @click="handleToolClick('createWebpage')">
-          <div class="submenu-icon">
-            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-              <rect x="3" y="3" width="18" height="14" rx="2"></rect>
-              <path d="M12 8v6"></path>
-              <path d="M9 11h6"></path>
-            </svg>
-          </div>
-          <span class="submenu-label">{{ lang.ssNewWebpage }}</span>
         </div>
         <!-- <div class="submenu-item">
           <div class="submenu-icon">
@@ -446,6 +558,67 @@
         </div>
       </div>
     </div>
+    <div class="submenu" :class="{ visible: activeSubmenu === 'uploadCode' }">
+      <div class="submenu-title">
+        <div class="title">{{ lang.ssUploadWebpage }}</div>
+        <div class="close-icon" @click="activeSubmenu = 'h5page'">
+          <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="line_box">
+        <div class="upload-box">
+          <div class="upload-tabs">
+            <button class="upload-tab" :class="{ active: uploadTab === 'file' }"
+              @click="switchUploadTab('file')">上传文件</button>
+            <button class="upload-tab" :class="{ active: uploadTab === 'code' }"
+              @click="switchUploadTab('code')">粘贴代码</button>
+          </div>
+          <div class="uploadFilePanel" v-if="uploadTab === 'file'">
+            <div class="form-group">
+              <FileInput accept=".html,.htm" @change="handleFileCodeUpload" v-if="!uploadCodeFile">
+                <div class="file-upload-area">
+                  <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
+                    <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
+                    <polyline points="17 8 12 3 7 8"></polyline>
+                    <line x1="12" y1="3" x2="12" y2="15"></line>
+                  </svg>
+                  <p class="upload-text">{{ lang.ssDragAndDropHint }}</p>
+                  <p class="upload-hint">{{ lang.ssSupportHTML }}</p>
+                </div>
+              </FileInput>
+              <div class="file-name-display" v-else>
+                <span>{{ uploadCodeFile.name }}</span>
+                <button type="button" class="upload-task-action"
+                  @click="handleFileCodeUpload(null)">×</button>
+              </div>
+            </div>
+          </div>
+          <div class="pasteCodePanel" v-if="uploadTab === 'code'">
+            <div class="form-group">
+              <textarea class="code-textarea" :placeholder="lang.ssPasteHTML" v-model="codeInput"></textarea>
+            </div>
+          </div>
+          <button class="webpage-link-button"
+            :class="{ 'loading': isLoading, 'disabled': !uploadCodeFile || isLoading }"
+            :disabled="!uploadCodeFile || isLoading" @click="handleUploadCodeFile"
+            v-if="uploadTab === 'file'">
+            {{ isLoading ? lang.ssUploading : !uploadCodeFile ? lang.ssWaitingForInput2 : lang.ssStartUpload }}
+          </button>
+          <button class="webpage-link-button"
+            :class="{ 'loading': isLoading, 'disabled': !codeInput || isLoading }"
+            :disabled="!codeInput || isLoading" @click="handleUploadCode"
+            v-else-if="uploadTab === 'code'">
+            {{ isLoading ? lang.ssUploading : !codeInput ? lang.ssWaitingForInput2 : lang.ssStartUpload }}
+          </button>
+        </div>
+      </div>
+    </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'multimedia' }">
       <div class="submenu-title">
         <div class="title">{{ lang.ssAddMultimedia }}</div>
@@ -527,26 +700,9 @@
       <SpeakingPanel />
     </div>
 
-    <div v-if="exporting" class="parsing-modal">
-      <div class="parsing-content">
-        <div class="loading-spinner" v-if="exporting"></div>
-        <div class="success-icon" v-if="!exporting">
-          <svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
-            <g id="Component 1">
-              <path id="Vector" d="M5.41675 14.084L9.75008 18.4173L20.5834 7.58398" stroke="#FF9300"
-                stroke-width="2.16667" stroke-linecap="round" stroke-linejoin="round" />
-            </g>
-          </svg>
 
-        </div>
-        <h3>{{ exporting ? lang.ssParsing : lang.ssExportCompleted }}</h3>
-        <p v-if="exporting">{{ lang.ssParsingFile }}{{ currentFileName }}</p>
-        <p v-if="!exporting">{{ lang.ssParsingCompleted }}</p>
-        <button class="close-btn2" @click="handleParsingClose">
-          {{ exporting ? lang.ssClose : lang.ssComplete }}
-        </button>
-      </div>
-    </div>
+
+
   </div>
 </template>
 
@@ -779,8 +935,15 @@ const uploadWebpageLink = async () => {
   })
 
   if (!isValid) {
-    message.error(lang.ssCocoLinkTip)
+    // message.error(lang.ssCocoLinkTip)
+
+    message.warning(lang.ssCocoLinkTip2)
+    createSlide()
+    createFrameElement(webpageUrl.value, 81) // 假设15是网页工具的类型
     isLoading.value = false
+    // 清空输入框和验证状态
+    webpageUrl.value = ''
+    isValidUrl.value = null
     return
   }
   isLoading.value = false
@@ -862,7 +1025,7 @@ const handleToolClick = _.debounce((tool: string) => {
   }
   else if (tool === 'photo') {
     parentWindow?.addTool?.(79)
-  } 
+  }
   else if (tool === 'qa') {
     parentWindow?.addTool?.(15)
   }
@@ -892,6 +1055,9 @@ const handleToolClick = _.debounce((tool: string) => {
   else if (tool === 'uploadWebpage') {
     activeSubmenu.value = 'uploadWebpage'
   }
+  else if (tool === 'uploadCode') {
+    activeSubmenu.value = 'uploadCode'
+  }
 }, 300)
 
 const loadContentList = () => {
@@ -973,6 +1139,7 @@ const getTypeLabel = (type?: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,
@@ -998,16 +1165,88 @@ const getTypeClass = (type?: number) => {
 
 import useImport from '@/hooks/useImport'
 import message from '@/utils/message'
-const { importPPTXFile, exporting, getFile } = useImport()
+const { importPPTXFile, exporting, getFile, getPPTInfo, uploadFileToS3 } = useImport()
 const currentFileName = ref('')
+const currentFileSize = ref(0)
 const parsingStatus = ref<'parsing' | 'success'>('parsing')
 const parsingAbortController = ref<AbortController | null>(null)
+const showFileConfirmModal = ref(false)
+const pendingFile = ref<FileList | null>(null)
+const selectedImportOption = ref<'page' | 'library'>('page')
+const pageCount = ref(1)
+const readingFile = ref(false)
+const importProgress = ref(0)
+const progressInterval = ref<NodeJS.Timeout | null>(null)
+const exportingDialog = ref(false)
+
+// 格式化文件大小
+const formatFileSize = (bytes: number): string => {
+  if (bytes === 0) return '0 B'
+  const k = 1024
+  const sizes = ['B', 'KB', 'MB', 'GB']
+  const i = Math.floor(Math.log(bytes) / Math.log(k))
+  return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
+}
 
 const handleFileUpload = async (files: FileList) => {
   if (!files || files.length === 0) return
 
   const file = files[0]
   currentFileName.value = file.name
+  currentFileSize.value = file.size
+  pendingFile.value = files
+  selectedImportOption.value = 'page'
+  readingFile.value = true
+
+  try {
+    // 读取 PPT 信息获取页码
+    const info = await getPPTInfo(file)
+    pageCount.value = info.pageCount
+  }
+  catch (error) {
+    console.error('获取 PPT 信息失败:', error)
+    pageCount.value = 1 // 失败时默认显示 1 页
+  }
+  finally {
+    readingFile.value = false
+  }
+
+  showFileConfirmModal.value = true
+}
+
+const confirmFileUpload = async () => {
+  if (!pendingFile.value) return
+
+  showFileConfirmModal.value = false
+  importProgress.value = 0
+  exportingDialog.value = true
+  const startTimer = () => {
+    // 启动虚拟进度条(30秒从0-99%)
+    if (progressInterval.value) clearInterval(progressInterval.value)
+    const startTime = Date.now()
+    const duration = 30000 // 30秒
+    progressInterval.value = setInterval(() => {
+      console.log('progressInterval.value', progressInterval.value)
+      const elapsed = Date.now() - startTime
+      const progress = Math.min(99, Math.floor((elapsed / duration) * 99))
+      importProgress.value = progress
+      if (progress >= 99) {
+        importProgress.value = 99
+      }
+
+      if (!exporting.value) {
+        if (progressInterval.value) {
+          clearInterval(progressInterval.value)
+          importProgress.value = 100
+          progressInterval.value = null
+        }
+      }
+    }, 100)
+  }
+
+  const confirmOnclose = () => {
+    handleParsingClose()
+  }
 
   try {
     // 创建AbortController用于取消操作
@@ -1015,7 +1254,7 @@ const handleFileUpload = async (files: FileList) => {
     const signal = parsingAbortController.value.signal
 
     // 调用importPPTXFile并传入signal
-    await importPPTXFile(files, { signal })
+    await importPPTXFile(pendingFile.value, { signal, startTimer, confirmOnclose })
   }
   catch (error) {
     if (error instanceof DOMException && error.name === 'AbortError') {
@@ -1026,9 +1265,26 @@ const handleFileUpload = async (files: FileList) => {
       message.error(lang.ssFileParseFailedRetry)
     }
   }
+  finally {
+    // if (progressInterval.value) {
+    //   clearInterval(progressInterval.value)
+    //   progressInterval.value = null
+    //   importProgress.value = 100
+    // }
+    pendingFile.value = null
+  }
+}
+
+const cancelFileUpload = () => {
+  showFileConfirmModal.value = false
+  pendingFile.value = null
 }
 
 const handleParsingClose = () => {
+  if (progressInterval.value) {
+    clearInterval(progressInterval.value)
+    progressInterval.value = null
+  }
   if (exporting.value && parsingAbortController.value) {
     parsingAbortController.value.abort()
     exporting.value = false
@@ -1038,6 +1294,50 @@ const handleParsingClose = () => {
   else if (!exporting.value) {
     emit('close')
   }
+
+  exportingDialog.value = false
+}
+
+const uploadTab = ref('file')
+const switchUploadTab = (tab: 'file' | 'code') => {
+  uploadTab.value = tab
+}
+
+const codeInput = ref('')
+const uploadCodeFile = ref<File | null>()
+const handleFileCodeUpload = (files: FileList | null) => {
+  
+  if (!files || files.length === 0) {
+    uploadCodeFile.value = null
+    return
+  }
+
+  const file = files[0]
+  uploadCodeFile.value = file
+}
+
+const handleUploadCodeFile = async () => {
+  if (!uploadCodeFile.value) return
+  const fileName = uploadCodeFile.value.name.toLowerCase()
+  if (fileName.endsWith('.html') || fileName.endsWith('.htm')) {
+    isLoading.value = true
+    const url = await uploadFileToS3(uploadCodeFile.value)
+    createSlide()
+    createFrameElement(url, 73)
+    uploadCodeFile.value = null
+    isLoading.value = false
+  }
+}
+
+const handleUploadCode = async () => {
+  if (!codeInput.value) return
+  const file = new File([codeInput.value], 'index.html', { type: 'text/html' })
+  isLoading.value = true
+  const url = await uploadFileToS3(file)
+  createSlide()
+  createFrameElement(url, 73)
+  codeInput.value = ''
+  isLoading.value = false
 }
 </script>
 
@@ -1065,6 +1365,44 @@ const handleParsingClose = () => {
   position: relative;
 }
 
+.sidebar-divider {
+  position: relative;
+  margin: 4px 2px 2px;
+  height: 12px;
+}
+
+.sidebar-divider::before {
+  content: "";
+  position: absolute;
+  left: 0;
+  right: 0;
+  top: 50%;
+  height: 1px;
+  background: #e8ddd0;
+}
+
+.feature-sidebar-item {
+  min-height: 82px;
+  padding-top: 12px;
+  padding-bottom: 12px;
+  gap: 7px;
+  border: 1px solid #f1e2cc;
+  background: #fffdfa;
+  box-shadow: 0 8px 18px rgba(15, 23, 42, 0.05);
+
+  &:hover {
+    background: #fff2df !important;
+    border-color: rgba(247, 139, 34, 0.4) !important;
+    box-shadow: 0 14px 28px rgba(247, 139, 34, 0.18) !important;
+  }
+
+  &.active {
+    background: #fff2df !important;
+    border-color: rgba(247, 139, 34, 0.4) !important;
+    box-shadow: 0 14px 28px rgba(247, 139, 34, 0.18) !important;
+  }
+}
+
 .sidebar-item {
   width: 84px;
   padding: 12px 8px;
@@ -1467,112 +1805,129 @@ const handleParsingClose = () => {
 }
 
 
-.parsing-modal {
-  position: fixed;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  background: rgba(0, 0, 0, 0.5);
+
+
+.progress-inline {
   display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 1000;
+  flex-direction: column;
+  gap: 12px;
+  padding: 14px 14px 12px;
+  border-radius: 16px;
+  background: #fff;
+  border: 1px solid #f0ebe3;
+  box-shadow: 0 8px 20px rgba(15, 23, 42, 0.04);
+  width: calc(100% - 30px);
+  margin: 15px auto;
 
-  .parsing-content {
-    background: white;
-    border-radius: 12px;
-    padding: 24px;
-    width: 400px;
-    text-align: center;
-    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
-
-    .loading-spinner {
-      width: 48px;
-      height: 48px;
-      border: 4px solid #f0f0f0;
-      border-top: 4px solid #FF9300;
-      border-radius: 50%;
-      margin: 0 auto 20px;
-      animation: spin 1s linear infinite;
+  .progress-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 12px;
+
+    // .file-name {
+    //   font-size: 14px;
+    //   font-weight: 600;
+    //   color: #333;
+    // }
+
+    // .progress-percent {
+    //   font-size: 14px;
+    //   font-weight: 600;
+    //   color: #FF9300;
+    // }
+
+    .upload-task-main {
+      min-width: 0;
     }
 
-    .success-icon {
-      width: 48px;
-      height: 48px;
-      margin: 0 auto 20px;
-      background: #FFFAF0;
-      border-radius: 5px;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      color: white;
-      font-size: 24px;
-      font-weight: bold;
+    .upload-task-name {
+      font-size: 13px;
+      font-weight: 700;
+      color: #111827;
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
     }
 
-    h3 {
-      font-size: 20px;
-      font-weight: 600;
-      color: #333;
-      margin: 0 0 12px;
+    .upload-task-meta {
+      margin-top: 2px;
+      font-size: 11px;
+      color: #8b7356;
     }
 
-    p {
-      font-size: 14px;
-      color: #666;
-      margin: 0 0 24px;
+    .upload-task-side {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      flex-shrink: 0;
     }
 
-    .close-btn2 {
-      background: #FF9300;
-      color: white;
-      border: none;
-      border-radius: 8px;
-      padding: 12px 24px;
-      font-size: 14px;
-      font-weight: 500;
+    .upload-task-percent {
+      font-size: 12px;
+      font-weight: 700;
+      color: #c76a0c;
+      min-width: 40px;
+      text-align: right;
+    }
+
+    .upload-task-action {
+      border: 0;
+      background: transparent;
+      color: #9a8a77;
+      font-size: 12px;
+      font-weight: 600;
       cursor: pointer;
-      width: 100%;
-      transition: all 0.3s;
+    }
 
-      &:hover {
-        background: #e68a00;
-      }
+    .upload-task-close {
+      width: 20px;
+      height: 20px;
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      border-radius: 999px;
+      color: #8b7356;
+      font-size: 16px;
+      line-height: 1;
     }
   }
 
-  @keyframes spin {
-    0% {
-      transform: rotate(0deg);
-    }
+  .progress-bar {
+    width: 100%;
+    height: 4px;
+    background: #f0f0f0;
+    border-radius: 4px;
+    overflow: hidden;
 
-    100% {
-      transform: rotate(360deg);
+    .progress-fill {
+      height: 100%;
+      background: linear-gradient(90deg, #f9b24e 0%, #f07815 100%);
+      transition: width 0.24s ease;
+      border-radius: 4px;
+      transition: width 0.1s ease;
     }
   }
 
+  .progress-loading {
+    font-size: 12px;
+    color: #111827;
+  }
+
   .close-btn {
-    width: 32px;
-    height: 32px;
+    background: #FF9300;
+    color: white;
     border: none;
-    background: none;
+    border-radius: 8px;
+    padding: 10px 20px;
+    font-size: 14px;
+    font-weight: 500;
     cursor: pointer;
-    color: #999;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    border-radius: 4px;
-    transition: all 0.2s;
+    width: 100%;
+    transition: all 0.3s;
 
     &:hover {
-      background: #f0f0f0;
-      color: #666;
-    }
-
-    svg {
-      width: 16px;
-      height: 16px;
+      background: #e68a00;
     }
   }
 }
@@ -1645,4 +2000,413 @@ const handleParsingClose = () => {
     }
   }
 }
+
+.upload-dropzone {
+  position: relative;
+  border: 1.5px dashed rgba(247, 139, 34, 0.38);
+  border-radius: 20px;
+  background: #fffaf4;
+  min-height: 156px;
+  padding: 22px 20px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  text-align: center;
+  gap: 10px;
+  cursor: pointer;
+  transition: all 0.22s ease;
+  overflow: hidden;
+  width: calc(100% - 30px);
+  margin: 15px auto;
+  box-sizing: border-box;
+
+  &:hover {
+    border-style: solid;
+    border-color: #f78b22;
+    background: #fff3e2;
+    box-shadow: 0 18px 34px rgba(247, 139, 34, 0.16);
+    transform: translateY(-1px);
+  }
+
+  .upload-dropzone-icon {
+    width: 56px;
+    height: 56px;
+    border-radius: 18px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #f78b22;
+    background: rgba(247, 139, 34, 0.10);
+    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
+
+    svg {
+      width: 28px;
+      height: 28px;
+    }
+  }
+
+  .upload-dropzone-title {
+    font-size: 16px;
+    font-weight: 700;
+    color: #111827;
+  }
+
+  .upload-dropzone-subtitle {
+    font-size: 12px;
+    line-height: 1.6;
+    color: #7c6d5d;
+    max-width: 320px;
+  }
+
+  .upload-dropzone-footnote {
+    font-size: 11px;
+    color: #9a8a77;
+  }
+}
+
+.reading-file {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  padding: 40px 20px;
+  gap: 16px;
+
+  .loading-spinner {
+    width: 40px;
+    height: 40px;
+    border: 4px solid #f0f0f0;
+    border-top: 4px solid #FF9300;
+    border-radius: 50%;
+    animation: spin 1s linear infinite;
+  }
+
+  .reading-text {
+    font-size: 14px;
+    color: #666;
+  }
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.file-confirm-inline {
+  width: calc(100% - 30px);
+  margin: 15px auto;
+  box-sizing: border-box;
+  margin-top: 18px;
+  padding: 18px;
+  border-radius: 18px;
+  background: #fff;
+  border: 1px solid #f3e4cf;
+  box-shadow: 0 12px 28px rgba(15, 23, 42, 0.06);
+
+  .file-info {
+    margin-bottom: 16px;
+
+    .file-title {
+      font-size: 14px;
+      font-weight: 700;
+      color: #111827;
+      margin-bottom: 6px;
+    }
+
+    .file-subtitle {
+      font-size: 12px;
+      color: #7c6d5d;
+    }
+  }
+
+  .import-options {
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    margin-bottom: 20px;
+
+    .import-option {
+      border: 1px solid #efe6d8;
+      border-radius: 16px;
+      padding: 14px;
+      display: flex;
+      gap: 12px;
+      cursor: pointer;
+      transition: all 0.22s ease;
+      background: #fffdfa;
+
+      &:hover {
+        border-color: rgba(247, 139, 34, 0.4);
+        background: #fffdfa;
+      }
+
+      &.active {
+        border-color: #ff9300;
+        background: #fff5e5;
+
+        .option-icon {
+          border-color: #f78b22;
+
+          &::after {
+            content: '';
+            position: absolute;
+            inset: 3px;
+            border-radius: 50%;
+            background: #f78b22;
+          }
+        }
+      }
+
+      .option-icon {
+        width: 18px;
+        height: 18px;
+        border-radius: 50%;
+        border: 1.5px solid #d6c0a1;
+        margin-top: 2px;
+        position: relative;
+        flex-shrink: 0;
+      }
+
+      .option-text {
+        flex: 1;
+
+        .option-title {
+          font-size: 14px;
+          font-weight: 600;
+          color: #111827;
+          margin-bottom: 3px;
+        }
+
+        .option-desc {
+          font-size: 12px;
+          color: #6b7280;
+          line-height: 1.4;
+        }
+      }
+    }
+  }
+
+  .modal-buttons {
+    display: flex;
+    gap: 10px;
+    justify-content: flex-end;
+
+    .cancel-btn,
+    .confirm-btn {
+      min-width: 84px;
+      height: 34px;
+      padding: 0 14px;
+      border-radius: 10px;
+      font-size: 12px;
+      font-weight: 600;
+      cursor: pointer;
+      transition: all 0.2s ease;
+    }
+
+    .cancel-btn {
+      background: #fff;
+      border: 1px solid #e5e7eb;
+      color: #6b7280;
+    }
+
+    .confirm-btn {
+      background: linear-gradient(180deg, #f89a34 0%, #f07815 100%);
+      border: 0;
+      color: #fff;
+      box-shadow: 0 10px 20px rgba(240, 120, 21, 0.18);
+    }
+  }
+}
+
+.line_box {
+  height: calc(100% - 75px);
+  overflow: hidden;
+}
+
+.upload-box {
+  padding: 20px;
+
+  .upload-tabs {
+    display: flex;
+    margin-bottom: 20px;
+    border-bottom: 1px solid rgb(229, 231, 235);
+
+    .upload-tab {
+      border-top-width: initial;
+      border-right-width: initial;
+      border-left-width: initial;
+      border-top-color: initial;
+      border-right-color: initial;
+      border-left-color: initial;
+      background-color: transparent;
+      cursor: pointer;
+      font-size: 14px;
+      color: rgb(107, 114, 128);
+      margin-bottom: -1px;
+      padding: 10px 16px;
+      border-style: none none solid;
+      border-image: initial;
+      border-bottom: 2px solid transparent;
+
+      &.active {
+        color: rgb(247, 139, 34);
+        border-bottom-color: rgb(247, 139, 34);
+        font-weight: 600;
+      }
+    }
+  }
+
+  .uploadFilePanel {
+
+    .form-group {
+      display: flex;
+      flex-direction: column;
+      gap: 12px;
+
+      .file-upload-area {
+        text-align: center;
+        cursor: pointer;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        min-height: 190px;
+        border-width: 2px;
+        border-style: dashed;
+        border-color: rgb(229, 231, 235);
+        border-image: initial;
+        border-radius: 12px;
+        background: rgb(250, 251, 252);
+        padding: 40px 20px;
+        transition: 0.2s;
+        gap: 10px;
+
+        svg {
+          color: rgb(156, 163, 175);
+          margin-bottom: 12px;
+        }
+
+        .upload-text {
+          font-size: 14px;
+          font-weight: 500;
+          color: rgb(55, 65, 81);
+          margin-bottom: 6px;
+        }
+
+        .upload-hint {
+          font-size: 12px;
+          color: rgb(107, 114, 128);
+        }
+
+        &:hover {
+          border-color: rgb(247, 139, 34);
+          background: rgb(255, 248, 240);
+        }
+      }
+
+      .file-name-display {
+        margin-top: 12px;
+        font-size: 13px;
+        color: rgb(75, 85, 99);
+        background-color: rgb(249, 250, 251);
+        border-radius: 6px;
+        padding: 8px 12px;
+        border-width: 1px;
+        border-style: solid;
+        border-color: rgb(229, 231, 235);
+        border-image: initial;
+        display: flex;
+        justify-content: space-between;
+
+        span {
+          flex: 1;
+        }
+
+        .upload-task-action {
+          border: 0;
+          background: transparent;
+          color: #9a8a77;
+          font-size: 12px;
+          font-weight: 600;
+          cursor: pointer;
+        }
+      }
+    }
+  }
+
+  .pasteCodePanel {
+    .form-group {
+      display: flex;
+      flex-direction: column;
+      gap: 10px;
+
+      .code-textarea {
+        width: 100%;
+        min-height: 190px;
+        font-family: monospace;
+        font-size: 13px;
+        resize: vertical;
+        border-width: 1px;
+        border-style: solid;
+        border-color: rgb(209, 213, 219);
+        border-image: initial;
+        border-radius: 8px;
+        padding: 12px;
+        transition: border-color 0.2s;
+        box-sizing: border-box;
+        resize: none;
+
+        &:focus {
+          outline: none;
+          border-color: rgb(247, 139, 34);
+        }
+      }
+    }
+  }
+
+  .webpage-link-button {
+    text-align: center;
+    padding: 10px 24px;
+    border: none;
+    border-radius: 4px;
+    font-size: 14px;
+    font-weight: 500;
+    cursor: pointer;
+    transition: all 0.3s;
+    background-color: #FF9300;
+    color: white;
+    margin: 0 auto;
+    display: block;
+    margin-top: 15px;
+
+    &:hover:not(:disabled) {
+      background-color: #e68a00;
+    }
+
+    &:disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+    }
+
+    &.loading {
+      cursor: not-allowed;
+      opacity: 0.8;
+    }
+
+    &.error {
+      background-color: #ff4d4f;
+      color: white;
+
+      &:hover:not(:disabled) {
+        background-color: #ff7875;
+      }
+    }
+  }
+}
 </style>

+ 14 - 158
src/components/CollapsibleToolbar/index21.vue → src/components/CollapsibleToolbar/index22.vue

@@ -2,7 +2,7 @@
   <div class="collapsible-toolbar" :class="{ collapsed: isCollapsed }">
     <div class="toolbar-content" v-show="!isCollapsed">
       <div class="sidebar-content">
-        <div class="sidebar-item feature-sidebar-item" :class="{ active: activeSubmenu === 'cocoai' }" @click="toggleSubmenu('cocoai')">
+        <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>
@@ -10,17 +10,6 @@
           </svg>
           <span class="item-label">Coco AI</span>
         </div>
-        <div class="sidebar-item feature-sidebar-item" :class="{ active: activeSubmenu === 'uploadFile' }"
-          @click="toggleSubmenu('uploadFile')">
-          <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <path d="M12 16V4"></path>
-            <path d="M7 9l5-5 5 5"></path>
-            <path d="M4 16.5v1.5A2 2 0 006 20h12a2 2 0 002-2v-1.5"></path>
-            <path d="M5 14h14"></path>
-          </svg>
-          <span class="item-label">{{ lang.ssUploadFile }}</span>
-        </div>
-        <div class="sidebar-divider"></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">
@@ -89,12 +78,10 @@
           </svg>
           <span class="item-label">{{ lang.ssMultimedia }}</span>
         </div>
-        <div class="sidebar-item" :class="{ active: activeSubmenu === 'english' }" @click="toggleSubmenu('english')">
+        <div class="sidebar-item" :class="{ active: activeSubmenu === 'english' }"
+          @click="toggleSubmenu('english')">
           <svg class="item-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"></path>
-            <path d="M19 10v2a7 7 0 01-14 0v-2"></path>
-            <line x1="12" y1="19" x2="12" y2="23"></line>
-            <line x1="8" y1="23" x2="16" y2="23"></line>
+            <path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"></path><path d="M19 10v2a7 7 0 01-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line>
           </svg>
           <span class="item-label">{{ lang.ssEnglish }}</span>
         </div>
@@ -117,37 +104,6 @@
         <AiChat :userid="props.userid" />
       </div>
     </div>
-    <div class="submenu" :class="{ visible: activeSubmenu === 'uploadFile' }">
-      <div class="submenu-title" style="margin-bottom: 0;">
-        <div class="title">{{ lang.ssUploadFile }}</div>
-        <div class="close-icon" @click="toggleSubmenu('uploadFile')">
-          <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">
-        <FileInput accept=".pptx"
-          @change="handleFileUpload">
-          <div class="upload-dropzone">
-              <div class="upload-dropzone-icon">
-                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
-                      <path d="M12 15V4"></path>
-                      <path d="M7 9l5-5 5 5"></path>
-                      <path d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2"></path>
-                  </svg>
-              </div>
-              <div class="upload-dropzone-title">拖拽文件至此,或点击选择</div>
-              <div class="upload-dropzone-subtitle">支持.pptx</div>
-              <!-- <div class="upload-dropzone-footnote">同类型文件支持批量导入,跨类型文件请分开处理</div> -->
-          </div>
-        </FileInput>
-      </div>
-    </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'page' }">
       <div class="submenu-title">
         <div class="title">{{ lang.ssAddTemplatePage }}</div>
@@ -219,7 +175,7 @@
         </div>
       </div>
       <FileInput accept="application/vnd.openxmlformats-officedocument.presentationml.presentation"
-        @change="handleFileUpload" v-if="false">
+        @change="handleFileUpload">
         <div class="submenu-upload">
           <div class="submenu-icon">
             <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
@@ -297,25 +253,25 @@
         <div class="submenu-item" @click="handleToolClick('vote')" @mouseenter="hoveredTool = 'vote'"
           @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <polyline points="9 11 12 14 22 4"></polyline>
-            <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
+              <polyline points="9 11 12 14 22 4"></polyline>
+              <path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11"></path>
           </svg>
           <span class="submenu-label">{{ lang.ssVote }}</span>
         </div>
         <div class="submenu-item" @click="handleToolClick('photo')" @mouseenter="hoveredTool = 'photo'"
           @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
-            <circle cx="12" cy="13" r="4"></circle>
+              <path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"></path>
+              <circle cx="12" cy="13" r="4"></circle>
           </svg>
           <span class="submenu-label">{{ lang.ssPhoto }}</span>
         </div>
         <div class="submenu-item" @click="handleToolClick('creative')" @mouseenter="hoveredTool = null"
           @mouseleave="hoveredTool = null">
           <svg class="submenu-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-            <circle cx="12" cy="12" r="10" />
-            <line x1="12" y1="8" x2="12" y2="16" />
-            <line x1="8" y1="12" x2="16" y2="12" />
+            <circle cx="12" cy="12" r="10"/>
+            <line x1="12" y1="8" x2="12" y2="16"/>
+            <line x1="8" y1="12" x2="16" y2="12"/>
           </svg>
           <span class="submenu-label">{{ lang.ssCreative }}</span>
         </div>
@@ -906,7 +862,7 @@ const handleToolClick = _.debounce((tool: string) => {
   }
   else if (tool === 'photo') {
     parentWindow?.addTool?.(79)
-  }
+  } 
   else if (tool === 'qa') {
     parentWindow?.addTool?.(15)
   }
@@ -1017,6 +973,7 @@ const getTypeLabel = (type?: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,
@@ -1109,44 +1066,6 @@ const handleParsingClose = () => {
   position: relative;
 }
 
-.sidebar-divider {
-  position: relative;
-  margin: 4px 2px 2px;
-  height: 12px;
-}
-
-.sidebar-divider::before {
-    content: "";
-    position: absolute;
-    left: 0;
-    right: 0;
-    top: 50%;
-    height: 1px;
-    background: #e8ddd0;
-}
-
-.feature-sidebar-item {
-  min-height: 82px;
-  padding-top: 12px;
-  padding-bottom: 12px;
-  gap: 7px;
-  border: 1px solid #f1e2cc;
-  background: #fffdfa;
-  box-shadow: 0 8px 18px rgba(15, 23, 42, 0.05);
-
-  &:hover {
-    background: #fff2df !important;
-    border-color: rgba(247, 139, 34, 0.4) !important;
-    box-shadow: 0 14px 28px rgba(247, 139, 34, 0.18) !important;
-  }
-
-  &.active {
-    background: #fff2df !important;
-    border-color: rgba(247, 139, 34, 0.4) !important;
-    box-shadow: 0 14px 28px rgba(247, 139, 34, 0.18) !important;
-  }
-}
-
 .sidebar-item {
   width: 84px;
   padding: 12px 8px;
@@ -1727,67 +1646,4 @@ const handleParsingClose = () => {
     }
   }
 }
-
-.upload-dropzone{
-  position: relative;
-  border: 1.5px dashed rgba(247, 139, 34, 0.38);
-  border-radius: 20px;
-  background: #fffaf4;
-  min-height: 156px;
-  padding: 22px 20px;
-  display: flex;
-  flex-direction: column;
-  align-items: center;
-  justify-content: center;
-  text-align: center;
-  gap: 10px;
-  cursor: pointer;
-  transition: all 0.22s ease;
-  overflow: hidden;
-  width: calc(100% - 30px);
-  margin: 15px auto;
-  box-sizing: border-box;
-
-  &:hover{
-    border-style: solid;
-    border-color: #f78b22;
-    background: #fff3e2;
-    box-shadow: 0 18px 34px rgba(247, 139, 34, 0.16);
-    transform: translateY(-1px);
-  }
-  .upload-dropzone-icon{
-    width: 56px;
-    height: 56px;
-    border-radius: 18px;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    color: #f78b22;
-    background: rgba(247, 139, 34, 0.10);
-    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
-
-    svg{
-      width: 28px;
-      height: 28px;
-    }
-  }
-
-  .upload-dropzone-title{
-    font-size: 16px;
-    font-weight: 700;
-    color: #111827;
-  }
-
-  .upload-dropzone-subtitle{
-    font-size: 12px;
-    line-height: 1.6;
-    color: #7c6d5d;
-    max-width: 320px;
-  }
-
-  .upload-dropzone-footnote{
-    font-size: 11px;
-    color: #9a8a77;
-  }
-}
 </style>

+ 30 - 5
src/hooks/useImport.ts

@@ -1,4 +1,4 @@
-import { ref, nextTick } from 'vue'
+import { ref, nextTick } from 'vue'
 import { storeToRefs } from 'pinia'
 import { parse, type Shape, type Element, type ChartItem, type BaseElement } from 'pptxtojson'
 import { nanoid } from 'nanoid'
@@ -15,6 +15,29 @@ import { showConfirmDialog } from '@/utils/confirmDialog'
 import { EMFJS, WMFJS } from 'rtf.js'
 import * as UTIF from 'utif2'
 
+/**
+ * 获取 PPT 文件的基本信息
+ */
+const getPPTInfo = async (file: File): Promise<{ pageCount: number; filename: string }> => {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader()
+    reader.onload = async (e) => {
+      try {
+        const json = await parse(e.target!.result as ArrayBuffer)
+        resolve({
+          pageCount: json.slides.length,
+          filename: file.name
+        })
+      }
+      catch (error) {
+        reject(error)
+      }
+    }
+    reader.onerror = reject
+    reader.readAsArrayBuffer(file)
+  })
+}
+
 import type {
   Slide,
   TableCellStyle,
@@ -1620,13 +1643,13 @@ export default () => {
     }
   */
 
-  const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void }) => {
+  const importPPTXFile = (files: FileList, options?: { cover?: boolean; fixedViewport?: boolean; signal?: AbortSignal, onclose?: () => void, startTimer?: () => void, confirmOnclose?: () => void }) => {
     console.log('导入', files)
     const defaultOptions = {
       cover: false,
       fixedViewport: false,
     }
-    const { cover, fixedViewport, signal, onclose } = { ...defaultOptions, ...options }
+    const { cover, fixedViewport, signal, onclose, startTimer, confirmOnclose } = { ...defaultOptions, ...options }
 
     let isNone = false
     if (slides.value.length === 1 && slides.value[0].elements.length === 0) {
@@ -1638,7 +1661,7 @@ export default () => {
 
     exporting.value = true // 假设 exporting 是一个全局 ref
     imgExporting.value = true // 假设 imgExporting 是一个全局 ref
-
+    startTimer?.()
     // 预加载形状库(用于后续形状匹配)
     const shapeList: ShapePoolItem[] = []
     for (const item of SHAPE_LIST) {
@@ -1693,6 +1716,7 @@ export default () => {
             exporting.value = false
             imgExporting.value = false
             onclose?.()
+            confirmOnclose?.()
             return Promise.reject(new Error('用户取消导入'))
           }
         }
@@ -2502,6 +2526,7 @@ export default () => {
     getFile,
     getFile2,
     dataToFile,
-    uploadFileToS3
+    uploadFileToS3,
+    getPPTInfo
   }
 }

+ 2 - 2
src/services/course.ts

@@ -1,8 +1,8 @@
 import axios from './config'
 
 export const API_URL = 'https://pbl.cocorobo.cn/api/pbl/'
-export const yweb_socket = 'wss://yjs.cocorobo.cn'
-// export const yweb_socket = 'wss://yrs.cocorobo.cn'
+// export const yweb_socket = 'wss://yjs.cocorobo.cn'
+export const yweb_socket = 'wss://yrs.cocorobo.cn'
 
 /**
  * 获取课程详情

+ 1 - 0
src/views/Editor/CanvasTool/WebpageInput.vue

@@ -103,6 +103,7 @@ const getTypeLabel = (type: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,

+ 1 - 0
src/views/Editor/CanvasTool/index2.vue

@@ -327,6 +327,7 @@ const getTypeLabel = (type: number) => {
     15: lang.ssQATest,
     72: lang.ssAiApp,
     73: lang.ssHPage,
+    81: lang.ssHPage,
     74: lang.ssVideo,
     75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
     76: lang.ssCreative,

+ 454 - 31
src/views/Student/components/choiceQuestionDetailDialog.vue

@@ -342,7 +342,7 @@
       <div class="c_t72" v-if="props.showData && props.showData.toolType === 72">
         <div class="c_t72_title">{{ lang.ssAiApp }}</div>
         <span class="c_t72_type">{{ lang.ssAiApp }}</span>
-                <div class="c_t72_msg">
+        <div class="c_t72_msg">
           <div>{{ lang.ssAnswerCount }} {{ props.showData.workArray.length }}<span
               v-if="props.showData.unsubmittedStudents.length > 0">/{{ props.showData.workArray.length +
                 props.showData.unsubmittedStudents.length
@@ -503,6 +503,7 @@ const props = defineProps<{
   userId: string;
   workId: string;
   cid: string;
+  workUrl: string;
 }>()
 
 const emit = defineEmits<{
@@ -1361,6 +1362,8 @@ const getWordCloud15 = () => {
 课程数据:
 - 课程名称:${props.courseDetail.title}
 - 课程学科:${props.courseDetail.name}
+- 需要提交人数:${props.showData.workArray.length + props.showData.unsubmittedStudents.length}
+- 已提交人数:${props.showData.workArray.length}
 当前页面答题数据(问答题):【分析重点】
 - 问答题题目:${props.showData.workDetail.json.answerQ}
 - 回答数据:${JSON.stringify(processedWorkArray.value.map((i) => ({ answer: i.content.answer })))}
@@ -1387,6 +1390,7 @@ const getWordCloud15 = () => {
 const aiAnalysisRefresh72 = async () => {
 
   let chatMsg = ``
+  let agentId = 'a7741704-ba56-40b7-a6b8-62a423ef9376'
   console.log('processedWorkArray.value', processedWorkArray.value)
   processedWorkArray.value.forEach((i) => {
     if (typeof i.content === 'object') {
@@ -1419,6 +1423,8 @@ ${a.content}\n`
 课程数据:
 - 课程名称:${props.courseDetail.title}
 - 课程学科:${props.courseDetail.name}
+- 需要提交人数:${props.showData.workArray.length + props.showData.unsubmittedStudents.length}
+- 已提交人数:${props.showData.workArray.length}
 当前页面答题数据(问答题):【分析重点】
 - AI应用
 - 对话数据:${chatMsg}
@@ -1477,50 +1483,467 @@ ${a.content}\n`
 `
   }
   else if (['6c56ec0e-2c74-11ef-bee5-005056b86db5', 'aea65da6-4399-11f1-9985-005056924926'].includes(props.userId)) {
-    msg = `你是K-12阶段的AI课堂分析助手,负责基于学生互动数据或作文批改数据生成课堂学情分析报告。请遵循以下要求:
 
-### 任务目标
-1. **总体统计与分层分析**
-   - 对学生选择的分层(如A/B/C 或作文框架等级)进行统计,显示每层学生人数。
-   - 对每层学生的基础任务完成情况进行汇总,例如:
-     - 口语互动:1.内容——基础问题是否提及(如“What’s the name of the painting?”、“Who painted it?” 等),创新问题(时间、地点、感受等)是否有?2.发音/拼写——发音是否准确(语音转文字结果是否正确识别/拼写是否正确)
-     - 作文批改:基础写作要素是否掌握(句型、词汇、语法、拼写),创新亮点是否有。
-2. **精简洞察(Key Insights)**
-   - 每层仅列出最关键的基础问题未完成或错误率高的点。
-   - 每层仅列出最突出的点。
-   - 保持简短 1~3 行即可,避免冗长描述。
-3. **不输出教学建议或干预措施**。
+    if(props.workUrl=='https://knowledge.cocorobo.cn/zh-CN/story-telling/a1a339d4-f522-4336-9aa9-e8394bea9731'){
+      msg = `# 角色定位
 
-### 输出格式
-1. **Markdown 表格**:显示每层学生人数、基础问题完成情况、创新问题亮点、整体轮次/得分统计等。
-2. **ASCII 条形图(可选)**:展示问题完成率、创新问题占比或主题/写作要素分布。
-3. **Key Insights 精简版**:
-   - 简明列出每层的基础问题表现及创新亮点。
-   - 每条洞察对应具体指标或学生数量。
-   - 可使用 Emoji 提示情绪、表现或亮点状态。
+你是K-12阶段的AI课堂分析助手,负责基于学生对世界名画的英语宣传稿(promotional text)批改记录生成课堂学情分析报告。
+本次分析的内容来自六年级英语Art课堂,学生选择一幅画作并完成英语宣传稿写作,由AI智能体完成批改,分为 Level A(独立完成)、Level B(信息辅助)、Level C(支架辅助)两个等级。
 
-### 注意事项
-- 输出简洁明了,重点突出。
-- 所有数字列右对齐,必要时显示百分比。
-- 避免冗长文字、详细案例或小瑕疵。
-- 保持专业、友好语气。
-- 保留数据和图表作为报告核心。
-- **对于老师需要重点要注意的内容,可以用 <span style="color:red"> 内容 </span> 来highlight** 
-- 避免输出类似“如需进一步生成”这种对话式的内容。
+---
+
+# 重要:数据解读规则(必须在生成报告前完成)
+
+在输出任何报告内容之前,必须先完成以下两步结构化提取。
+所有报告数字必须来源于此,禁止前后矛盾。
+
+---
+
+## 第一步:逐学生提取数据
+
+(此步骤为内部处理步骤,不输出提取结果。)
+
+### 有效作文判断
+
+在提取每位学生数据之前,先判断该学生是否提交了有效作文:
+
+**有效作文:** AI批改记录中出现了完整的"作文批改报告"结构(包含信息完整性表格、表达鲜活度、文化小触角、语法小诊断中至少两项),视为学生已提交有效内容,正常提取数据。
+
+**无效提交:** 以下情况均视为无效提交,不纳入各维度统计,仅在总结部分单独列出:
+- AI输出"未检测到有效的写作内容"
+- AI批改记录缺失或仅有对话内容而无批改报告结构
+- 学生仅回复单个词语、符号或与写作任务完全无关的内容
+
+**注意:** 不得根据学生与AI的后续对话内容(如学生补充回复、追问等)判断写作有效性,仅以AI是否输出完整批改报告为准。
+
+### 等级识别
+从批改记录中识别学生选择的等级:
+- 选择"I can wtite. 我可以自己写。" → Level A
+- 选择"I need information. 我需要信息支持。" → Level B
+- 选择"I need structure. 我需要支架。" → Level C
+
+### 信息完整与准确性提取
+检查批改报告中的信息完整性表格,逐项记录每位学生的情况:
+
+| 信息点 | 是否正确 |
+|---|---|
+| 画名 | 是/否 |
+| 画家 | 是/否 |
+| 绘画种类 | 是/否 |
+| 画面描述(颜色/内容) | 是/否 |
+| 个人感受 | 是/否 |
+
+对每位学生统计:
+- 全部正确的信息点数量(满分5项)
+- 有错误或遗漏的具体信息点
+
+### 表达鲜活度提取
+从批改记录的"表达鲜活度"部分,判断:
+- ✅ 表达生动:使用了形容词、比喻、比较句或个人联想
+- ⚠️ 表达平淡:仅使用简单句型,缺乏个人想法
+
+### 文化小触角提取
+从批改记录的"文化小触角"部分,判断:
+- ✅ 有涉及:自然提到中西艺术差异或文化感悟
+- — 未涉及:未提及任何文化对比
+
+### 语法情况提取
+从批改记录的"语法小诊断"部分,记录:
+- 是否存在明显语法错误
+- 错误类型(主谓一致 / 时态 / 冠词 / 其他)
+
+---
+
+## 第二步:汇总数据
+
+将所有学生数据按等级分组汇总,形成统计表。
+**一旦汇总完成,报告中所有数字必须与汇总表严格一致,禁止前后矛盾。**
+
+---
+
+# 输出格式
+
+## 第一步输出
+输出以下一句话,然后立即继续输出完整报告,不得停止:
+"正在逐个提取学生数据中,请稍候……"
+
+## 第二步输出:完整报告
+
+紧接上一句,输出以下完整报告内容:
+
+---
+
+## 英语写作 课堂学情分析报告
+
+**数据来源:** 批改记录 | **统计人数:** X 人
+
+---
+
+### 一、分层总览
+
+| 等级 | 人数 | 信息完整率均值 | 表达生动人数 | 文化触角人数 | 有语法问题人数 |
+|---|---|---|---|---|---|
+| Level A | X | XX% | X人(XX%) | X人(XX%) | X人(XX%) |
+| Level B | X | XX% | X人(XX%) | X人(XX%) | X人(XX%) |
+| Level C | X | XX% | X人(XX%) | X人(XX%) | X人(XX%) |
+
+---
+
+### 二、信息完整与准确性明细(A / B / C 合并)
+
+| 信息点 | 正确人数 | 正确率 |
+|---|---|---|
+| 画名 | X | XX% |
+| 画家 | X | XX% |
+| 绘画种类 | X | XX% |
+| 画面描述(颜色/内容) | X | XX% |
+| 个人感受 | X | XX% |
+
+[正确率最低项:<span style="color:red">⚠️ [信息点名称]正确率最低,仅 XX%,说明学生在该项普遍存在遗漏或错误。</span>]
+
+---
+
+### 三、表达与文化维度
+
+| 维度 | A 达成人数 | A 达成率 | B 达成人数 | B 达成率 | C 达成人数 | C 达成率 |
+|---|---|---|---|---|
+| 表达生动(含比喻/形容词/个人联想) | X | XX% | X | XX% | X | XX% |
+| 文化小触角(含中西艺术对比) | X | XX% | X | XX% | X | XX% |
+
+[若文化触角整体达成率低于50%:<span style="color:red">⚠️ 文化对比意识普遍薄弱,全班达成率仅 XX%。</span>]
+
+---
+
+### 四、语法问题汇总
+
+| 语法问题类型 | A 出现人次 | B 出现人次 | C 出现人次 |
+|---|---|---|
+| 主谓一致错误 | X | X | X |
+| 时态错误 | X | X | X |
+| 冠词错误 | X | X | X |
+| 其他 | X | X | X |
+
+*若无明显语法问题,写:"本次无明显高频语法问题。✅"*
+
+---
+
+### 五、亮点摘录
+
+**表达生动的例句:**
+[列出2–3句来自学生作文的优秀表达原文,注明等级和画作名称。]
+
+**文化感悟的例句:**
+[列出1–2句来自学生作文的文化对比原文,注明等级和画作名称。若无,写"本次暂无。"]
+
+---
+
+### 六、总结
+
+**整体:** [1句,简述全班整体写作完成情况。]
+
+**突出:** [1句,指出全班表现最好的维度,例如信息完整率高或表达生动人数较多。]
+
+**关注:** <span style="color:red">[1句,指出最薄弱的维度,例如文化触角普遍缺失或某信息点遗漏率高。]</span>
+
+**跟进:**
+- <span style="color:red">无效提交学生:[姓名列表],AI未能识别其提交内容,建议教师查看原图并确认是否需要重新上传。</span>
+- <span style="color:red">写作内容需关注的学生(有效提交中):[姓名及原因,例如信息缺失3项以上、存在多处语法问题等];若无,写"有效提交学生中暂无需特别跟进。"</span>
+
+*若全员有效提交且表现均衡,写:"本次全员提交有效内容,整体表现均衡,暂无需特别跟进。"*
+
+---
+
+# 注意事项
+
+- 所有数字来源于结构化提取,输出前核对一致性,禁止前后矛盾。
+- 对需要教师重点关注的内容使用 <span style="color:red"> 内容 </span> 高亮。
+- 禁止输出"如需进一步生成"等对话式内容。
+- 不输出教学建议或干预措施。
+- 若某等级无学生数据,对应列填"—",不单独说明。
 
 #INPUT#
 课程数据:
 - 课程名称:${props.courseDetail.title}
 - 课程学科:${props.courseDetail.name}
-当前页面答题数据(问答题):【分析重点】
+- 需要提交人数:${props.showData.workArray.length + props.showData.unsubmittedStudents.length}
+- 已提交人数:${props.showData.workArray.length}
+当前页面答题数据(AI应用):【分析重点】
 - AI应用
 - 对话数据:${chatMsg}`
-  }
+    }else if(props.workUrl=='https://knowledge.cocorobo.cn/zh-CN/story-telling/0d04cef1-876a-41b4-9768-6547088bc162'){
+      msg = `# 角色定位
+
+你是K-12阶段的AI课堂分析助手,负责基于学生词句训练对话记录生成课堂学情分析报告。
+本次分析的环节为:学生与AI就画作内容进行词句问答训练,分为 Level A、Level B、Level C 三个等级。
 
+---
+
+# 重要:数据解读规则(必须在生成报告前完成)
+
+在输出任何报告内容之前,必须先完成以下两步结构化提取。
+所有报告数字必须来源于此,禁止前后矛盾。
+
+---
+
+## 第一步:逐学生提取数据
+
+(此步骤为内部处理步骤,不输出提取结果。)
+
+### 等级识别
+从对话记录中的 sender 字段识别等级:
+- sender 含"Level A" → Level A
+- sender 含"Level B" → Level B
+- sender 含"Level C" → Level C
+
+### 完成状态判断
+- **完整完成**:所有句子/问题均有学生回应记录
+- **部分完成**:至少一条学生回应,但未完成全部
+- **未完成**:对话记录中仅有 AI 开场消息,无任何学生回应内容
+
+### Level A 提取项目
+
+**主题覆盖性:** 检查学生是否主动提问了以下 6 个主题:
+① name ② artist ③ kind ④ scene ⑤ colours ⑥ why
+- ✅ 已提问 / — 未提问
+
+**问题准确性(语法):** 识别以下错误类型并记录人次:
+- 疑问句结构错误 / 主谓一致错误 / 时态错误 / 其他
+
+**创新性:** 6 个主题之外的问题,记录原文。
+
+### Level B 提取项目
+
+**主题覆盖性:**(同 Level A,6个主题)
+
+**问题准确性(语法):**(同 Level A)
+
+### Level C 提取项目
+
+**流程完成性:** 逐句核对学生是否跟读了全部 6 个问句。
+- ✅ 跟读基本正确 / ⚠️ 跟读明显偏差 / — 未跟读
+
+**创新亮点:** 学生是否有自发延伸表达,记录原文。
+
+---
+
+## 第二步:汇总数据
+
+将所有学生数据按等级分组汇总。
+**报告中所有数字必须与汇总表严格一致,禁止前后矛盾。**
+
+---
+
+# 输出格式
+
+## 第一步输出
+输出以下一句话,然后立即继续输出完整报告,不得停止:
+"正在逐个提取学生数据中,请稍候……"
+
+## 第二步输出:完整报告
+
+紧接上一句,输出以下完整报告内容:
+
+---
+
+## 词句问答训练 课堂学情分析报告
+
+**数据来源:** 学生对话记录 | **统计人数:** X 人
+
+---
+
+### 一、分层总览
+
+| 等级 | 人数 | 完整完成 | 主题覆盖率均值 | 语法问题人数 | 创新提问/亮点 |
+|---|---|---|---|---|---|
+| Level A | X | X人(XX%) | XX% | X人 | X人 |
+| Level B | X | X人(XX%) | XX% | X人 | — |
+| Level C | X | X人(XX%) | — | — | X人 |
+
+[若有未完成学生:<span style="color:red">⚠️ 未完成学生:[姓名列表]</span>]
+
+---
+
+### 二、主题覆盖明细(Level A / B)
+
+| 主题 | A 覆盖率 | B 覆盖率 |
+|---|---|---|
+| name | XX% | XX% |
+| artist | XX% | XX% |
+| kind | XX% | XX% |
+| scene | XX% | XX% |
+| colours | XX% | XX% |
+| why | XX% | XX% |
+
+*若本次无某等级学生,对应列填"—"。*
+
+---
+
+### 三、语法问题(Level A / B)
+
+| 问题类型 | A 出现人次 | B 出现人次 |
+|---|---|---|
+| 疑问句结构错误 | X | X |
+| 主谓一致错误 | X | X |
+| 时态错误 | X | X |
+| 其他 | X | X |
+
+*若无语法问题,写:"本次无明显语法问题。✅"*
+
+---
+
+### 四、创新与亮点
+
+**Level A 创新提问:**
+[列出学生姓名及原文;若无,写"本次暂无。"]
+
+**Level C 自发延伸:**
+[列出学生姓名及原文;若无,写"本次暂无。"]
+
+---
+
+### 五、总结
+
+**整体:** [1句,简述全班完成情况。]
+
+**突出:** [1句,指出表现最好的维度或等级。]
+
+**关注:** <span style="color:red">[1句,指出覆盖率最低的主题或问题最集中的点。]</span>
+
+**跟进:** <span style="color:red">[列出需个别跟进的学生姓名及原因;若无,写"全员表现均衡,暂无需特别跟进。"]</span>
+
+---
+
+# 注意事项
+
+- 所有数字来源于结构化提取,输出前核对一致性,禁止前后矛盾。
+- 对需要教师重点关注的内容使用 <span style="color:red"> 内容 </span> 高亮。
+- 若某等级无学生数据,相关行/列填"—",不单独输出该等级报告。
+- 禁止输出"如需进一步生成"等对话式内容。
+- 不输出教学建议或干预措施。
+
+#INPUT#
+课程数据:
+- 课程名称:${props.courseDetail.title}
+- 课程学科:${props.courseDetail.name}
+- 需要提交人数:${props.showData.workArray.length + props.showData.unsubmittedStudents.length}
+- 已提交人数:${props.showData.workArray.length}
+当前页面答题数据(AI应用):【分析重点】
+- AI应用
+- 对话数据:${chatMsg}`
+    }
+
+//     msg = `# 角色定位
+// 你是K-12阶段的AI课堂分析助手,负责基于学生词句训练对话记录生成课堂学情分析报告。
+// 本次分析的环节为:学生与AI就画作内容进行词句问答训练,分为 Level A、Level B、Level C 三个等级。
+// ---
+// # 重要:数据解读规则(必须在生成报告前完成)
+// 在输出任何报告内容之前,必须先完成以下两步结构化提取。
+// 所有报告数字必须来源于此,禁止前后矛盾。
+// ---
+// ## 第一步:逐学生提取数据
+// (此步骤为内部处理步骤,不输出提取结果。)
+// ### 等级识别
+// 从对话记录中的 sender 字段识别等级:
+// - sender 含"Level A" → Level A
+// - sender 含"Level B" → Level B
+// - sender 含"Level C" → Level C
+// ### 完成状态判断
+// - **完整完成**:所有句子/问题均有学生回应记录
+// - **部分完成**:至少一条学生回应,但未完成全部
+// - **未完成**:对话记录中仅有 AI 开场消息,无任何学生回应内容
+// ### Level A 提取项目
+// **主题覆盖性:** 检查学生是否主动提问了以下 6 个主题:
+// ① name ② artist ③ kind ④ scene ⑤ colours ⑥ why
+// - ✅ 已提问 / — 未提问
+// **问题准确性(语法):** 识别以下错误类型并记录人次:
+// - 疑问句结构错误 / 主谓一致错误 / 时态错误 / 其他
+// **创新性:** 6 个主题之外的问题,记录原文。
+// ### Level B 提取项目
+// **主题覆盖性:**(同 Level A,6个主题)
+// **问题准确性(语法):**(同 Level A)
+// ### Level C 提取项目
+// **流程完成性:** 逐句核对学生是否跟读了全部 6 个问句。
+// - ✅ 跟读基本正确 / ⚠️ 跟读明显偏差 / — 未跟读
+// **创新亮点:** 学生是否有自发延伸表达,记录原文。
+// ---
+// ## 第二步:汇总数据
+// 将所有学生数据按等级分组汇总。
+// **报告中所有数字必须与汇总表严格一致,禁止前后矛盾。**
+// ---
+// # 输出格式
+// ## 第一步输出
+// 输出以下一句话,然后立即继续输出完整报告,不得停止:
+// "正在逐个提取学生数据中,请稍候……"
+// ## 第二步输出:完整报告
+// 紧接上一句,输出以下完整报告内容:
+// ---
+// ## 词句问答训练 课堂学情分析报告
+// **数据来源:** 学生对话记录 | **统计人数:** X 人
+// ---
+// ### 一、分层总览
+// | 等级 | 人数 | 完整完成 | 主题覆盖率均值 | 语法问题人数 | 创新提问/亮点 |
+// |---|---|---|---|---|---|
+// | Level A | X | X人(XX%) | XX% | X人 | X人 |
+// | Level B | X | X人(XX%) | XX% | X人 | — |
+// | Level C | X | X人(XX%) | — | — | X人 |
+// [若有未完成学生:<span style="color:red">⚠️ 未完成学生:[姓名列表]</span>]
+// ---
+// ### 二、主题覆盖明细(Level A / B)
+// | 主题 | A 覆盖率 | B 覆盖率 |
+// |---|---|---|
+// | name | XX% | XX% |
+// | artist | XX% | XX% |
+// | kind | XX% | XX% |
+// | scene | XX% | XX% |
+// | colours | XX% | XX% |
+// | why | XX% | XX% |
+// *若本次无某等级学生,对应列填"—"。*
+// ---
+// ### 三、语法问题(Level A / B)
+// | 问题类型 | A 出现人次 | B 出现人次 |
+// |---|---|---|
+// | 疑问句结构错误 | X | X |
+// | 主谓一致错误 | X | X |
+// | 时态错误 | X | X |
+// | 其他 | X | X |
+// *若无语法问题,写:"本次无明显语法问题。✅"*
+// ---
+// ### 四、创新与亮点
+// **Level A 创新提问:**
+// [列出学生姓名及原文;若无,写"本次暂无。"]
+// **Level C 自发延伸:**
+// [列出学生姓名及原文;若无,写"本次暂无。"]
+// ---
+// ### 五、总结
+// **整体:** [1句,简述全班完成情况。]
+// **突出:** [1句,指出表现最好的维度或等级。]
+// **关注:** <span style="color:red">[1句,指出覆盖率最低的主题或问题最集中的点。]</span>
+// **跟进:** <span style="color:red">[列出需个别跟进的学生姓名及原因;若无,写"全员表现均衡,暂无需特别跟进。"]</span>
+// ---
+// # 注意事项
+// - 所有数字来源于结构化提取,输出前核对一致性,禁止前后矛盾。
+// - 对需要教师重点关注的内容使用 <span style="color:red"> 内容 </span> 高亮。
+// - 若某等级无学生数据,相关行/列填"—",不单独输出该等级报告。
+// - 禁止输出"如需进一步生成"等对话式内容。
+// - 不输出教学建议或干预措施。
+
+// #INPUT#
+// 课程数据:
+// - 课程名称:${props.courseDetail.title}
+// - 课程学科:${props.courseDetail.name}
+// - 需要提交人数:${props.showData.workArray.length + props.showData.unsubmittedStudents.length}
+// - 已提交人数:${props.showData.workArray.length}
+// 当前页面答题数据(AI应用):【分析重点】
+// - AI应用
+// - 对话数据:${chatMsg}`
+
+
+  }
 
 
 
-  console.log('cs', msg)
+  console.log('workUrl', props.workUrl)
+  console.log('ai应用提示词', msg)
   if (!currentAnalysis.value) {
     aiAnalysisData.value.push({
       pid: props.workId + (props.cid ? ',' + props.cid : ''),

+ 23 - 17
src/views/Student/index.vue

@@ -106,7 +106,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) && currentSlideToolType !== 77" :cid="props.cid" :workId="workId"  :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"/>
+          <choiceQuestionDetailDialog v-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType !== 77" :cid="props.cid" :workId="workId" :workUrl="workUrl" :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"/>
           <SpeakingClassPanel
             v-else-if="choiceQuestionDetailDialogOpenList.includes(slideIndex) && currentSlideToolType === 77"
             ref="speakingPanelRef"
@@ -309,7 +309,7 @@
         <!-- 回答结果内容 -->
         <div v-show="!workPanelCollapsed && rightPanelMode === 'homework'" class="panel-content">
           <div v-if="workLoading" class="homework-loading">{{ lang.ssHwLoading }}</div>
-          <answerTheResult :toolType="toolType" :workId="workId" :workArray="workArray" :unsubmittedStudents="unsubmittedStudents" :slideIndex="slideIndex" v-else ref="answerTheResultRef" @openChoiceQuestionDetail="openChoiceQuestionDetail" @openWorkModal="openWorkModal"/>
+          <answerTheResult :toolType="toolType" :workId="workId"  :workArray="workArray" :unsubmittedStudents="unsubmittedStudents" :slideIndex="slideIndex" v-else ref="answerTheResultRef" @openChoiceQuestionDetail="openChoiceQuestionDetail" @openWorkModal="openWorkModal"/>
           <!--<div class="homework-title">已提交</div>
           <div v-if="workLoading" class="homework-loading">正在加载作业...</div>
           <div v-else>
@@ -623,6 +623,8 @@ provide('choiceQuestionDetailDialogOpenList', choiceQuestionDetailDialogOpenList
 
 // 当前作业选择/问答题的ID
 const workId = ref<string>('')
+// 当前作业的url  
+const workUrl = ref<string>('')
 // 当前作业的type
 const toolType = ref<string>('')
 
@@ -1063,7 +1065,7 @@ const currentSlideConfigId = computed(() => {
 // 检测当前幻灯片是否包含B站视频
 const currentSlideHasBilibiliVideo = computed(() => {
   return elementList.value.some(element => 
-    element.type === ElementTypes.FRAME && (element.toolType === 75 || element.toolType === 74 || element.toolType === 76)
+    element.type === ElementTypes.FRAME && (element.toolType === 75 || element.toolType === 74 || element.toolType === 76 || element.toolType === 81)
   )
 })
 
@@ -1242,12 +1244,14 @@ const getWorkId = () => {
     // 提取链接中的id参数
     const url = (element as any).url
     let id = ''
+    
     toolType.value = (element as any).toolType
     if (typeof url === 'string') {
       const match = url.match(/[?&]id=([^&]+)/)
       if (match) {
         id = match[1]
       }
+      workUrl.value = url
       workId.value = id
       if ((element as any).toolType === 72) {
         workId.value = (element as any).id
@@ -1255,10 +1259,12 @@ const getWorkId = () => {
     }
     else {
       workId.value = ''
+      workUrl.value = ''
     }
   }
   else {
     workId.value = ''
+    workUrl.value = ''
   }
 }
 
@@ -3916,17 +3922,17 @@ const createWebSocketConnection = async (type = 1) => {
     }
     
     // 获取认证 token
-    // try {
-    //   authToken.value = await getAuthToken()
-    //   console.log('🔐 认证 token 获取成功,准备连接 WebSocket')
-    // }
-    // catch (error) {
-    //   console.error('🔐 获取认证 token 失败,连接可能失败:', error)
-    //   connectionStatus.value = 'disconnected'
-    //   isConnecting.value = false
-    //   handleDisconnection()
-    //   return
-    // }
+    try {
+      authToken.value = await getAuthToken()
+      console.log('🔐 认证 token 获取成功,准备连接 WebSocket')
+    }
+    catch (error) {
+      console.error('🔐 获取认证 token 失败,连接可能失败:', error)
+      connectionStatus.value = 'disconnected'
+      isConnecting.value = false
+      handleDisconnection()
+      return
+    }
 
     docSocket.value = new Y.Doc()
     docSocket.value.gc = true
@@ -3934,11 +3940,11 @@ const createWebSocketConnection = async (type = 1) => {
       api.yweb_socket,
       'PPT' + props.courseid,
       docSocket.value,
-      // { params: { yauth: authToken.value } }
+      { params: { yauth: authToken.value } }
     )
     
     // 启动定期更新 token
-    // updateAuthToken()
+    updateAuthToken()
 
     providerSocket.value.on('status', (event: any) => {
       console.log('👉 WebSocket状态:', event.status)
@@ -4032,7 +4038,7 @@ const createWebSocketConnection = async (type = 1) => {
   }
 
   // 启动 socket 连接检查定时器
-  startSocketCheckTimer()
+  // startSocketCheckTimer()
 }
 
 // 处理连接断开

+ 1 - 0
src/views/components/element/FrameElement/BaseFrameElement.vue

@@ -169,6 +169,7 @@ const getTypeLabel = (type: number): string => {
     15: 'ssEssayQ',
     72: 'ssAIApp',
     73: 'ssH5Page',
+    81: 'ssH5Page',
     74: 'ssVideo',
     75: lang.lang == 'cn' ? 'ssBiliVideo' : 'ssYouTube',
     76: 'ssCreateSpace',

+ 31 - 2
src/views/lang/cn.json

@@ -707,7 +707,7 @@
   "ssCreateApp": "创建应用",
   "ssAddInteractiveWebpage": "添加交互网页",
   "ssWebpageCenter": "网页中心",
-  "ssUploadWebpage": "上传网页",
+  "ssUploadWebpage": "上传代码",
   "ssCrawlWebpage": "爬取网页",
   "ssAddMultimedia": "添加多媒体",
   "ssDocument": "文档",
@@ -716,6 +716,7 @@
   "ssWebpageLink": "网页链接",
   "ssEnterCompleteUrl": "请输入完整的网页URL地址",
   "ssWaitingForInput": "等待输入...",
+  "ssWaitingForInput2": "等待上传...",
   "ssInvalidUrl": "URL格式不正确,请检查",
   "ssStartUpload": "开始上传",
   "ssUploading": "上传中...",
@@ -728,6 +729,7 @@
   "ssClearToolContent": "该操作将清除当前工具的编辑内容,是否继续?",
   "ssUploadWebpageLink": "上传链接",
   "ssCocoLinkTip":"请添加 Cocorobo 同域、亚马逊或可访问的 HTML 链接。",
+  "ssCocoLinkTip2": "非平台域名支持链接,可能存在显示异常,且不支持学生端提交作业。",
   "ssChoiceQuestion":"选择题",
   "ssAnswerCount":"回答人数",
   "ssQuestion":"题目",
@@ -742,10 +744,24 @@
   "ssAiChatShortcut": "输入 / 获取快捷操作短语",
   "ssAiChatQuickAction1": "为当前页面内容生成2道选择题",
   "ssAiChatQuickAction2": "为当前页面内容生成1道问答题",
+  "ssAiChatQuickAction6": "为当前页面生成投票",
+  "ssAiChatQuickAction7": "为当前页面生成拍照题",
   "ssAiChatParsing": "解析中...",
   "ssAiChatWaitUpload": "请等待文件上传完成后再发送消息",
   "ssAiChatFileSizeLimit": "文件大小不能超过10MB",
   "ssAiChatUploadFailed": "文件上传失败:",
+  "ssAiChatQuickTitle": "快捷指令",
+  "ssAiChatQuickSubtitle": "快捷生成单页内容或单个元素",
+  "ssAiChatQuickTabPage": "针对单个页面",
+  "ssAiChatQuickTabCourse": "针对完整课件",
+  "ssAiChatQuickGenChoicesTitle": "生成 2 道选择题",
+  "ssAiChatQuickGenChoicesDesc": "为当前页生成课堂练习,支持除创作空间外的所有工具。",
+  "ssAiChatQuickGenWebTitle": "生成互动网页",
+  "ssAiChatQuickGenWebDesc": "为当前页生成一个交互网页(如模拟器、小游戏等)。",
+  "ssAiChatQuickRecommendToolsTitle": "互动工具推荐",
+  "ssAiChatQuickRecommendToolsDesc": "扫描课程内容,推荐适合页面的互动工具。",
+  "ssAiChatQuickBatchGenToolsTitle": "批量生成互动工具",
+  "ssAiChatQuickBatchGenToolsDesc": "按照课程结构批量生成互动工具,并在待选区确认。",
   "ssClassroomAiAssistant": "课堂AI助手",
   "ssRetryMessage": "网络有点慢,请稍后重试",
   "ssBackToList": "返回列表",
@@ -876,5 +892,18 @@
   "flipHorizontally": "水平翻转",
   "ssUploadFile": "上传文件",
   "ssDragAndDrop": "拖拽文件至此,或点击选择",
-  "ssSupportPptx": "支持.pptx文件"
+  "ssSupportPptx": "支持.pptx文件",
+  "ssFileDetected": "检测到文件:",
+  "ssTotalPages": "共 {count} 页",
+  "ssImportAsSlide": "导入为课件页面",
+  "ssImportAsSlideDesc": "将文件导入为在线课件页面。",
+  "ssImportAndSave": "导入并保存到资源库",
+  "ssImportAndSaveDesc": "导入为课件页面,并同步保存到资源库。",
+  "ssReadingFile": "正在读取文件...",
+  "ssAiChatQuickAction3": "为当前页面内容生成2道选择题",
+  "ssAiChatQuickAction4": "为当前页面生成互动网页",
+  "ssAiChatQuickAction5": "为课件推荐适合页面的互动工具",
+  "ssDragAndDropHint": "点击或拖拽文件到此处上传",
+  "ssSupportHTML": "支持 HTML、HTM 格式",
+  "ssPasteHTML": "请在此处粘贴完整的HTML代码"
 }

+ 31 - 2
src/views/lang/en.json

@@ -708,7 +708,7 @@
   "ssCreateApp": "Create App",
   "ssAddInteractiveWebpage": "Add Interactive Webpage",
   "ssWebpageCenter": "Webpage Center",
-  "ssUploadWebpage": "Upload Webpage",
+  "ssUploadWebpage": "Upload Code",
   "ssCrawlWebpage": "Crawl Webpage",
   "ssAddMultimedia": "Add Multimedia",
   "ssDocument": "Document",
@@ -717,6 +717,7 @@
   "ssWebpageLink": "Webpage Link",
   "ssEnterCompleteUrl": "Please enter complete webpage URL",
   "ssWaitingForInput": "Waiting for input...",
+  "ssWaitingForInput2": "Waiting for upload...",
   "ssInvalidUrl": "URL format is incorrect, please check",
   "ssStartUpload": "Start Upload",
   "ssUploading": "Uploading...",
@@ -729,6 +730,7 @@
   "ssClearToolContent": "This operation will clear the current tool's editing content. Continue?",
   "ssUploadWebpageLink": "Upload Webpage Link",
   "ssCocoLinkTip":"Please add Cocorobo, Amazon, or accessible HTML link.",
+  "ssCocoLinkTip2": "Non-platform domain links may cause display exceptions and do not support student submission of assignments.",
   "ssChoiceQuestion":"Choice Question",
   "ssAnswerCount":"Answer count",
   "ssQuestion":"Question",
@@ -743,10 +745,24 @@
   "ssAiChatShortcut": "Type / for quick actions",
   "ssAiChatQuickAction1": "Generate 2 multiple choice questions for the current page content",
   "ssAiChatQuickAction2": "Generate 1 essay question for the current page content",
+  "ssAiChatQuickAction6": "Generate a poll for the current page",
+  "ssAiChatQuickAction7": "Generate a photo question for the current page",
   "ssAiChatParsing": "Parsing...",
   "ssAiChatWaitUpload": "Please wait for the file upload to complete before sending a message",
   "ssAiChatFileSizeLimit": "File size cannot exceed 10MB",
   "ssAiChatUploadFailed": "File upload failed:",
+  "ssAiChatQuickTitle": "Quick Actions",
+  "ssAiChatQuickSubtitle": "Quickly generate single page content or single element",
+  "ssAiChatQuickTabPage": "For Single Page",
+  "ssAiChatQuickTabCourse": "For Entire Course",
+  "ssAiChatQuickGenChoicesTitle": "Generate 2 Multiple Choice Questions",
+  "ssAiChatQuickGenChoicesDesc": "Generate classroom practice for the current page, supports all tools except Creative Space.",
+  "ssAiChatQuickGenWebTitle": "Generate Interactive Webpage",
+  "ssAiChatQuickGenWebDesc": "Generate an interactive webpage for the current page (e.g., simulator, mini-games, etc.).",
+  "ssAiChatQuickRecommendToolsTitle": "Interactive Tool Recommendations",
+  "ssAiChatQuickRecommendToolsDesc": "Scan course content and recommend interactive tools suitable for pages.",
+  "ssAiChatQuickBatchGenToolsTitle": "Batch Generate Interactive Tools",
+  "ssAiChatQuickBatchGenToolsDesc": "Batch generate interactive tools according to course structure and confirm in the pending area.",
   "ssClassroomAiAssistant": "Classroom AI Assistant",
   "ssRetryMessage": "Network is slow, please try again later",
   "ssBackToList": "Back to List",
@@ -876,5 +892,18 @@
   "flipHorizontally": "Flip Horizontally",
   "ssUploadFile": "Upload File",
   "ssDragAndDrop": "Drag and Drop Files Here, or Click to Select",
-  "ssSupportPptx": "Supports .pptx Files"
+  "ssSupportPptx": "Supports .pptx Files",
+  "ssFileDetected": "File detected: ",
+  "ssTotalPages": "Total {count} pages",
+  "ssImportAsSlide": "Import as course slides",
+  "ssImportAsSlideDesc": "Import the file as online course slides.",
+  "ssImportAndSave": "Import and save to library",
+  "ssImportAndSaveDesc": "Import as course slides and save to library.",
+  "ssReadingFile": "Reading file...",
+  "ssAiChatQuickAction3": "Generate 2 multiple-choice questions for the current page content",
+  "ssAiChatQuickAction4": "Generate an interactive web page for the current page",
+  "ssAiChatQuickAction5": "Recommend interactive tools for the current page",
+  "ssDragAndDropHint": "Click or drag files here to upload",
+  "ssSupportHTML": "Supports HTML, HTM format",
+  "ssPasteHTML": "Please paste the complete HTML code here"
 }

+ 31 - 2
src/views/lang/hk.json

@@ -708,7 +708,7 @@
   "ssCreateApp": "創建應用",
   "ssAddInteractiveWebpage": "添加交互網頁",
   "ssWebpageCenter": "網頁中心",
-  "ssUploadWebpage": "上傳網頁",
+  "ssUploadWebpage": "上傳代碼",
   "ssCrawlWebpage": "爬取網頁",
   "ssAddMultimedia": "添加多媒體",
   "ssDocument": "文檔",
@@ -717,6 +717,7 @@
   "ssWebpageLink": "網頁鏈接",
   "ssEnterCompleteUrl": "請輸入完整的網頁URL地址",
   "ssWaitingForInput": "等待輸入...",
+  "ssWaitingForInput2": "等待上傳...",
   "ssInvalidUrl": "URL格式不正確,請檢查",
   "ssStartUpload": "開始上傳",
   "ssUploading": "上傳中...",
@@ -729,6 +730,7 @@
   "ssClearToolContent": "該操作將清除當前工具的編輯內容,是否繼續?",
   "ssUploadWebpageLink": "上傳鏈接",
   "ssCocoLinkTip":"請添加 Cocorobo 同域、亚马逊或可访问的 HTML 链接。",
+  "ssCocoLinkTip2": "非平台域名支持鏈接,可能存在顯示異常,且不支持學生端提交作業。",
   "ssChoiceQuestion":"選擇題",
   "ssAnswerCount":"回答人數",
   "ssQuestion":"題目",
@@ -743,10 +745,24 @@
   "ssAiChatShortcut": "輸入 / 獲取快捷操作短語",
   "ssAiChatQuickAction1": "為當前頁面內容生成2道選擇題",
   "ssAiChatQuickAction2": "為當前頁面內容生成1道問答題",
+  "ssAiChatQuickAction6": "為當前頁面生成投票",
+  "ssAiChatQuickAction7": "為當前頁面生成拍照題",
   "ssAiChatParsing": "解析中...",
   "ssAiChatWaitUpload": "請等待文件上傳完成後再發送消息",
   "ssAiChatFileSizeLimit": "文件大小不能超過10MB",
   "ssAiChatUploadFailed": "文件上傳失敗:",
+  "ssAiChatQuickTitle": "快捷指令",
+  "ssAiChatQuickSubtitle": "快捷生成單頁內容或單個元素",
+  "ssAiChatQuickTabPage": "針對單個頁面",
+  "ssAiChatQuickTabCourse": "針對完整課件",
+  "ssAiChatQuickGenChoicesTitle": "生成 2 道選擇題",
+  "ssAiChatQuickGenChoicesDesc": "為當前頁生成課堂練習,支持除創作空間外的所有工具。",
+  "ssAiChatQuickGenWebTitle": "生成互動網頁",
+  "ssAiChatQuickGenWebDesc": "為當前頁生成一個交互網頁(如模擬器、小遊戲等)。",
+  "ssAiChatQuickRecommendToolsTitle": "互動工具推薦",
+  "ssAiChatQuickRecommendToolsDesc": "掃描課程內容,推薦適合頁面的互動工具。",
+  "ssAiChatQuickBatchGenToolsTitle": "批量生成互動工具",
+  "ssAiChatQuickBatchGenToolsDesc": "按照課程結構批量生成互動工具,並在待選區確認。",
   "ssClassroomAiAssistant": "課堂AI助手",
   "ssRetryMessage": "網絡稍慢,請稍後重試",
   "ssBackToList": "返回列表",
@@ -876,5 +892,18 @@
   "flipHorizontally": "水平翻轉",
   "ssUploadFile": "上傳文件",
   "ssDragAndDrop": "拖拽文件至此,或點擊選擇",
-  "ssSupportPptx": "支持.pptx文件"
+  "ssSupportPptx": "支持.pptx文件",
+  "ssFileDetected": "檢測到文件:",
+  "ssTotalPages": "共 {count} 頁",
+  "ssImportAsSlide": "導入為課件頁面",
+  "ssImportAsSlideDesc": "將文件導入為在線課件頁面。",
+  "ssImportAndSave": "導入並保存到資源庫",
+  "ssImportAndSaveDesc": "導入為課件頁面,並同步保存到資源庫。",
+  "ssReadingFile": "正在讀取文件...",
+  "ssAiChatQuickAction3": "為當頁內容生成2道選擇題",
+  "ssAiChatQuickAction4": "為當頁生成互動網頁。",
+  "ssAiChatQuickAction5": "為課件推薦適合的互動工具。",
+  "ssDragAndDropHint": "點擊或拖拽文件至此,或點擊選擇",
+  "ssSupportHTML": "支持 HTML、HTM 格式",
+  "ssPasteHTML": "請在此处粘貼完整的HTML代碼"
 }