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

feat(AI聊天): 添加聊天取消功能和快捷操作短语支持

- 在axios配置中添加取消请求功能
- 实现聊天消息的取消发送功能
- 添加快捷操作短语支持及弹出提示
- 优化聊天界面加载状态显示
- 增加文件上传功能(暂隐藏)
- 更新多语言翻译文件
lsc 2 недель назад
Родитель
Сommit
ebe450c0f0

+ 283 - 66
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -7,54 +7,98 @@
                 <div class="message-content user-message chat" v-if="message.content">
                     <div v-html="message.content"></div>
                 </div>
-                <div class="message-content ai-message chat" v-if="message.aiContent">
-                    <div v-html="message.aiContent"></div>
-                    <button class="confirm-btn" v-if="message.jsonData?.gType !== 'chat'" @click="generate(message)">确定</button>
+                <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" v-if="message.jsonData?.gType !== 'chat' && !message.chatloading && message.aiContent"
+                        @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm}}</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="messages.length === 0 ? '例如:创建45分钟的四年级教学内容为水的三态变化的课程' : '输入 / 获取快捷操作短语'"
+                    :placeholder="messages.length === 0 ? lang.ssAiChatExample : lang.ssAiChatShortcut"
                     v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
                 <div class="input-actions">
-                    <button class="attach-btn" v-show="false">
-                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-                            <path
-                                d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48">
-                            </path>
-                        </svg>
-                    </button>
-                    <button class="send-btn" @click="sendMessage">
+                    <FileInput accept="*" @change="handleFileUpload" v-show="false">
+                        <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="messages.length === 0">
             <!-- 快捷操作 -->
             <div class="quick-actions">
-                <button class="quick-action-btn" @click="sendQuickAction('为当前页面内容生成2道选择题')">为当前页面内容生成2道选择题</button>
-                <button class="quick-action-btn" @click="sendQuickAction('为当前页面生成2页内容页面')">为当前页面生成2页内容页面</button>
+                <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn" @click="sendQuickAction(action)">{{ action }}</button>
             </div>
         </div>
     </div>
 </template>
 
 <script lang="ts" setup>
-import { ref, onMounted, useTemplateRef, nextTick } from 'vue'
+import { ref, onMounted, useTemplateRef, nextTick, watch } from 'vue'
 import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2 } from '@/tools/aiChat'
 import { useSlidesStore } from '@/store'
 import { lang } from '@/main'
 import MarkdownIt from 'markdown-it'
 import { getWorkPageId } from '@/services/course'
