فهرست منبع

feat(aiChat): add interactive web page generation feature

1. 新增多语言文案支持互动网页创建流程
2. 为AI聊天面板添加互动网页草稿卡片与工作台跳转逻辑
3. 修复组件布局与样式问题,新增弹窗组件处理网页编辑/创建
4. 调整快速操作按钮的代码格式与逻辑判断
lsc 3 هفته پیش
والد
کامیت
7d0bba1956

+ 237 - 27
src/components/CollapsibleToolbar/componets/aiChat.vue

@@ -29,7 +29,8 @@
               @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)">
+            <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>
@@ -43,7 +44,8 @@
               </span>
             </button>
 
-            <!-- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'" @click="sendQuickAction(lang.ssAiChatQuickAction4)">
+            <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>
@@ -55,18 +57,20 @@
                   <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>
-                </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>
+              </span>
             </button>
             <!-- <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'">
                 <span class="coco-quick-action-copy">
@@ -115,8 +119,24 @@
             </circle>
           </svg>
           <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }"
-            v-if="message.jsonData?.gType !== 'chat' && !message.chatloading && message.aiContent"
+            v-if="message.jsonData?.gType !== 'chat' && message.jsonData?.gType !== 'generate_h5page' && !message.chatloading && message.aiContent"
             @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm }}</button>
+          <button type="button" class="coco-interactive-web-card"
+            v-if="message.jsonData?.gType === 'generate_h5page' && !message.chatloading" :class="{ 'loading-state': create_app_loading }" @click="!create_app_loading && handle_add_aiWeb()">
+            <div class="submenu-icon loading-icon" v-show="create_app_loading">
+              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
+                <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
+              </svg>
+            </div>
+            <div class="coco-interactive-web-card-head">
+              <div class="coco-interactive-web-card-title">{{ lang.ssInteractiveWebTitle }}</div>
+              <!-- <span class="coco-interactive-web-card-status">待确认</span> -->
+            </div>
+            <div class="coco-interactive-web-card-desc">{{ lang.ssInteractiveWebDesc }}</div>
+            <div class="coco-interactive-web-card-foot">
+              <span class="coco-interactive-web-card-action">{{ lang.ssOpenWorkspace }}</span>
+            </div>
+          </button>
         </div>
       </div>
     </div>
@@ -135,8 +155,8 @@
             </button>
           </div>
         </div>
-        <textarea class="ai-input" :placeholder="lang.ssAiChatShortcut"
-          v-model="inputText" @keyup.enter.exact="sendMessage" rows="5" />
+        <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">
@@ -178,6 +198,9 @@
           @click="sendQuickAction(action)">{{ action }}</button>
       </div>
     </div>
+
+    <AiWeb :visible="showAiWebModal" :web-id="webId" @close="showAiWebModal = false" @add="addAiWeb" :is-loading="isLoading" />
+
   </div>
 </template>
 
@@ -191,6 +214,7 @@ import { getWorkPageId } from '@/services/course'
 import FileInput from '@/components/FileInput2.vue'
 import axios from '@/services/config'
 import message from '@/utils/message'
