Explorar o código

feat(ai-chat): add quick action panel and optimize related config

1. 新增多语言快捷指令文案,支持中英文繁体简体
2. 重构AI聊天组件,添加快捷指令折叠面板
3. 修复WebSocket连接配置,取消注释并启用token认证
4. 临时隐藏侧边栏页面菜单选项
lsc hai 1 semana
pai
achega
05ba72d531

+ 678 - 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'">
+                <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,
 })
@@ -174,7 +258,8 @@ const quickActions = [
 
 // 监听输入变化,当输入"/"时显示快捷操作
 watch(inputText, (newValue) => {
-  if (messages.value.length > 0 && newValue === '/') {
+  // messages.value.length > 0 && 
+  if (newValue === '/') {
     showQuickActions.value = true
   }
   else if (newValue !== '/') {
@@ -242,7 +327,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 +366,10 @@ const handleFileUpload = async (files2: File[]) => {
         files.value.splice(fileIndex, 1)
       }
     })
-    
+
     uploadPromises.push(uploadPromise)
   }
-  
+
   // 等待所有文件上传完成
   await Promise.allSettled(uploadPromises)
 }
@@ -367,7 +452,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 +567,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 +589,7 @@ const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
         signal: signal
       }
     )
-    
+
     console.log(res)
     return res
   }
@@ -522,6 +607,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 +633,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>

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

@@ -21,7 +21,7 @@
           <span class="item-label">{{ lang.ssUploadFile }}</span>
         </div>
         <div class="sidebar-divider"></div>
-        <div class="sidebar-item" :class="{ active: activeSubmenu === 'page' }" @click="toggleSubmenu('page')">
+        <!-- <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"
@@ -31,7 +31,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">

+ 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'
 
 /**
  * 获取课程详情

+ 13 - 13
src/views/Student/index.vue

@@ -3922,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
@@ -3940,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)

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

@@ -746,6 +746,18 @@
   "ssAiChatWaitUpload": "请等待文件上传完成后再发送消息",
   "ssAiChatFileSizeLimit": "文件大小不能超过10MB",
   "ssAiChatUploadFailed": "文件上传失败:",
+  "ssAiChatQuickTitle": "快捷指令",
+  "ssAiChatQuickSubtitle": "快捷生成单页内容或单个元素",
+  "ssAiChatQuickTabPage": "针对单个页面",
+  "ssAiChatQuickTabCourse": "针对完整课件",
+  "ssAiChatQuickGenChoicesTitle": "生成 2 道选择题",
+  "ssAiChatQuickGenChoicesDesc": "为当前页生成课堂练习,支持除创作空间外的所有工具。",
+  "ssAiChatQuickGenWebTitle": "生成互动网页",
+  "ssAiChatQuickGenWebDesc": "为当前页生成一个交互网页(如模拟器、小游戏等)。",
+  "ssAiChatQuickRecommendToolsTitle": "互动工具推荐",
+  "ssAiChatQuickRecommendToolsDesc": "扫描课程内容,推荐适合页面的互动工具。",
+  "ssAiChatQuickBatchGenToolsTitle": "批量生成互动工具",
+  "ssAiChatQuickBatchGenToolsDesc": "按照课程结构批量生成互动工具,并在待选区确认。",
   "ssClassroomAiAssistant": "课堂AI助手",
   "ssRetryMessage": "网络有点慢,请稍后重试",
   "ssBackToList": "返回列表",
@@ -883,5 +895,7 @@
   "ssImportAsSlideDesc": "将文件导入为在线课件页面。",
   "ssImportAndSave": "导入并保存到资源库",
   "ssImportAndSaveDesc": "导入为课件页面,并同步保存到资源库。",
-  "ssReadingFile": "正在读取文件..."
+  "ssReadingFile": "正在读取文件...",
+  "ssAiChatQuickAction3": "为当前页面内容生成2道选择题",
+  "ssAiChatQuickAction4": "为当前页面生成互动网页"
 }

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

@@ -747,6 +747,18 @@
   "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",
@@ -883,5 +895,7 @@
   "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..."
+  "ssReadingFile": "Reading file...",
+  "ssAiChatQuickAction3": "Generate 2 multiple-choice questions for the current page content",
+  "ssAiChatQuickAction4": "Generate an interactive web page for the current page"
 }

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

@@ -747,6 +747,18 @@
   "ssAiChatWaitUpload": "請等待文件上傳完成後再發送消息",
   "ssAiChatFileSizeLimit": "文件大小不能超過10MB",
   "ssAiChatUploadFailed": "文件上傳失敗:",
+  "ssAiChatQuickTitle": "快捷指令",
+  "ssAiChatQuickSubtitle": "快捷生成單頁內容或單個元素",
+  "ssAiChatQuickTabPage": "針對單個頁面",
+  "ssAiChatQuickTabCourse": "針對完整課件",
+  "ssAiChatQuickGenChoicesTitle": "生成 2 道選擇題",
+  "ssAiChatQuickGenChoicesDesc": "為當前頁生成課堂練習,支持除創作空間外的所有工具。",
+  "ssAiChatQuickGenWebTitle": "生成互動網頁",
+  "ssAiChatQuickGenWebDesc": "為當前頁生成一個交互網頁(如模擬器、小遊戲等)。",
+  "ssAiChatQuickRecommendToolsTitle": "互動工具推薦",
+  "ssAiChatQuickRecommendToolsDesc": "掃描課程內容,推薦適合頁面的互動工具。",
+  "ssAiChatQuickBatchGenToolsTitle": "批量生成互動工具",
+  "ssAiChatQuickBatchGenToolsDesc": "按照課程結構批量生成互動工具,並在待選區確認。",
   "ssClassroomAiAssistant": "課堂AI助手",
   "ssRetryMessage": "網絡稍慢,請稍後重試",
   "ssBackToList": "返回列表",
@@ -883,5 +895,7 @@
   "ssImportAsSlideDesc": "將文件導入為在線課件頁面。",
   "ssImportAndSave": "導入並保存到資源庫",
   "ssImportAndSaveDesc": "導入為課件頁面,並同步保存到資源庫。",
-  "ssReadingFile": "正在讀取文件..."
+  "ssReadingFile": "正在讀取文件...",
+  "ssAiChatQuickAction3": "為當頁內容生成2道選擇題",
+  "ssAiChatQuickAction4": "為當頁生成互動網頁。"
 }