+import FileInput from '@/components/FileInput.vue'
 
 interface ChatMessage {
     uid?: string
@@ -62,6 +106,9 @@ interface ChatMessage {
     content?: string
     aiContent?: string
     oldContent?: string
+    loading?: boolean
+    chatloading?: boolean
+    gLoading?: boolean
     rawContent?: string
     timestamp?: Date
     like?: boolean
@@ -80,6 +127,11 @@ interface ChatMessage {
         gType?: string
         headUrl?: string
         assistantName?: string
+        files?: Array<{
+            title: string
+            id?: string
+            url?: string
+        }>
         sourceArray?: Array<{
             text?: string
             id?: string
@@ -100,15 +152,37 @@ const inputText = ref('')
 const messages = ref<ChatMessage[]>([])
 const chatSection = useTemplateRef<HTMLElement>('chatSection')
 const chatLoading = ref(false)
+const showQuickActions = ref(false)
+const streamController = ref<{ abort: () => void } | null>(null)
+const noStreamController = ref<{ promise: Promise<string>; abort: () => void } | null>(null)
+const files = ref<Array<{ title: string; id?: string; url?: string }>>([])
+
+// 快捷操作短语数组
+const quickActions = [
+  lang.ssAiChatQuickAction1,
+  lang.ssAiChatQuickAction2,
+]
+
+// 监听输入变化,当输入"/"时显示快捷操作
+watch(inputText, (newValue) => {
+  if (messages.value.length > 0 && newValue === '/') {
+    showQuickActions.value = true
+  }
+  else if (newValue !== '/') {
+    showQuickActions.value = false
+  }
+})
 
 const sendMessage = () => {
-  if (inputText.value.trim()) {
+  if (chatLoading.value) {
+    return
+  }
+  if (inputText.value.trim() || files.value.length > 0) {
     // 添加用户消息
     messages.value.push({
       role: 'user',
-      content: inputText.value
+      content: inputText.value,
     })
-
     // 模拟AI回复
     // setTimeout(() => {
 
@@ -121,12 +195,50 @@ const sendMessage = () => {
     // }, 500)
     prevChatResult()
     messages.value.at(-1).loading = true
+    messages.value.at(-1).chatloading = true
     chatLoading.value = true
     sendAction(inputText.value)
     inputText.value = ''
+    files.value = []
   }
 }
 
+const stopMessage = () => {
+  if (streamController.value) {
+    streamController.value.abort()
+    streamController.value = null
+  }
+  if (noStreamController.value) {
+    noStreamController.value.abort()
+    noStreamController.value = null
+  }
+  chatLoading.value = false
+  if (messages.value.length > 0) {
+    messages.value.at(-1).chatloading = false
+    messages.value.at(-1).loading = false
+  }
+}
+
+// 处理文件上传
+const handleFileUpload = (files2) => {
+  const maxSize = 10 * 1024 * 1024 // 10MB
+  for (let i = 0; i < files2.length; i++) {
+    const file = files2[i]
+    if (file.size > maxSize) {
+      alert('文件大小不能超过10MB')
+      continue
+    }
+    files.value.push({
+      title: file.name
+    })
+  }
+}
+
+// 移除文件
+const removeFile = (index: number) => {
+  files.value.splice(index, 1)
+}
+
 const prevChatResult = () => {
   nextTick(() => {
     if (chatSection.value) {
@@ -136,17 +248,8 @@ const prevChatResult = () => {
 }
 
 const sendQuickAction = (action: string) => {
-  // 添加用户消息
-  messages.value.push({
-    role: 'user',
-    content: action
-  })
-
-  // 模拟AI回复
-  //   setTimeout(() => {
-  //     messages.value.at(-1).aiContent = '请问您想聚焦哪一重点?'
-  //   }, 500)
-  sendAction(action)
+  inputText.value = action
+  sendMessage()
 }
 
 import { v4 as uuidv4 } from 'uuid'
@@ -157,7 +260,14 @@ const gType = ref('chat')
 const sendAction = async (action: string) => {
   const md = new MarkdownIt()
   if (gType.value === 'chat') {
-    const content = await chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
+    const result = chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
+    noStreamController.value = result
+    console.log(result)
+    const content = await result.promise
+    if (!content) {
+      stopMessage()
+      return
+    }
     console.log(content)
     // 渲染 Markdown 格式
     try {
@@ -195,12 +305,16 @@ const sendAction = async (action: string) => {
     }
     else if (event.type === 'messageEnd') {
       messages.value.at(-1).aiContent = md.render(event.data)
+      messages.value.at(-1).chatloading = false
       chatLoading.value = false
       prevChatResult()
     }
-  }, session_name.value).catch(err => {
+  }, session_name.value).then(controller => {
+    streamController.value = controller
+  }).catch(err => {
     chatLoading.value = false
     console.log('err', err)
+    stopMessage()
   })
 }
 import useCreateElement from '@/hooks/useCreateElement'
@@ -209,51 +323,57 @@ const { createSlide } = useSlideHandler()
 const { createFrameElement } = useCreateElement()
 
 const generate = (message: ChatMessage) => {
+  if (message.gLoading) {
+    return
+  }
+  message.gLoading = true
   if (message.jsonData?.gType === 'generate_choice_question') {
     console.log(message.jsonData?.gType)
     const prompt = [
-        {
-          role: 'user',
-          content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成选择题的json。输出一个json格式的回复,格式如下:{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[1],"userAnswer":[],"explanation":"解析"}]}。输出语言为${lang.lang === "en"? "英文" : lang.lang === "hk"? "繁体中文" : "简体中文"}`,
-        },
-      ];
-      chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
-        console.log('选择题', JSON.parse(res));
-        gType.value = 'chat'
-        setPageId(45, res).then(res => {
-            let url = `https://beta.pbl.cocorobo.cn/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${45}`;
-            createSlide()
-            createFrameElement(url, 45)
-        });
-      });
+      {
+        role: 'user',
+        content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成选择题的json。输出一个json格式的回复,格式如下:{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[1],"userAnswer":[],"explanation":"解析"}]}。输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}`,
+      },
+    ]
+    chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
+      console.log('选择题', JSON.parse(res))
+      gType.value = 'chat'
+      setPageId(45, res).then(res => {
+        const url = `https://beta.pbl.cocorobo.cn/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${45}`
+        createSlide()
+        createFrameElement(url, 45)
+        message.gLoading = false
+      })
+    })
   }
   else if (message.jsonData?.gType === 'generate_qa') {
     console.log(message.jsonData?.gType)
-        const prompt = [
-        {
-          role: 'user',
-          content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成问答题的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}。输出语言为${lang.lang === "en"? "英文" : lang.lang === "hk"? "繁体中文" : "简体中文"}`,
-        },
-      ];
-      chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
-        console.log('问答题', JSON.parse(res));
-        gType.value = 'chat'
-            setPageId(15, res).then(res => {
-            let url = `https://beta.pbl.cocorobo.cn/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${15}`;
-            createSlide()
-            createFrameElement(url, 15)
-        });
-      });
+    const prompt = [
+      {
+        role: 'user',
+        content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成问答题的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}。输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}`,
+      },
+    ]
+    chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
+      console.log('问答题', JSON.parse(res))
+      gType.value = 'chat'
+      setPageId(15, res).then(res => {
+        const url = `https://beta.pbl.cocorobo.cn/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${15}`
+        createSlide()
+        createFrameElement(url, 15)
+        message.gLoading = false
+      })
+    })
   }
 }
 
 const setPageId = async (tool: any, json: any) => {
-    const res = await getWorkPageId({
-        userid: props.userid || '',
-        type: tool,
-        json: json
-    })
-    return res[0][0].id
+  const res = await getWorkPageId({
+    userid: props.userid || '',
+    type: tool,
+    json: json
+  })
+  return res[0][0].id
 }
 
 