+import AiWeb from './aiWeb.vue'
 
 interface ChatMessage {
   uid?: string
@@ -235,8 +259,10 @@ interface ChatMessage {
 
 const props = withDefaults(defineProps<{
   userid?: string | null
+  userJson?: any
 }>(), {
   userid: null,
+  userJson: null,
 })
 
 
@@ -405,7 +431,7 @@ const gType = ref('chat')
 
 const sendAction = async (action: string) => {
   const md = new MarkdownIt()
-  if (gType.value === 'chat') {
+  if (gType.value === 'chat' || gType.value === 'generate_h5page') {
     const result = chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
     noStreamController.value = result
     console.log(result)
@@ -424,12 +450,36 @@ const sendAction = async (action: string) => {
     }
 
     // generate_qa // generate_choice_question // generate_photo // generate_poll
-    if (gType.value !== 'chat') {
+    if (gType.value !== 'chat' && gType.value !== 'generate_h5page') {
       messages.value.at(-1).jsonData = {
         gType: gType.value,
         isGenerate: false
       }
     }
+    else if (gType.value === 'generate_h5page') {
+      messages.value.at(-1).jsonData = {
+        gType: gType.value,
+        isGenerate: false
+      }
+      messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
+      messages.value.at(-1).loading = false
+      messages.value.at(-1).chatloading = false
+      chatLoading.value = false
+      return
+    }
+
+    if (content.includes(lang.ssAiChatQuickGenWebTitle)) {
+      messages.value.at(-1).jsonData = {
+        gType: 'generate_h5page',
+        isGenerate: false
+      }
+      messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
+      messages.value.at(-1).loading = false
+      chatLoading.value = false
+      messages.value.at(-1).chatloading = false
+
+      return
+    }
   }
   else {
     messages.value.at(-1).jsonData = {
@@ -666,7 +716,80 @@ const setCocoQuickTab = (tab: string) => {
   cocoQuickTab.value = tab
 }
 
+// AI Web 相关状态
+const create_app_loading = ref(false)
+const webId = ref('')
+const showAiWebModal = ref(false)
+const isEdit = ref(false)
+import { storeToRefs } from 'pinia'
+import _ from 'lodash'
+const { currentSlide } = storeToRefs(slidesStore)
+
+const handle_add_aiWeb = _.throttle(async () => {
+  create_app_loading.value = true
+  try {
+    const elements = currentSlide.value?.elements || []
+    console.log(elements)
+    const existingWebElement = elements.find((el: any) => el.toolType === 73 && el.webId && el.type === 'frame')
+    
+    if (existingWebElement && existingWebElement.webId) {
+      webId.value = existingWebElement.webId
+      create_app_loading.value = false
+      showAiWebModal.value = true
+      isEdit.value = true
+      return
+    }
+
+    const res = await axios.post('https://appapi.cocorobo.cn/api/agents/ai_edit', {
+      userid: props.userid || '',
+      username: props.userJson.name || '',
+      muti_name: lang.ssAddWebH5,
+      description: lang.ssAddWebH5,
+      headUrl: 'https://ccrb.s3.cn-northwest-1.amazonaws.com.cn/default%2F%E9%BB%98%E8%AE%A4%E5%A4%B4%E5%83%8F51741770599274.svg',
+      organizeid: props.userJson.organizeid || '',
+      content: ''
+    })
+    console.log(res)
+    webId.value = res.id || ''
 
+    create_app_loading.value = false
+    showAiWebModal.value = true
+  }
+  catch (error) {
+    console.log(error)
+    create_app_loading.value = false
+  }
+}, 3000)
+
+const isLoading = ref(false)
+import useImport from '@/hooks/useImport'
+const { uploadFileToS3 } = useImport()
+
+const addAiWeb = async (code: string) => {
+  if (!code) return
+  const file = new File([code], 'index.html', { type: 'text/html' })
+  isLoading.value = true
+  const url = await uploadFileToS3(file)
+  if (isEdit.value) {
+    const elements = currentSlide.value?.elements || []
+    const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 73))
+    if (frameElement) {
+      slidesStore.updateElement({
+        id: frameElement.id,
+        props: { url: url }
+      })
+    }
+  }
+  else {
+    createSlide()
+    createFrameElement(url, 73, webId.value)
+  }
+  
+  isLoading.value = false
+  showAiWebModal.value = false
+  webId.value = ''
+  isEdit.value = false
+}
 
 onMounted(() => {
   session_name.value = uuidv4()
@@ -1100,11 +1223,13 @@ ul {
 
     .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);
@@ -1118,6 +1243,7 @@ ul {
           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;
@@ -1131,6 +1257,7 @@ ul {
         display: flex;
         flex-direction: column;
         gap: 8px;
+
         .coco-quick-action {
           width: 100%;
           display: flex;
@@ -1146,24 +1273,26 @@ ul {
           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;
+              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);
+              font-size: 12px;
+              line-height: 1.6;
+              color: rgb(139, 115, 86);
             }
           }
 
@@ -1179,6 +1308,87 @@ ul {
     }
   }
 }
+
+.coco-interactive-web-card {
+  width: 100%;
+  margin-top: 10px;
+  display: flex;
+  flex-direction: column;
+  text-align: left;
+  cursor: pointer;
+  border-width: 1px;
+  border-style: solid;
+  border-color: rgb(247, 197, 143);
+  border-image: initial;
+  border-radius: 14px;
+  background: linear-gradient(rgb(255, 250, 243) 0%, rgb(255, 255, 255) 100%);
+  padding: 12px 14px;
+  gap: 8px;
+  transition: 0.2s;
+  position: relative;
+  overflow: hidden;
+
+  &.loading-state {
+    opacity: 0.7;
+    cursor: not-allowed;
+    pointer-events: none;
+
+    .submenu-icon {
+      animation: pulse 1s infinite;
+    }
+
+    .spin {
+      animation: spin 1s linear infinite;
+    }
+
+    .submenu-icon {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      background: #00000026;
+      top: 0;
+      left: 0;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      svg {
+        height: 30px;
+        color: #fff;
+      }
+    }
+  }
+
+  .coco-interactive-web-card-head,
+  .coco-interactive-web-card-foot {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 10px;
+
+    .coco-interactive-web-card-title {
+      min-width: 0px;
+      font-size: 14px;
+      font-weight: 700;
+      color: rgb(17, 24, 39);
+      line-height: 1.4;
+      flex: 1 1 0%;
+    }
+
+    .coco-interactive-web-card-action {
+      margin-left: auto;
+      font-size: 12px;
+      font-weight: 700;
+      color: rgb(240, 120, 21);
+    }
+  }
+
+  .coco-interactive-web-card-desc {
+    font-size: 12px;
+    color: rgb(107, 114, 128);
+    line-height: 1.6;
+  }
+}
 </style>
 
 <style>

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

@@ -115,7 +115,7 @@
         </div>
       </div>
       <div class="submenu-content">
-        <AiChat :userid="props.userid" />
+        <AiChat :userid="props.userid" :userJson="userJson" />
       </div>
     </div>
     <div class="submenu" :class="{ visible: activeSubmenu === 'uploadFile' }">
@@ -1443,6 +1443,7 @@ const addAiWeb = async (code: string) => {
   background: #fff;
   border-right: 1px solid #e5e7eb;
   transition: width 0.3s ease;
+  z-index: 1000;
 }
 
 .toolbar-content {

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

@@ -924,5 +924,9 @@
   "ssDownloadLoading": "下载中...",
   "ssBatchDownload": "批量下载",
   "ssClipboardEmpty": "剪贴板为空或者不包含文本",
-  "ssClipboardPermission": "浏览器不支持或禁止访问剪贴板,请使用快捷键 Ctrl + V"
+  "ssClipboardPermission": "浏览器不支持或禁止访问剪贴板,请使用快捷键 Ctrl + V",
+  "ssInteractiveWebTitle": "当前页面互动网页",
+  "ssInteractiveWebDesc": "已生成互动网页草稿,点击打开工作台后可确认新增为交互网页页面。",
+  "ssOpenWorkspace": "点击打开工作台",
+  "ssInteractiveWebWorkspaceDesc": "已生成互动网页草稿,点击卡片进入工作台。"
 }

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

@@ -924,5 +924,9 @@
   "ssDownloadLoading": "Downloading...",
   "ssBatchDownload": "Batch Download",
   "ssClipboardEmpty": "Clipboard is empty or does not contain text",
-  "ssClipboardPermission": "Browser does not support or blocks access to clipboard, please use Ctrl + V to paste code"
+  "ssClipboardPermission": "Browser does not support or blocks access to clipboard, please use Ctrl + V to paste code",
+  "ssInteractiveWebTitle": "Current Page Interactive Web",
+  "ssInteractiveWebDesc": "A draft interactive web page has been generated. Click to confirm and add it to the web page.",
+  "ssOpenWorkspace": "Open Workspace",
+  "ssInteractiveWebWorkspaceDesc": "Click to enter the workspace to confirm the interactive web page."
 }

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

@@ -924,5 +924,9 @@
   "ssDownloadLoading": "下載中...",
   "ssBatchDownload": "批量下載",
   "ssClipboardEmpty": "剪貼板空或不包含文本",
-  "ssClipboardPermission": "瀏覽器不支持或禁止訪問剪貼板,請使用快捷鍵 Ctrl + V 粘貼代碼"
+  "ssClipboardPermission": "瀏覽器不支持或禁止訪問剪貼板,請使用快捷鍵 Ctrl + V 粘貼代碼",
+  "ssInteractiveWebTitle": "當前頁面互動網頁",
+  "ssInteractiveWebDesc": "已生成互動網頁草稿,點擊打開工作台後可確認新增為互動網頁頁面。",
+  "ssOpenWorkspace": "打開工作台",
+  "ssInteractiveWebWorkspaceDesc": "點擊卡片進入工作台,確認互動網頁。"
 }