@@ -283,6 +403,7 @@ onMounted(() => {
     display: flex;
     flex-direction: column;
     gap: 12px;
+    position: relative;
 }
 
 .input-wrapper {
@@ -341,6 +462,57 @@ onMounted(() => {
         color: #F78B22;
         border-radius: 4px;
     }
+
+    input[type="file"] {
+        display: none;
+    }
+}
+
+.file-box {
+    margin-bottom: 8px;
+    min-height: 24px;
+    max-height: 70px;
+    overflow-y: auto;
+
+    .file-item {
+        display: flex;
+        align-items: center;
+        background: #f5f5f5;
+        padding: 4px 8px;
+        border-radius: 4px;
+        margin-bottom: 4px;
+
+        .file-name {
+            flex: 1;
+            font-size: 12px;
+            color: #374151;
+            overflow: hidden;
+            text-overflow: ellipsis;
+            white-space: nowrap;
+        }
+
+        .remove-file-btn {
+            background: none;
+            border: none;
+            cursor: pointer;
+            color: #9CA3AF;
+            font-size: 12px;
+            padding: 2px;
+            margin-left: 8px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+
+            svg {
+                width: 14px;
+                height: 14px;
+            }
+
+            &:hover {
+                color: #EF4444;
+            }
+        }
+    }
 }
 
 .send-btn {
@@ -365,6 +537,15 @@ onMounted(() => {
     &:hover {
         background: #E68A00;
     }
+
+    &.stop {
+        background: unset;
+        width: auto;
+        svg {
+            width: 32px;
+            height: 32px;
+        }
+    }
 }
 
 .quick-actions {
@@ -453,6 +634,11 @@ onMounted(() => {
         background: #fafbfc;
         border: 1.5px solid #e5e7eb;
         border-bottom-left-radius: 2px;
+
+        &>svg {
+            width: 17px;
+            height: 17px;
+        }
     }
 }
 
@@ -500,6 +686,37 @@ ul {
         margin: 4px 0;
     }
 }
+
+.quick-actions-popup {
+    position: absolute;
+    bottom: 100%;
+    left: 0;
+    right: 0;
+    background: white;
+    border: 1px solid #E5E7EB;
+    border-radius: 8px;
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
+    padding: 8px;
+    z-index: 100;
+    margin-bottom: 8px;
+
+    .quick-action-btn {
+        width: 100%;
+        text-align: left;
+        padding: 10px 12px;
+        background: white;
+        border: none;
+        border-radius: 6px;
+        font-size: 14px;
+        color: #374151;
+        cursor: pointer;
+        transition: all 0.2s ease;
+
+        &:hover {
+            background: #F3F4F6;
+        }
+    }
+}
 </style>
 
 <style>

+ 10 - 0
src/services/config.ts

@@ -3,6 +3,7 @@ import message from '@/utils/message'
 import qs from 'qs'
 
 const instance = axios.create({ timeout: 1000 * 300 })
+const cancelToken = axios.CancelToken
 axios.defaults.withCredentials = true
 // POST传参序列化(添加请求拦截器)
 
@@ -66,6 +67,8 @@ instance.interceptors.request.use(
       config.data = encoded
     }
 
+
+
     return config
   },
   (error) => {
@@ -84,6 +87,12 @@ instance.interceptors.response.use(
     return Promise.reject(response)
   },
   (error) => {
+    // 处理请求取消的情况
+    if (axios.isCancel(error)) {
+      console.log('请求被取消:', error.message)
+      return Promise.reject(error)
+    }
+
     const config = error.config
     let fullUrl = '未知请求'
   
@@ -132,3 +141,4 @@ instance.interceptors.response.use(
 )
 
 export default instance
+export { cancelToken }

+ 119 - 97
src/tools/aiChat.ts

@@ -1,4 +1,4 @@
-import axios from '@/services/config'
+import axios, { cancelToken } from '@/services/config'
 import { v4 as uuidv4 } from 'uuid'
 import { fetchEventSource } from '@microsoft/fetch-event-source'
 
@@ -33,42 +33,58 @@ const DEFAULT_PARAMS: Omit<ChatParams, 'message' | 'uid' | 'stream'> = {
   tts_language: 'zh-CN'
 }
 
-export const chat_no_stream = async (msg: string, agentId: string, userId: string, language: string, session_name?: string): Promise<string> => {
-  const agentData = await getAgentModel(agentId)
-  const params: ChatParams = {
-    ...DEFAULT_PARAMS,
-    id: agentId,
-    message: msg,
-    uid: uuidv4(),
-    stream: false,
-    model: agentData?.modelType || 'open-doubao',
-    userId: userId,
-    tts_language: getTtsLanguage(language),
-    session_name: session_name || uuidv4()
-  }
-
-  const res = await axios.post('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', params)
-  let content = res?.message || ''
-  console.log(content)
+export const chat_no_stream = (msg: string, agentId: string, userId: string, language: string, session_name?: string): { promise: Promise<string>; abort: () => void } => {
+  const source = cancelToken.source()
   
-  // 清理可能的 markdown 格式
-  if (content.includes('```json')) {
-    // 提取 ```json 和 ``` 之间的内容
-    const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/)
-    if (jsonMatch) {
-      content = jsonMatch[1].trim()
-    }
-  }
-  else if (content.includes('```')) {
-    // 提取 ``` 和 ``` 之间的内容
-    const codeMatch = content.match(/```\s*([\s\S]*?)\s*```/)
-    if (codeMatch) {
-      content = codeMatch[1].trim()
+  const promise = (async () => {
+    const agentData = await getAgentModel(agentId)
+    const params: ChatParams = {
+      ...DEFAULT_PARAMS,
+      id: agentId,
+      message: msg,
+      uid: uuidv4(),
+      stream: false,
+      model: agentData?.modelType || 'open-doubao',
+      userId: userId,
+      tts_language: getTtsLanguage(language),
+      session_name: session_name || uuidv4()
     }
-  }
 
-  return content
+    try {
+      const res = await axios.post('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', params, {
+        cancelToken: source.token
+      })
+      let content = res?.message || ''
+      console.log(content)
+      
+      // 清理可能的 markdown 格式
+      if (content.includes('```json')) {
+        // 提取 ```json 和 ``` 之间的内容
+        const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/)
+        if (jsonMatch) {
+          content = jsonMatch[1].trim()
+        }
+      }
+      else if (content.includes('```')) {
+        // 提取 ``` 和 ``` 之间的内容
+        const codeMatch = content.match(/```\s*([\s\S]*?)\s*```/)
+        if (codeMatch) {
+          content = codeMatch[1].trim()
+        }
+      }
+
+      return content
+    }
+    catch (error) {
+      console.log(error)
+      return ''
+    }
+  })()
 
+  return {
+    promise,
+    abort: () => source.cancel('Request canceled by user')
+  }
 }
 
 export const chat_stream = async (
@@ -77,8 +93,8 @@ export const chat_stream = async (
   userId: string,
   language: string,
   onMessage: (event: { type: 'message' | 'close' | 'error' | 'messageEnd'; data: string }) => void,
-  session_name?: string
-): Promise<void> => {
+  session_name?: string,
+): Promise<{ abort: () => void }> => {
   const agentData = await getAgentModel(agentId)
   const params: ChatParams = {
     ...DEFAULT_PARAMS,
@@ -94,52 +110,57 @@ export const chat_stream = async (
 
   const ctrl = new AbortController()
   let content = ''
-  try {
-    await fetchEventSource('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', {
-      method: 'POST',
-      body: JSON.stringify(params),
-      signal: ctrl.signal,
-      headers: {
-        'Content-Type': 'application/json',
-      },
-      onmessage(event) {
-        const data = JSON.parse(event.data)
-        if (data.content) {
-          if (data.content != '[DONE]') {
-            content += data.content
-            onMessage({
-              type: 'message',
-              data: content
-            })
-          }
-          else {
-            onMessage({
-              type: 'messageEnd',
-              data: content
-            })
-          }
-
+  
+  // 开始请求
+  fetchEventSource('https://appapi.cocorobo.cn/api/agentchats/ai_agent_chat', {
+    method: 'POST',
+    body: JSON.stringify(params),
+    signal: ctrl.signal,
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    onmessage(event) {
+      const data = JSON.parse(event.data)
+      if (data.content) {
+        if (data.content != '[DONE]') {
+          content += data.content
+          onMessage({
+            type: 'message',
+            data: content
+          })
         }
-      },
-      onclose() {
-        onMessage({
-          type: 'close',
-          data: 'SSE Connection closed'
-        })
-      },
-      onerror(err) {
-        onMessage({
-          type: 'error',
-          data: err.message || 'Unknown error'
-        })
-        // 返回 undefined 以阻止自动重连,如需重连则删除此行
-        throw err
-      },
-    })
-  }
-  finally {
-    ctrl.abort()
+        else {
+          onMessage({
+            type: 'messageEnd',
+            data: content
+          })
+        }
+
+      }
+    },
+    onclose() {
+      onMessage({
+        type: 'close',
+        data: 'SSE Connection closed'
+      })
+    },
+    onerror(err) {
+      onMessage({
+        type: 'error',
+        data: err.message || 'Unknown error'
+      })
+      // 返回 undefined 以阻止自动重连,如需重连则删除此行
+      throw err
+    },
+  }).catch(err => {
+    console.log('err', err)
+  })
+  
+  // 返回 abort 方法
+  return {
+    abort: () => ctrl.abort()
   }
+
 }
 
 export const getAgentModel = async (agentId: string) => {
@@ -165,11 +186,11 @@ const AI_MODEL_CONSTANTS = {
 }
 
 export const chat_no_stream2 = async (prompt: any[] = [], response_format = {
-  "type": "text"
+  'type': 'text'
 }, model = AI_MODEL_CONSTANTS.DEFAULT_MODEL) => {
   return await new Promise((resolve) => {
-    let uid = uuidv4();
-    let data = JSON.stringify({
+    const uid = uuidv4()
+    const data = JSON.stringify({
       model: model,
       temperature: 0,
       max_tokens: 4096,
@@ -181,40 +202,41 @@ export const chat_no_stream2 = async (prompt: any[] = [], response_format = {
       mind_map_question: '',
       stream: false,
       response_format: response_format
-    });
-    let config = {
+    })
+    const config = {
       method: 'post',
       url: 'https://appapi.cocorobo.cn/api/common/chat',
       headers: {
         'Content-Type': 'application/json'
       },
       data: data,
-    };
+    }
     axios(config)
       .then((response) => {
-        let content = response?.FunctionResponse?.choices[0]?.message?.content || '';
+        let content = response?.FunctionResponse?.choices[0]?.message?.content || ''
 
         // 清理可能的 markdown 格式
         if (content.includes('```json')) {
           // 提取 ```json 和 ``` 之间的内容
-          const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/);
+          const jsonMatch = content.match(/```json\s*([\s\S]*?)\s*```/)
           if (jsonMatch) {
-            content = jsonMatch[1].trim();
+            content = jsonMatch[1].trim()
           }
-        } else if (content.includes('```')) {
+        }
+        else if (content.includes('```')) {
           // 提取 ``` 和 ``` 之间的内容
-          const codeMatch = content.match(/```\s*([\s\S]*?)\s*```/);
+          const codeMatch = content.match(/```\s*([\s\S]*?)\s*```/)
           if (codeMatch) {
-            content = codeMatch[1].trim();
+            content = codeMatch[1].trim()
           }
         }
 
-        resolve(content);
+        resolve(content)
       })
-      .catch( (error)=> {
-        console.log(error);
+      .catch( (error) => {
+        console.log(error)
         // this.$message.error('服务器繁忙');
-        resolve(false);
-      });
-  });
+        resolve(false)
+      })
+  })
 }

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

@@ -728,5 +728,9 @@
   "ssUnsubmittedStudents":"未提交学生",
   "ssAnalysis":"分析",
   "ssAIGenerate":"AI生成",
-  "ssUpdateTime":"最后更新"
+  "ssUpdateTime":"最后更新",
+  "ssAiChatExample": "例如:创建45分钟的四年级教学内容为水的三态变化的课程",
+  "ssAiChatShortcut": "输入 / 获取快捷操作短语",
+  "ssAiChatQuickAction1": "为当前页面内容生成2道选择题",
+  "ssAiChatQuickAction2": "为当前页面生成2页内容页面"
 }

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

@@ -728,5 +728,9 @@
   "ssUnsubmittedStudents":"Unsubmitted students",
   "ssAnalysis":"Analysis",
   "ssAIGenerate":"AI Generate",
-  "ssUpdateTime":"Last Updated"
+  "ssUpdateTime":"Last Updated",
+  "ssAiChatExample": "Example: Create a 45-minute lesson plan on water's three states of matter for 4th graders",
+  "ssAiChatShortcut": "Type / for quick actions",
+  "ssAiChatQuickAction1": "Generate 2 multiple choice questions for the current page content",
+  "ssAiChatQuickAction2": "Generate 2 content pages based on the current page"
 }

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

@@ -728,5 +728,9 @@
   "ssUnsubmittedStudents":"未提交學生",
   "ssAnalysis":"分析",
   "ssAIGenerate":"AI生成",
-  "ssUpdateTime":"最後更新"
+  "ssUpdateTime":"最後更新",
+  "ssAiChatExample": "例如:創建45分鐘的四年級教學內容為水的三態變化的課程",
+  "ssAiChatShortcut": "輸入 / 獲取快捷操作短語",
+  "ssAiChatQuickAction1": "為當前頁面內容生成2道選擇題",
+  "ssAiChatQuickAction2": "為當前頁面生成2頁內容頁面"
 }