aiChat.vue 62 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989
  1. <template>
  2. <div class="ai-chat-container">
  3. <div class="message-quick-box">
  4. <div class="message-quick-box-item" :class="{ 'active': isQuickActions.includes('quick') }">
  5. <button type="button" class="coco-collapse-trigger" @click="toggleQuickActions('quick')">
  6. <span class="coco-collapse-icon soft-orange">
  7. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"
  8. stroke-linecap="round">
  9. <path d="M4 7h16"></path>
  10. <path d="M4 12h10"></path>
  11. <path d="M4 17h7"></path>
  12. </svg>
  13. </span>
  14. <span class="coco-collapse-copy">
  15. <span class="coco-collapse-title">{{ lang.ssAiChatQuickTitle }}</span>
  16. <span class="coco-collapse-subtitle" id="cocoQuickSectionHint">{{ lang.ssAiChatQuickSubtitle }}</span>
  17. </span>
  18. <span class="coco-collapse-chevron">
  19. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  20. <polyline points="6 9 12 15 18 9"></polyline>
  21. </svg>
  22. </span>
  23. </button>
  24. <div class="coco-collapse-body" v-if="isQuickActions.includes('quick')">
  25. <div class="coco-quick-tabs">
  26. <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'page' }"
  27. @click="setCocoQuickTab('page')">{{ lang.ssAiChatQuickTabPage }}</button>
  28. <button type="button" class="coco-quick-tab" :class="{ 'active': cocoQuickTab === 'course' }"
  29. @click="setCocoQuickTab('course')">{{ lang.ssAiChatQuickTabCourse }}</button>
  30. </div>
  31. <div class="coco-quick-list">
  32. <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'"
  33. @click="sendQuickAction(lang.ssAiChatQuickAction3, 1)">
  34. <span class="coco-quick-action-copy">
  35. <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenChoicesTitle }}</span>
  36. <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenChoicesDesc }}</span>
  37. </span>
  38. <span class="coco-quick-action-arrow">
  39. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
  40. stroke-linecap="round" stroke-linejoin="round">
  41. <path d="M5 12h14"></path>
  42. <path d="M13 5l7 7-7 7"></path>
  43. </svg>
  44. </span>
  45. </button>
  46. <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'page'"
  47. @click="sendQuickAction(lang.ssAiChatQuickAction4, 1)">
  48. <span class="coco-quick-action-copy">
  49. <span class="coco-quick-action-title">{{ lang.ssAiChatQuickGenWebTitle }}</span>
  50. <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickGenWebDesc }}</span>
  51. </span>
  52. <span class="coco-quick-action-arrow">
  53. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
  54. stroke-linecap="round" stroke-linejoin="round">
  55. <path d="M5 12h14"></path>
  56. <path d="M13 5l7 7-7 7"></path>
  57. </svg>
  58. </span>
  59. </button>
  60. <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'"
  61. @click="sendQuickAction(lang.ssAiChatQuickAction5, 1)">
  62. <span class="coco-quick-action-copy">
  63. <span class="coco-quick-action-title">{{ lang.ssAiChatQuickRecommendToolsTitle }}</span>
  64. <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickRecommendToolsDesc }}</span>
  65. </span>
  66. <span class="coco-quick-action-arrow">
  67. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
  68. stroke-linecap="round" stroke-linejoin="round">
  69. <path d="M5 12h14"></path>
  70. <path d="M13 5l7 7-7 7"></path>
  71. </svg>
  72. </span>
  73. </button>
  74. <button type="button" class="coco-quick-action" v-if="cocoQuickTab === 'course'" @click="sendQuickAction(lang.ssBatchInteractiveWeb, 1)">
  75. <span class="coco-quick-action-copy">
  76. <span class="coco-quick-action-title">{{ lang.ssAiChatQuickBatchGenToolsTitle }}</span>
  77. <span class="coco-quick-action-desc">{{ lang.ssAiChatQuickBatchGenToolsDesc }}</span>
  78. </span>
  79. <span class="coco-quick-action-arrow">
  80. <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  81. <path d="M5 12h14"></path>
  82. <path d="M13 5l7 7-7 7"></path>
  83. </svg>
  84. </span>
  85. </button>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. <!-- 聊天区域 -->
  91. <div class="chat-section" ref="chatSection">
  92. <!-- 消息列表 -->
  93. <div v-for="(message, index) in messages" :key="index" class="chat-message">
  94. <div class="message-content user-message chat" v-if="message.content">
  95. <div v-html="message.content"></div>
  96. <!-- 显示上传的文件 -->
  97. <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
  98. <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
  99. <span>{{ file.title }}</span>
  100. </div>
  101. </div>
  102. </div>
  103. <!-- :class="{'loading-state': message.chatloading && message.jsonData?.gType === 'trigger_batch_tool'}" -->
  104. <div class="message-content ai-message chat" v-if="message.aiContent || message.chatloading">
  105. <!-- <div class="submenu-icon loading-icon" v-show="message.chatloading && message.jsonData?.gType === 'trigger_batch_tool'">
  106. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
  107. <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
  108. </svg>
  109. </div> -->
  110. <div v-if="message.aiContent" v-html="message.aiContent"></div>
  111. <svg v-else xmlns="http://www.w3.org/2000/svg" width="32" height="32"
  112. viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
  113. <circle cx="4" cy="12" r="3" fill="currentColor">
  114. <animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s"
  115. calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
  116. </circle>
  117. <circle cx="12" cy="12" r="3" fill="currentColor">
  118. <animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline" dur="0.6s"
  119. keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
  120. </circle>
  121. <circle cx="20" cy="12" r="3" fill="currentColor">
  122. <animate id="svgSpinners3DotsBounce1" attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.2s"
  123. calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
  124. </circle>
  125. </svg>
  126. <button class="confirm-btn" :class="{ disabled: message.jsonData?.isGenerate }"
  127. v-if="message.jsonData?.gType !== 'chat' && message.jsonData?.gType !== 'generate_interactive_web' && message.jsonData?.gType !== 'trigger_tool_recommend' && message.jsonData?.gType !== 'trigger_batch_tool' && !message.chatloading && message.aiContent"
  128. @click="generate(message)">{{ message.gLoading ? lang.ssLoading : lang.ssConfirm }}</button>
  129. <button type="button" class="coco-interactive-web-card"
  130. v-if="message.jsonData?.gType === 'generate_interactive_web' && !message.chatloading" :class="{ 'loading-state': create_app_loading }" @click="!create_app_loading && handle_add_aiWeb(message)">
  131. <div class="submenu-icon loading-icon" v-show="create_app_loading">
  132. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="spin">
  133. <circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="12"></circle>
  134. </svg>
  135. </div>
  136. <div class="coco-interactive-web-card-head">
  137. <div class="coco-interactive-web-card-title">{{ lang.ssInteractiveWebTitle }}</div>
  138. <!-- <span class="coco-interactive-web-card-status">待确认</span> -->
  139. </div>
  140. <div class="coco-interactive-web-card-desc">{{ lang.ssInteractiveWebDesc }}</div>
  141. <div class="coco-interactive-web-card-foot">
  142. <span class="coco-interactive-web-card-action">{{ lang.ssOpenWorkspace }}</span>
  143. </div>
  144. </button>
  145. </div>
  146. </div>
  147. </div>
  148. <!-- 输入区域 -->
  149. <div class="input-section">
  150. <div class="input-wrapper">
  151. <div class="file-box" v-show="files.length">
  152. <div v-for="(file, index) in files" :key="index" class="file-item">
  153. <span class="file-name">{{ file.title }}</span>
  154. <button class="remove-file-btn" @click="removeFile(index)">
  155. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  156. stroke-linejoin="round">
  157. <line x1="18" y1="6" x2="6" y2="18"></line>
  158. <line x1="6" y1="6" x2="18" y2="18"></line>
  159. </svg>
  160. </button>
  161. </div>
  162. </div>
  163. <textarea class="ai-input" :placeholder="lang.ssAiChatShortcut" v-model="inputText"
  164. @keyup.enter.exact="sendMessage" rows="5" />
  165. <div class="input-actions">
  166. <FileInput accept="*" @change="handleFileUpload">
  167. <button class="attach-btn">
  168. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  169. <path
  170. 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">
  171. </path>
  172. </svg>
  173. </button>
  174. </FileInput>
  175. <button class="send-btn" @click="sendMessage" v-if="!chatLoading">
  176. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  177. stroke-linejoin="round">
  178. <line x1="22" y1="2" x2="11" y2="13"></line>
  179. <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
  180. </svg>
  181. </button>
  182. <button class="send-btn stop" @click="stopMessage" v-if="chatLoading">
  183. <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
  184. <rect width="32" height="32" rx="16" fill="black" fill-opacity="0.4"></rect>
  185. <path
  186. 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"
  187. fill="white" fill-opacity="0.9"></path>
  188. </svg>
  189. </button>
  190. </div>
  191. </div>
  192. <!-- 输入时的快捷操作弹出 -->
  193. <div class="quick-actions-popup" v-if="showQuickActions">
  194. <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
  195. @click="sendQuickAction(action)">{{ action }}</button>
  196. </div>
  197. </div>
  198. <!-- 初始状态 -->
  199. <div class="initial-state" v-if="false">
  200. <!-- 快捷操作 -->
  201. <div class="quick-actions">
  202. <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
  203. @click="sendQuickAction(action)">{{ action }}</button>
  204. </div>
  205. </div>
  206. <AiWeb :visible="showAiWebModal" :web-id="webId" @close="showAiWebModal = false" :tx="webTx" @add="addAiWeb" :is-loading="isLoading" />
  207. </div>
  208. </template>
  209. <script lang="ts" setup>
  210. import { ref, onMounted, useTemplateRef, nextTick, watch } from 'vue'
  211. import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2 } from '@/tools/aiChat'
  212. import { useSlidesStore, useMainStore } from '@/store'
  213. import { lang } from '@/main'
  214. import MarkdownIt from 'markdown-it'
  215. import { getWorkPageId } from '@/services/course'
  216. import FileInput from '@/components/FileInput2.vue'
  217. import axios from '@/services/config'
  218. import message from '@/utils/message'
  219. import AiWeb from './aiWeb.vue'
  220. import katex from 'katex'
  221. import 'katex/dist/katex.min.css'
  222. interface ChatMessage {
  223. uid?: string
  224. role: 'ai' | 'user'
  225. content?: string
  226. aiContent?: string
  227. oldContent?: string
  228. loading?: boolean
  229. chatloading?: boolean
  230. gLoading?: boolean
  231. rawContent?: string
  232. timestamp?: Date
  233. like?: boolean
  234. unlike?: boolean
  235. isTyping?: boolean
  236. AI?: string
  237. isShowSynchronization?: boolean
  238. filename?: string
  239. is_mind_map?: boolean
  240. sourceFiles?: Array<{
  241. title: string
  242. id?: string
  243. url?: string
  244. }>
  245. jsonData?: {
  246. gType?: string
  247. isGenerate?: boolean
  248. headUrl?: string
  249. assistantName?: string
  250. files?: Array<{
  251. title: string
  252. id?: string
  253. url?: string
  254. }>
  255. sourceArray?: Array<{
  256. text?: string
  257. id?: string
  258. title?: string
  259. }>
  260. aiContent?: string
  261. }
  262. }
  263. const props = withDefaults(defineProps<{
  264. userid?: string | null
  265. userJson?: any
  266. courseTitle?: string | null
  267. }>(), {
  268. userid: null,
  269. userJson: null,
  270. courseTitle: null,
  271. })
  272. const inputText = ref('')
  273. const messages = ref<ChatMessage[]>([])
  274. const chatSection = useTemplateRef<HTMLElement>('chatSection')
  275. const chatLoading = ref(false)
  276. const showQuickActions = ref(false)
  277. const streamController = ref<{ abort: () => void } | null>(null)
  278. const noStreamController = ref<{ promise: Promise<string>; abort: () => void } | null>(null)
  279. const files = ref<Array<{ title: string; id?: string | null; url?: string; isProcessing?: boolean; cancel?: () => void }>>([])
  280. // 快捷操作短语数组
  281. const quickActions = [
  282. lang.ssAiChatQuickAction1,
  283. lang.ssAiChatQuickAction2,
  284. lang.ssAiChatQuickAction6,
  285. lang.ssAiChatQuickAction7,
  286. lang.ssAiChatQuickAction4,
  287. lang.ssAiChatQuickAction8,
  288. lang.ssAiChatQuickAction9,
  289. ]
  290. // 监听输入变化,当输入"/"时显示快捷操作
  291. watch(inputText, (newValue) => {
  292. // messages.value.length > 0 &&
  293. if (newValue === '/') {
  294. showQuickActions.value = true
  295. }
  296. else if (newValue !== '/') {
  297. showQuickActions.value = false
  298. }
  299. })
  300. const sendMessage = () => {
  301. if (chatLoading.value) {
  302. return
  303. }
  304. // 检查是否有文件正在处理中
  305. const hasProcessingFile = files.value.some(file => file.isProcessing)
  306. if (hasProcessingFile) {
  307. message.error(lang.ssAiChatWaitUpload)
  308. return
  309. }
  310. if (inputText.value.trim() || files.value.length > 0) {
  311. isQuickActions.value = []
  312. // 添加用户消息
  313. messages.value.push({
  314. role: 'user',
  315. content: inputText.value,
  316. })
  317. // 模拟AI回复
  318. // setTimeout(() => {
  319. // setTimeout(() => {
  320. // messages.value.at(-1).aiContent = '课程生成完成!为您创建了5个内容页面和3个互动工具。您可以查看底部课程大纲,或在中央区域开始编辑。',
  321. // messages.value.at(-1).jsonData = {
  322. // isChoice: true
  323. // }
  324. // }, 1000)
  325. // }, 500)
  326. prevChatResult()
  327. messages.value.at(-1).loading = true
  328. messages.value.at(-1).chatloading = true
  329. messages.value.at(-1).sourceFiles = files.value.filter(file => file.id !== null).map(file => ({
  330. title: file.title,
  331. id: file.id
  332. }))
  333. chatLoading.value = true
  334. sendAction(inputText.value)
  335. inputText.value = ''
  336. files.value = []
  337. }
  338. }
  339. const stopMessage = () => {
  340. if (streamController.value) {
  341. streamController.value.abort()
  342. streamController.value = null
  343. }
  344. if (noStreamController.value) {
  345. noStreamController.value.abort()
  346. noStreamController.value = null
  347. }
  348. chatLoading.value = false
  349. if (messages.value.length > 0) {
  350. messages.value.at(-1).chatloading = false
  351. messages.value.at(-1).loading = false
  352. }
  353. if (loadingInterval.value) {
  354. clearInterval(loadingInterval.value)
  355. loadingInterval.value = null
  356. }
  357. }
  358. // 处理文件上传
  359. const handleFileUpload = async (files2: File[]) => {
  360. const maxSize = 10 * 1024 * 1024 // 10MB
  361. const uploadPromises = []
  362. for (let i = 0; i < files2.length; i++) {
  363. const file = files2[i]
  364. if (file.size > maxSize) {
  365. message.error(lang.ssAiChatFileSizeLimit)
  366. continue
  367. }
  368. // 先添加文件到列表,显示解析中状态
  369. const fileIndex = files.value.length
  370. files.value.push({
  371. title: file.name + ' (' + lang.ssAiChatParsing + ')',
  372. id: null,
  373. isProcessing: true,
  374. cancel: null
  375. })
  376. // 创建取消控制器
  377. const controller = new AbortController()
  378. files.value[fileIndex].cancel = () => {
  379. controller.abort()
  380. files.value.splice(fileIndex, 1)
  381. }
  382. // 创建上传Promise并添加到数组
  383. const uploadPromise = uploadFile2(file, controller.signal).then(res => {
  384. if (!res) {
  385. files.value.splice(fileIndex, 1)
  386. return
  387. }
  388. // 上传成功,更新文件状态
  389. files.value[fileIndex] = {
  390. title: file.name,
  391. id: res.results.document_id,
  392. isProcessing: false
  393. }
  394. }).catch(error => {
  395. if (error.name !== 'AbortError') {
  396. console.error(lang.ssAiChatUploadFailed, error)
  397. files.value.splice(fileIndex, 1)
  398. }
  399. })
  400. uploadPromises.push(uploadPromise)
  401. }
  402. // 等待所有文件上传完成
  403. await Promise.allSettled(uploadPromises)
  404. }
  405. // 移除文件
  406. const removeFile = (index: number) => {
  407. const file = files.value[index]
  408. if (file && file.isProcessing && file.cancel) {
  409. file.cancel()
  410. }
  411. files.value.splice(index, 1)
  412. }
  413. const prevChatResult = () => {
  414. nextTick(() => {
  415. if (chatSection.value) {
  416. chatSection.value.scrollTop = chatSection.value.scrollHeight
  417. }
  418. })
  419. }
  420. const sendQuickAction = (action: string, type: number = 2) => {
  421. inputText.value = action
  422. if (type === 1) {
  423. isQuickActions.value = []
  424. }
  425. // sendMessage()
  426. }
  427. import { v4 as uuidv4 } from 'uuid'
  428. const session_name = ref('')
  429. const slidesStore = useSlidesStore()
  430. const mainStore = useMainStore()
  431. const gType = ref('chat')
  432. const loadingDots = ref('...')
  433. const loadingInterval = ref(null)
  434. const sendAction = async (action: string) => {
  435. // html: true 保留 katexZ 生成的公式HTML,使其能正常渲染到页面
  436. const md = new MarkdownIt({ html: true })
  437. if (gType.value === 'chat' || gType.value === 'generate_interactive_web' || gType.value === 'trigger_tool_recommend' || gType.value === 'trigger_batch_tool') {
  438. const result = chat_no_stream(action, agentid1.value, props.userid || '', lang.lang)
  439. noStreamController.value = result
  440. console.log(result)
  441. const content = await result.promise
  442. if (!content) {
  443. stopMessage()
  444. return
  445. }
  446. console.log(content)
  447. // 渲染 Markdown 格式
  448. try {
  449. gType.value = JSON.parse(content).intent
  450. }
  451. catch (error) {
  452. gType.value = 'chat'
  453. }
  454. // generate_qa // generate_choice_question // generate_take_photo // generate_poll
  455. if (gType.value !== 'chat' && gType.value !== 'generate_interactive_web' && gType.value !== 'trigger_batch_tool') {
  456. messages.value.at(-1).jsonData = {
  457. gType: gType.value,
  458. isGenerate: false
  459. }
  460. }
  461. else if (gType.value === 'generate_interactive_web') {
  462. messages.value.at(-1).jsonData = {
  463. gType: gType.value,
  464. isGenerate: false
  465. }
  466. // messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
  467. // messages.value.at(-1).loading = false
  468. // messages.value.at(-1).chatloading = false
  469. // chatLoading.value = false
  470. // return
  471. }
  472. else if (gType.value === 'trigger_batch_tool') {
  473. messages.value.at(-1).jsonData = {
  474. gType: gType.value,
  475. isGenerate: false
  476. }
  477. let isHave = 1
  478. const hasUnprocessedTools = slidesStore.slides.some(slide => slide.toolsArray && slide.toolsArray?.length > 0)
  479. if (hasUnprocessedTools) {
  480. isHave = 2
  481. }
  482. if (isHave == 2) {
  483. messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc3
  484. messages.value.at(-1).loading = false
  485. chatLoading.value = false
  486. messages.value.at(-1).chatloading = false
  487. return
  488. }
  489. messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc
  490. // messages.value.at(-1).loading = false
  491. // messages.value.at(-1).chatloading = false
  492. // chatLoading.value = false
  493. // return
  494. }
  495. if (content.includes(lang.ssAiChatQuickGenWebTitle)) {
  496. gType.value = 'generate_interactive_web'
  497. messages.value.at(-1).jsonData = {
  498. gType: 'generate_interactive_web',
  499. isGenerate: false
  500. }
  501. }
  502. if (content.includes(lang.ssAiChatQuickAction5)) {
  503. gType.value = 'trigger_tool_recommend'
  504. messages.value.at(-1).jsonData = {
  505. gType: 'trigger_tool_recommend',
  506. isGenerate: false
  507. }
  508. }
  509. if (content.includes(lang.ssBatchInteractiveWeb)) {
  510. gType.value = 'trigger_batch_tool'
  511. messages.value.at(-1).jsonData = {
  512. gType: 'trigger_batch_tool',
  513. isGenerate: false
  514. }
  515. let isHave = 1
  516. const hasUnprocessedTools = slidesStore.slides.some(slide => slide.toolsArray && slide.toolsArray?.length > 0)
  517. if (hasUnprocessedTools) {
  518. isHave = 2
  519. }
  520. if (isHave == 2) {
  521. messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc3
  522. messages.value.at(-1).loading = false
  523. chatLoading.value = false
  524. messages.value.at(-1).chatloading = false
  525. return
  526. }
  527. messages.value.at(-1).aiContent = lang.ssBatchInteractiveWebDesc
  528. // messages.value.at(-1).loading = false
  529. // chatLoading.value = false
  530. // messages.value.at(-1).chatloading = false
  531. // return
  532. }
  533. }
  534. else {
  535. messages.value.at(-1).jsonData = {
  536. gType: gType.value,
  537. isGenerate: false
  538. }
  539. }
  540. const getTypeLabel = (type?: number) => {
  541. const typeMap: Record<number, string> = {
  542. 45: lang.ssChoiceQ,
  543. 15: lang.ssQATest,
  544. 72: lang.ssAiApp,
  545. 73: lang.ssHPage,
  546. 81: lang.ssHPage,
  547. 74: lang.ssVideo,
  548. 75: lang.lang == 'cn' ? lang.ssBiliVideo : lang.ssYouTube,
  549. 76: lang.ssCreative,
  550. 77: lang.ssEnglishSpeakingTool,
  551. 78: lang.ssVote,
  552. 79: lang.ssPhoto,
  553. 84: lang.ssFile,
  554. 831: lang.ssKwlTitle,
  555. }
  556. return typeMap[type || 0] || lang.ssUnknown
  557. }
  558. // 提取所有页面中 type 为 'text' 的元素的纯文本内容
  559. const allTextContents = slidesStore.slides
  560. .map((slide: any, index: number) => {
  561. const elements = slide.elements.filter((element: any) => {
  562. if (element.type === 'text' || (element.type === 'shape' && element.text && element.text.content)) {
  563. return true
  564. }
  565. // 如果 element.type 包含 frame 且 tooltype 是对应的工具,输出 lang.ssInteract(对应工具)
  566. if (element.type === 'frame' && element.toolType) {
  567. const toolLabel = getTypeLabel(element.toolType)
  568. if (toolLabel !== lang.ssUnknown) {
  569. return true
  570. }
  571. }
  572. return false
  573. })
  574. const pageContents = elements.map((textElement: any) => {
  575. let content = ''
  576. if (textElement.type === 'shape') {
  577. // 创建一个临时元素来解析 HTML 并提取纯文本
  578. const tempElement = document.createElement('div')
  579. tempElement.innerHTML = textElement.text.content
  580. content = tempElement.textContent || tempElement.innerText || ''
  581. }
  582. else if (textElement.type === 'frame' && textElement.toolType) {
  583. // 如果是 frame 类型且有 toolType,输出交互工具标签
  584. content = lang.ssInteract + `(${getTypeLabel(textElement.toolType)})`
  585. }
  586. else {
  587. // 创建一个临时元素来解析 HTML 并提取纯文本
  588. const tempElement = document.createElement('div')
  589. tempElement.innerHTML = textElement.content
  590. content = tempElement.textContent || tempElement.innerText || ''
  591. }
  592. return content
  593. }).filter((content: string) => content.trim() !== '')
  594. const pageContent = pageContents.length > 0 ? pageContents.join(' ') : '无文本内容'
  595. return `第${index + 1}页的内容:${pageContent}`
  596. }) || []
  597. // 提取当前页面中 type 为 'text' 的元素的纯文本内容
  598. const textContents = slidesStore.currentSlide?.elements
  599. .filter((element: any) => element.type === 'text' || (element.type === 'shape' && element.text && element.text.content))
  600. .map((textElement: any) => {
  601. if (textElement.type === 'shape') {
  602. // 创建一个临时元素来解析 HTML 并提取纯文本
  603. const tempElement = document.createElement('div')
  604. tempElement.innerHTML = textElement.text.content
  605. return tempElement.textContent || tempElement.innerText || ''
  606. }
  607. // 创建一个临时元素来解析 HTML 并提取纯文本
  608. const tempElement = document.createElement('div')
  609. tempElement.innerHTML = textElement.content
  610. return tempElement.textContent || tempElement.innerText || ''
  611. })
  612. .filter(content => content.trim() !== '') || []
  613. console.log('allTextContents', allTextContents)
  614. console.log('textContents', textContents)
  615. let prompt = ''
  616. let agentid = agentid2.value
  617. if (gType.value === 'trigger_tool_recommend') {
  618. agentid = agentid3.value
  619. prompt = `
  620. #课程标题(course_title): ${props.courseTitle || ''}
  621. #当前页面内容(all_pages_content): ${allTextContents.length > 0 ? allTextContents.join('\n') : '无文本内容'}
  622. #query: ${action}
  623. `
  624. }
  625. else if (gType.value === 'trigger_batch_tool') {
  626. agentid = agentid4.value
  627. prompt = `
  628. #课程标题(course_title): ${props.courseTitle || ''}
  629. #当前页面内容(all_pages_content): ${allTextContents.length > 0 ? allTextContents.join('\n') : '无文本内容'}
  630. #query: ${action}
  631. `
  632. }
  633. else {
  634. prompt = `
  635. #课程标题(course_title): ${props.courseTitle || ''}
  636. #当前页面内容(current_page_content): ${textContents.length > 0 ? textContents.join('\n') : '无文本内容'}
  637. #query: ${action}
  638. `
  639. }
  640. if (gType.value === 'trigger_batch_tool') {
  641. loadingInterval.value = setInterval(() => {
  642. messages.value.at(-1).aiContent = messages.value.at(-1).aiContent.replace(/\./g, '')
  643. messages.value.at(-1).aiContent += loadingDots.value
  644. loadingDots.value = loadingDots.value === '...' ? '......' : '...'
  645. console.log('loadingDots.value', loadingDots.value)
  646. console.log('messages.value.at(-1).aiContent', messages.value.at(-1).aiContent)
  647. }, 500)
  648. const result = chat_no_stream(prompt, agentid, props.userid || '', lang.lang)
  649. streamController.value = result
  650. console.log(result)
  651. const content = await result.promise
  652. messages.value.at(-1).jsonData.aiContent = content
  653. console.log('event.data', content)
  654. generate(messages.value.at(-1))
  655. // chat_stream(prompt, agentid, props.userid || '', lang.lang, async (event) => {
  656. // if (event.type === 'message') {
  657. // messages.value.at(-1).jsonData.aiContent = md.render(event.data)
  658. // messages.value.at(-1).loading = false
  659. // }
  660. // else if (event.type === 'messageEnd') {
  661. // messages.value.at(-1).jsonData.aiContent = md.render(event.data)
  662. // console.log('event.data', event.data)
  663. // generate(messages.value.at(-1))
  664. // }
  665. // }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
  666. // streamController.value = controller
  667. // }).catch(err => {
  668. // chatLoading.value = false
  669. // console.log('err', err)
  670. // stopMessage()
  671. // })
  672. return
  673. }
  674. else if (gType.value === 'generate_interactive_web') {
  675. agentid = agentid5.value
  676. const result = chat_no_stream(prompt, agentid, props.userid || '', lang.lang)
  677. streamController.value = result
  678. console.log(result)
  679. const content = await result.promise
  680. messages.value.at(-1).jsonData.aiContent = content
  681. console.log('event.data', content)
  682. generate(messages.value.at(-1))
  683. // chat_stream(prompt, agentid, props.userid || '', lang.lang, async (event) => {
  684. // if (event.type === 'message') {
  685. // // messages.value.at(-1).jsonData.aiContent = md.render(event.data)
  686. // // messages.value.at(-1).loading = false
  687. // }
  688. // else if (event.type === 'messageEnd') {
  689. // messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
  690. // messages.value.at(-1).jsonData.aiContent = event.data
  691. // messages.value.at(-1).loading = false
  692. // chatLoading.value = false
  693. // messages.value.at(-1).chatloading = false
  694. // }
  695. // }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
  696. // streamController.value = controller
  697. // }).catch(err => {
  698. // chatLoading.value = false
  699. // console.log('err', err)
  700. // stopMessage()
  701. // })
  702. return
  703. }
  704. chat_stream(prompt, agentid, props.userid || '', lang.lang, (event) => {
  705. if (event.type === 'message') {
  706. messages.value.at(-1).aiContent = md.render(katexZ(event.data))
  707. messages.value.at(-1).loading = false
  708. prevChatResult()
  709. }
  710. else if (event.type === 'messageEnd') {
  711. messages.value.at(-1).aiContent = md.render(katexZ(event.data))
  712. messages.value.at(-1).chatloading = false
  713. chatLoading.value = false
  714. prevChatResult()
  715. }
  716. }, session_name.value, messages.value.at(-1).sourceFiles?.map(file => file.id).filter(Boolean)).then(controller => {
  717. streamController.value = controller
  718. }).catch(err => {
  719. chatLoading.value = false
  720. console.log('err', err)
  721. stopMessage()
  722. })
  723. }
  724. import useCreateElement from '@/hooks/useCreateElement'
  725. import useSlideHandler from '@/hooks/useSlideHandler'
  726. const { createSlide } = useSlideHandler()
  727. const { createFrameElement, createImageElement } = useCreateElement()
  728. const katexZ = (str) => {
  729. if (!str) return ''
  730. const escapeHtml = (text) => {
  731. return text
  732. .replace(/&/g, '&amp;')
  733. .replace(/</g, '&lt;')
  734. .replace(/>/g, '&gt;')
  735. }
  736. try {
  737. let result = ''
  738. let lastIndex = 0
  739. // 匹配 $$...$$(块级公式)或 $...$(行内公式)
  740. const regex = /\$\$([\s\S]+?)\$\$|\$([^$]+?)\$/g
  741. let match
  742. while ((match = regex.exec(str)) !== null) {
  743. // 转义并拼接公式前的纯文本
  744. result += escapeHtml(str.substring(lastIndex, match.index))
  745. if (match[1] !== undefined) {
  746. // 块级公式
  747. result += katex.renderToString(match[1], {
  748. throwOnError: false,
  749. displayMode: true
  750. })
  751. }
  752. else {
  753. // 行内公式
  754. result += katex.renderToString(match[2], {
  755. throwOnError: false
  756. })
  757. }
  758. lastIndex = regex.lastIndex
  759. }
  760. // 拼接剩余纯文本
  761. result += escapeHtml(str.substring(lastIndex))
  762. return result || escapeHtml(str)
  763. }
  764. catch (e) {
  765. return str
  766. }
  767. }
  768. const setUrl = () => {
  769. let url = 'https://beta.pbl.cocorobo.cn'
  770. if (lang.lang === 'cn') {
  771. url = 'https://pbl.cocorobo.cn/'
  772. }
  773. else if (lang.lang === 'hk') {
  774. url = 'https://pbl.cocorobo.hk/'
  775. }
  776. else if (lang.lang === 'en') {
  777. url = 'https://pbl.cocorobo.com/'
  778. }
  779. return url
  780. }
  781. const generate = async (message: ChatMessage) => {
  782. if (message.gLoading || message.jsonData?.isGenerate) {
  783. return
  784. }
  785. message.gLoading = true
  786. if (message.jsonData?.gType === 'generate_choice_question') {
  787. console.log(message.jsonData?.gType)
  788. const prompt = [
  789. {
  790. role: 'user',
  791. 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' ? '繁体中文' : '简体中文'}
  792. },
  793. ]
  794. chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
  795. console.log('选择题', JSON.parse(res))
  796. gType.value = 'chat'
  797. setPageId(45, res).then(res => {
  798. const baseUrl = setUrl()
  799. const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${45}&userid=${props.userid || ''}`
  800. createSlide()
  801. createFrameElement(url, 45)
  802. message.gLoading = false
  803. message.jsonData.isGenerate = true
  804. })
  805. })
  806. }
  807. else if (message.jsonData?.gType === 'generate_qa') {
  808. console.log(message.jsonData?.gType)
  809. const prompt = [
  810. {
  811. role: 'user',
  812. content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成问答题的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}。`, // 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
  813. },
  814. ]
  815. chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
  816. console.log('问答题', JSON.parse(res))
  817. gType.value = 'chat'
  818. setPageId(15, res).then(res => {
  819. const baseUrl = setUrl()
  820. const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${15}&userid=${props.userid || ''}`
  821. createSlide()
  822. createFrameElement(url, 15)
  823. message.gLoading = false
  824. message.jsonData.isGenerate = true
  825. })
  826. })
  827. }
  828. else if (message.jsonData?.gType === 'generate_poll') {
  829. console.log(message.jsonData?.gType)
  830. const prompt = [
  831. {
  832. role: 'user',
  833. 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":[],"userAnswer":[]]}。`, // 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
  834. },
  835. ]
  836. chat_no_stream2(prompt, { type: 'json_object' }).then(async (res: any) => {
  837. console.log('投票', JSON.parse(res))
  838. gType.value = 'chat'
  839. setPageId(78, res).then(res => {
  840. const baseUrl = setUrl()
  841. const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${78}&userid=${props.userid || ''}`
  842. createSlide()
  843. createFrameElement(url, 78)
  844. message.gLoading = false
  845. message.jsonData.isGenerate = true
  846. })
  847. })
  848. }
  849. else if (message.jsonData?.gType === 'generate_take_photo') {
  850. console.log(message.jsonData?.gType)
  851. const prompt = [
  852. {
  853. role: 'user',
  854. content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成拍照的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[]}。`, // ,"imageList":[],"evaluationCriteria":"评价标准" 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
  855. },
  856. ]
  857. chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
  858. console.log('拍照', JSON.parse(res))
  859. gType.value = 'chat'
  860. setPageId(79, res).then(res => {
  861. const baseUrl = setUrl()
  862. const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${79}&userid=${props.userid || ''}`
  863. createSlide()
  864. createFrameElement(url, 79)
  865. message.gLoading = false
  866. message.jsonData.isGenerate = true
  867. })
  868. })
  869. }
  870. else if (message.jsonData?.gType === 'generate_file') {
  871. console.log(message.jsonData?.gType)
  872. const prompt = [
  873. {
  874. role: 'user',
  875. content: `这是用户输入的内容:“${message.aiContent}”,根据用户输入的内容,生成文件上传的json。输出一个json格式的回复,格式如下:{"answerQ":"问题","answer":"","fileList":[]}。`, // ,"imageList":[],"evaluationCriteria":"评价标准" 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
  876. },
  877. ]
  878. chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
  879. console.log('文件上传', JSON.parse(res))
  880. gType.value = 'chat'
  881. setPageId(84, res).then(res => {
  882. const baseUrl = setUrl()
  883. const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${res}&type=${84}&userid=${props.userid || ''}`
  884. createSlide()
  885. createFrameElement(url, 84)
  886. message.gLoading = false
  887. message.jsonData.isGenerate = true
  888. })
  889. })
  890. }
  891. else if (message.jsonData?.gType === 'generate_image') {
  892. console.log(message.jsonData?.gType)
  893. const prompt = `${message.aiContent} 根据用户的内容,生成图片`
  894. const result = chat_no_stream(prompt, agentidPhoto.value, props.userid || '', lang.lang)
  895. console.log('图片', result)
  896. const content = await result.promise
  897. console.log('event.data', content)
  898. // 从content中提取图片链接
  899. const urlMatch = content.match(/\[.*?\]\((https?:\/\/.*?)\)/)
  900. const imageUrl = urlMatch ? urlMatch[1] : ''
  901. // 判断当前页面是否是工具页(含frame元素)
  902. const isToolPage = (currentSlide.value?.elements || []).some((el: any) => el.type === 'frame')
  903. // 如果当前页不是工具页则直接加图片,是工具页则新建页面再加图片
  904. if (!isToolPage) {
  905. createImageElement(imageUrl)
  906. }
  907. else {
  908. createSlide()
  909. createImageElement(imageUrl)
  910. }
  911. message.gLoading = false
  912. message.jsonData.isGenerate = true
  913. }
  914. else if (message.jsonData?.gType === 'trigger_batch_tool') {
  915. console.log(message.jsonData?.gType)
  916. const prompt = [
  917. {
  918. role: 'user',
  919. content: `这是用户输入的内容:“${message.jsonData.aiContent}”,根据用户输入的内容,取出里面candidates的值。输出一个json格式的回复`, // ,"imageList":[],"evaluationCriteria":"评价标准" 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
  920. },
  921. ]
  922. chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
  923. console.log('批量生成工具', JSON.parse(res))
  924. gType.value = 'chat'
  925. messages.value.at(-1).chatloading = false
  926. clearInterval(loadingInterval.value)
  927. loadingInterval.value = null
  928. messages.value.push({
  929. role: 'ai',
  930. aiContent: lang.ssBatchInteractiveWebDesc2,
  931. jsonData: {
  932. gType: 'trigger_batch_tool'
  933. }
  934. })
  935. chatLoading.value = false
  936. const array = JSON.parse(res).candidates
  937. for (let i = 0; i < array.length; i++) {
  938. const item = array[i]
  939. const pageIndex = item.page_index - 1
  940. if (pageIndex >= 0 && pageIndex < slidesStore.slides.length) {
  941. const slide = slidesStore.slides[pageIndex]
  942. slidesStore.updateSlide({ toolsArray: item.tools }, slide.id)
  943. }
  944. }
  945. for (let i = 0; i < slidesStore.slides.length; i++) {
  946. const slide = slidesStore.slides[i]
  947. if (slide.toolsArray && slide.toolsArray.length > 0) {
  948. for (let j = 0; j < slide.toolsArray.length; j++) {
  949. const tool = slide.toolsArray[j]
  950. if (tool.url) {
  951. continue
  952. }
  953. createTool(slide.toolsArray, tool, j, slide)
  954. }
  955. }
  956. }
  957. const firstPageWithTools = slidesStore.slides.findIndex((slide: any) =>
  958. slide.toolsArray && slide.toolsArray.length > 0
  959. )
  960. if (firstPageWithTools !== -1) {
  961. slidesStore.updateSlideIndex(firstPageWithTools)
  962. mainStore.setChildPageState(true, slidesStore.slides[firstPageWithTools].toolsArray || [], firstPageWithTools || 0)
  963. }
  964. })
  965. }
  966. else if (message.jsonData?.gType === 'generate_interactive_web') {
  967. console.log(message.jsonData?.gType)
  968. const prompt = [
  969. {
  970. role: 'user',
  971. content: `这是用户输入的内容:“${message.jsonData.aiContent}”,根据用户输入的内容,取出里面prompt的值。输出一个json格式的回复{"prompt":""}。`, // ,"imageList":[],"evaluationCriteria":"评价标准" 输出语言为${lang.lang === 'en' ? '英文' : lang.lang === 'hk' ? '繁体中文' : '简体中文'}
  972. },
  973. ]
  974. chat_no_stream2(prompt, { type: 'json_object' }).then((res: any) => {
  975. console.log('批量生成工具', JSON.parse(res))
  976. gType.value = 'chat'
  977. chatLoading.value = false
  978. messages.value.at(-1).aiContent = lang.ssInteractiveWebWorkspaceDesc
  979. messages.value.at(-1).jsonData.aiContent = JSON.parse(res).prompt
  980. messages.value.at(-1).loading = false
  981. chatLoading.value = false
  982. messages.value.at(-1).chatloading = false
  983. })
  984. }
  985. }
  986. const createTool = async (array: any[], tool: any, index: number, slide: any) => {
  987. let action = ''
  988. let type = 0
  989. if (tool.tool_id === 'choice') {
  990. action = lang.ssAiChatQuickAction1
  991. type = 45
  992. }
  993. else if (tool.tool_id === 'qa') {
  994. action = lang.ssAiChatQuickAction2
  995. type = 15
  996. }
  997. else if (tool.tool_id === 'poll') {
  998. action = lang.ssAiChatQuickAction6
  999. type = 78
  1000. }
  1001. else if (tool.tool_id === 'photo') {
  1002. action = lang.ssAiChatQuickAction7
  1003. type = 79
  1004. }
  1005. else if (tool.tool_id === 'file') {
  1006. action = lang.ssAiChatQuickAction9
  1007. type = 84
  1008. }
  1009. const textContents = slide?.elements
  1010. .filter((element: any) => element.type === 'text' || (element.type === 'shape' && element.text && element.text.content))
  1011. .map((textElement: any) => {
  1012. if (textElement.type === 'shape') {
  1013. const tempElement = document.createElement('div')
  1014. tempElement.innerHTML = textElement.text.content
  1015. return tempElement.textContent || tempElement.innerText || ''
  1016. }
  1017. const tempElement = document.createElement('div')
  1018. tempElement.innerHTML = textElement.content
  1019. return tempElement.textContent || tempElement.innerText || ''
  1020. })
  1021. .filter(content => content.trim() !== '') || []
  1022. console.log('textContents', textContents)
  1023. const prompt = `
  1024. #课程标题(course_title): ${props.courseTitle || ''}
  1025. #当前页面内容(current_page_content): ${textContents.length > 0 ? textContents.join('\n') : '无文本内容'}
  1026. #query: ${action}
  1027. `
  1028. const result = chat_no_stream(prompt, agentid2.value, props.userid || '', lang.lang)
  1029. console.log(result)
  1030. const content = await result.promise
  1031. console.log('👶 创建工具:', content)
  1032. let jsonFormat = ''
  1033. if (tool.tool_id === 'choice') {
  1034. jsonFormat = `{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[1],"userAnswer":[],"explanation":"解析"}]}`
  1035. }
  1036. else if (tool.tool_id === 'qa') {
  1037. jsonFormat = `{"answerQ":"问题","answer":"","fileList":[],"imageList":[],"evaluationCriteria":"评价标准"}`
  1038. }
  1039. else if (tool.tool_id === 'poll') {
  1040. jsonFormat = `{"testCount":1,"testTitle":"","testJson":[{"id":"7de1fdb4-bec3-4324-8986-4623f838e3d7","type":"2","teststitle":"1+1?","checkList":["1","2","3"],"timuList":[],"answer":[],"userAnswer":[]}]}`
  1041. }
  1042. else if (tool.tool_id === 'photo') {
  1043. jsonFormat = `{"answerQ":"问题","answer":"","fileList":[]}`
  1044. }
  1045. else if (tool.tool_id === 'file') {
  1046. jsonFormat = `{"answerQ":"问题","answer":"","fileList":[]}`
  1047. }
  1048. const res = await chat_no_stream2([{
  1049. role: 'user',
  1050. content: `这是用户输入的内容:"${content}",根据用户输入的内容,有数学公式的话要保留对应的符号比如$,生成json。输出一个json格式的回复,格式如下:${jsonFormat}。`,
  1051. }], { type: 'json_object' })
  1052. console.log(`${tool.tool_name}`, res)
  1053. const pageId = await setPageId(type, res)
  1054. const baseUrl = setUrl()
  1055. const url = `${baseUrl}/pbl-teacher-table/dist/workPage.html#/setWorkPage?id=${pageId}&type=${type}&userid=${props.userid || ''}`
  1056. array[index].url = url
  1057. setTool(slide, array)
  1058. }
  1059. const setTool = (slide: any, toolsArray: any[]) => {
  1060. slidesStore.updateSlide({ toolsArray: toolsArray }, slide.id)
  1061. console.log(slide)
  1062. }
  1063. const setPageId = async (tool: any, json: any) => {
  1064. const res = await getWorkPageId({
  1065. userid: props.userid || '',
  1066. type: tool,
  1067. json: json
  1068. })
  1069. return res[0][0].id
  1070. }
  1071. // 上传文件
  1072. const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
  1073. try {
  1074. const uuid = uuidv4()
  1075. const formData = new FormData()
  1076. const timestamp = Date.now()
  1077. const finalExtension = file.name.split('.').pop()?.toLowerCase() || ''
  1078. const baseName = file.name.slice(0, -(finalExtension.length + 1))
  1079. formData.append(
  1080. 'file',
  1081. new File([file], `${baseName}${timestamp}.${finalExtension}`)
  1082. )
  1083. formData.append('collection_ids', JSON.stringify([]))
  1084. formData.append('id', uuid)
  1085. formData.append('metadata', JSON.stringify({ title: file.name }))
  1086. formData.append('ingestion_mode', 'fast')
  1087. formData.append('run_with_orchestration', 'true')
  1088. // 同步知识库
  1089. const res = await axios.post(
  1090. 'https://r2rserver.cocorobo.cn/v3/documents',
  1091. formData,
  1092. {
  1093. headers: {
  1094. 'Content-Type': 'multipart/form-data',
  1095. },
  1096. signal: signal
  1097. }
  1098. )
  1099. console.log(res)
  1100. return res
  1101. }
  1102. catch (error) {
  1103. console.log('err', error)
  1104. if (error.name === 'AbortError') {
  1105. throw error
  1106. }
  1107. return ''
  1108. }
  1109. }
  1110. const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
  1111. const agentid2 = ref('f86aa63c-b7b7-4d03-9b37-b59f116d36f3')// 生成内容
  1112. const agentid3 = ref('caaa43ab-41a5-466a-94bc-74e0c9265068')// 互动工具推荐
  1113. const agentid4 = ref('b41bc650-9861-468c-a624-b52269dfff22')// 批量生成工具
  1114. const agentid5 = ref('939d28ef-3a1b-442b-871f-3894eb67ac9c')// 互动网页
  1115. const agentidPhoto = ref('2adb7966-86a3-42b4-a9fb-7439635fb28b')// 互动图片
  1116. const isQuickActions = ref<string[]>([])
  1117. const toggleQuickActions = (section: string) => {
  1118. if (isQuickActions.value.includes(section)) {
  1119. isQuickActions.value = isQuickActions.value.filter((item: string) => item !== section)
  1120. }
  1121. else {
  1122. isQuickActions.value.push(section)
  1123. }
  1124. }
  1125. const cocoQuickTab = ref('page')
  1126. const setCocoQuickTab = (tab: string) => {
  1127. cocoQuickTab.value = tab
  1128. }
  1129. // AI Web 相关状态
  1130. const create_app_loading = ref(false)
  1131. const webId = ref('')
  1132. const showAiWebModal = ref(false)
  1133. const isEdit = ref(false)
  1134. const webTx = ref('')
  1135. import { storeToRefs } from 'pinia'
  1136. import _ from 'lodash'
  1137. const { currentSlide } = storeToRefs(slidesStore)
  1138. const handle_add_aiWeb = _.throttle(async (message: any) => {
  1139. console.log(message)
  1140. const aiContent = message?.jsonData?.aiContent || ''
  1141. let tx = ''
  1142. if (aiContent) {
  1143. tx = '&aiContent=' + aiContent
  1144. webTx.value = tx
  1145. }
  1146. else {
  1147. webTx.value = tx
  1148. }
  1149. create_app_loading.value = true
  1150. try {
  1151. const elements = currentSlide.value?.elements || []
  1152. console.log(elements)
  1153. const existingWebElement = elements.find((el: any) => el.toolType === 73 && el.webId && el.type === 'frame')
  1154. if (existingWebElement && existingWebElement.webId) {
  1155. webId.value = existingWebElement.webId
  1156. create_app_loading.value = false
  1157. showAiWebModal.value = true
  1158. isEdit.value = true
  1159. return
  1160. }
  1161. const res = await axios.post('https://appapi.cocorobo.cn/api/agents/ai_edit', {
  1162. userid: props.userid || '',
  1163. username: props.userJson.name || '',
  1164. muti_name: lang.ssAddWebH5,
  1165. description: lang.ssAddWebH5,
  1166. 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',
  1167. organizeid: props.userJson.organizeid || '',
  1168. content: ''
  1169. })
  1170. console.log(res)
  1171. webId.value = res.id || ''
  1172. create_app_loading.value = false
  1173. showAiWebModal.value = true
  1174. }
  1175. catch (error) {
  1176. console.log(error)
  1177. create_app_loading.value = false
  1178. }
  1179. }, 3000)
  1180. const isLoading = ref(false)
  1181. import useImport from '@/hooks/useImport'
  1182. const { uploadFileToS3 } = useImport()
  1183. const addAiWeb = async (code: string) => {
  1184. if (!code) return
  1185. const file = new File([code], 'index.html', { type: 'text/html' })
  1186. isLoading.value = true
  1187. const url = await uploadFileToS3(file)
  1188. if (isEdit.value) {
  1189. const elements = currentSlide.value?.elements || []
  1190. const frameElement = elements.find((el: any) => el.type === 'frame' && (el.toolType === 73))
  1191. if (frameElement) {
  1192. slidesStore.updateElement({
  1193. id: frameElement.id,
  1194. props: { url: url }
  1195. })
  1196. }
  1197. }
  1198. else {
  1199. createSlide()
  1200. createFrameElement(url, 73, webId.value)
  1201. }
  1202. isLoading.value = false
  1203. showAiWebModal.value = false
  1204. webId.value = ''
  1205. isEdit.value = false
  1206. }
  1207. defineExpose({
  1208. sendQuickAction,
  1209. sendMessage
  1210. })
  1211. onMounted(() => {
  1212. session_name.value = uuidv4()
  1213. getAgentModel(agentid1.value)
  1214. getAgentModel(agentid2.value)
  1215. getAgentModel(agentid3.value)
  1216. getAgentModel(agentid4.value)
  1217. getAgentModel(agentid5.value)
  1218. getAgentModel(agentidPhoto.value)
  1219. for (let i = 0; i < slidesStore.slides.length; i++) {
  1220. const slide = slidesStore.slides[i]
  1221. if (slide.toolsArray && slide.toolsArray.length > 0) {
  1222. for (let j = 0; j < slide.toolsArray.length; j++) {
  1223. const tool = slide.toolsArray[j]
  1224. if (tool.url) {
  1225. continue
  1226. }
  1227. createTool(slide.toolsArray, tool, j, slide)
  1228. }
  1229. }
  1230. }
  1231. })
  1232. </script>
  1233. <style lang="scss" scoped>
  1234. .ai-chat-container {
  1235. width: 100%;
  1236. height: 100%;
  1237. display: flex;
  1238. flex-direction: column;
  1239. padding: 16px;
  1240. gap: 16px;
  1241. }
  1242. .input-section {
  1243. display: flex;
  1244. flex-direction: column;
  1245. gap: 12px;
  1246. position: relative;
  1247. }
  1248. .input-wrapper {
  1249. position: relative;
  1250. display: flex;
  1251. flex-direction: column;
  1252. background: #fafbfc;
  1253. border: 1.5px solid #e5e7eb;
  1254. border-radius: 8px;
  1255. padding: 8px 12px;
  1256. min-height: 120px;
  1257. }
  1258. .ai-input {
  1259. flex: 1;
  1260. border: none;
  1261. background: transparent;
  1262. font-size: 14px;
  1263. color: #374151;
  1264. outline: none;
  1265. resize: none;
  1266. min-height: 80px;
  1267. &::placeholder {
  1268. color: #9CA3AF;
  1269. }
  1270. }
  1271. .input-actions {
  1272. display: flex;
  1273. // justify-content: space-between;
  1274. align-items: center;
  1275. margin-top: 8px;
  1276. }
  1277. .attach-btn {
  1278. width: 32px;
  1279. height: 32px;
  1280. background: none;
  1281. border: none;
  1282. cursor: pointer;
  1283. padding: 4px;
  1284. display: flex;
  1285. align-items: center;
  1286. justify-content: center;
  1287. transition: all 0.3s ease;
  1288. color: #6b7280;
  1289. svg {
  1290. width: 20px;
  1291. height: 20px;
  1292. }
  1293. &:hover {
  1294. background: #FFF4E5;
  1295. color: #F78B22;
  1296. border-radius: 4px;
  1297. }
  1298. input[type="file"] {
  1299. display: none;
  1300. }
  1301. }
  1302. .file-box {
  1303. margin-bottom: 8px;
  1304. min-height: 24px;
  1305. max-height: 70px;
  1306. overflow-y: auto;
  1307. .file-item {
  1308. display: flex;
  1309. align-items: center;
  1310. background: #f5f5f5;
  1311. padding: 4px 8px;
  1312. border-radius: 4px;
  1313. margin-bottom: 4px;
  1314. .file-name {
  1315. flex: 1;
  1316. font-size: 12px;
  1317. color: #374151;
  1318. overflow: hidden;
  1319. text-overflow: ellipsis;
  1320. white-space: nowrap;
  1321. }
  1322. .remove-file-btn {
  1323. background: none;
  1324. border: none;
  1325. cursor: pointer;
  1326. color: #9CA3AF;
  1327. font-size: 12px;
  1328. padding: 2px;
  1329. margin-left: 8px;
  1330. display: flex;
  1331. align-items: center;
  1332. justify-content: center;
  1333. svg {
  1334. width: 14px;
  1335. height: 14px;
  1336. }
  1337. &:hover {
  1338. color: #EF4444;
  1339. }
  1340. }
  1341. }
  1342. }
  1343. .send-btn {
  1344. margin-left: auto;
  1345. width: 32px;
  1346. height: 32px;
  1347. border: none;
  1348. background: #FF9300;
  1349. color: #fff;
  1350. border-radius: 50%;
  1351. cursor: pointer;
  1352. display: flex;
  1353. align-items: center;
  1354. justify-content: center;
  1355. transition: all 0.3s ease;
  1356. svg {
  1357. width: 20px;
  1358. height: 20px;
  1359. }
  1360. &:hover {
  1361. background: #E68A00;
  1362. }
  1363. &.stop {
  1364. background: unset;
  1365. width: auto;
  1366. svg {
  1367. width: 32px;
  1368. height: 32px;
  1369. }
  1370. }
  1371. }
  1372. .quick-actions {
  1373. // display: flex;
  1374. // flex-direction: column;
  1375. // gap: 8px;
  1376. .quick-action-btn {
  1377. padding: 5px 10px;
  1378. border: 1px solid #f7c58f;
  1379. background: #FFF9F2;
  1380. color: #6b4a1f;
  1381. border-radius: 16px;
  1382. font-size: 12px;
  1383. cursor: pointer;
  1384. text-align: left;
  1385. display: block;
  1386. &:hover {
  1387. border-color: #F78B22;
  1388. background: #FFF4E5;
  1389. color: #111827;
  1390. }
  1391. +.quick-action-btn {
  1392. margin-top: 8px;
  1393. }
  1394. }
  1395. }
  1396. .chat-section {
  1397. // flex: 1;
  1398. height: calc(100% - 155px);
  1399. overflow-y: auto;
  1400. display: flex;
  1401. flex-direction: column;
  1402. // gap: 16px;
  1403. padding-right: 8px;
  1404. &::-webkit-scrollbar {
  1405. width: 6px;
  1406. }
  1407. &::-webkit-scrollbar-track {
  1408. background: #F3F4F6;
  1409. border-radius: 3px;
  1410. }
  1411. &::-webkit-scrollbar-thumb {
  1412. background: #D1D5DB;
  1413. border-radius: 3px;
  1414. &:hover {
  1415. background: #9CA3AF;
  1416. }
  1417. }
  1418. }
  1419. .chat-message {
  1420. max-width: 100%;
  1421. margin-bottom: 10px;
  1422. display: flex;
  1423. flex-direction: column;
  1424. .message-content {
  1425. display: flex;
  1426. flex-direction: column;
  1427. border-radius: 8px;
  1428. padding: 8px 10px;
  1429. font-size: 14px;
  1430. line-height: 1.5;
  1431. color: #374151;
  1432. width: fit-content;
  1433. position: relative;
  1434. +.message-content {
  1435. margin-top: 10px;
  1436. }
  1437. &.loading-state {
  1438. opacity: 0.7;
  1439. cursor: not-allowed;
  1440. pointer-events: none;
  1441. // .submenu-icon {
  1442. // animation: pulse 1s infinite;
  1443. // }
  1444. .spin {
  1445. animation: spin 1s linear infinite;
  1446. }
  1447. .submenu-icon {
  1448. position: absolute;
  1449. width: 100%;
  1450. height: 100%;
  1451. background: #00000026;
  1452. top: 0;
  1453. left: 0;
  1454. display: flex;
  1455. align-items: center;
  1456. justify-content: center;
  1457. svg {
  1458. height: 30px;
  1459. color: #fff;
  1460. }
  1461. }
  1462. }
  1463. }
  1464. }
  1465. .message-content {
  1466. word-break: break-word;
  1467. &.ai-message {
  1468. align-self: flex-start;
  1469. background: #fafbfc;
  1470. border: 1.5px solid #e5e7eb;
  1471. border-bottom-left-radius: 2px;
  1472. &>svg {
  1473. width: 17px;
  1474. height: 17px;
  1475. }
  1476. }
  1477. }
  1478. .message-content {
  1479. &.user-message {
  1480. align-self: flex-end;
  1481. background: #FFF4E5;
  1482. border: 1.5px solid #F78B22;
  1483. border-bottom-right-radius: 2px;
  1484. }
  1485. }
  1486. .initial-state {
  1487. display: flex;
  1488. flex-direction: column;
  1489. justify-content: flex-start;
  1490. align-items: flex-start;
  1491. // padding: 24px;
  1492. gap: 16px;
  1493. }
  1494. .confirm-btn {
  1495. margin-top: 10px;
  1496. padding: 6px 15px;
  1497. background: #FF9300;
  1498. color: white;
  1499. border: none;
  1500. border-radius: 8px;
  1501. font-size: 14px;
  1502. cursor: pointer;
  1503. margin-left: auto;
  1504. transition: all 0.3s ease;
  1505. &:hover {
  1506. background: #E68A00;
  1507. }
  1508. &.disabled {
  1509. background: #9CA3AF;
  1510. cursor: not-allowed;
  1511. }
  1512. }
  1513. ul {
  1514. margin: 8px 0;
  1515. padding-left: 20px;
  1516. li {
  1517. margin: 4px 0;
  1518. }
  1519. }
  1520. .quick-actions-popup {
  1521. position: absolute;
  1522. bottom: 100%;
  1523. left: 0;
  1524. right: 0;
  1525. background: white;
  1526. border: 1px solid #E5E7EB;
  1527. border-radius: 8px;
  1528. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  1529. padding: 8px;
  1530. z-index: 100;
  1531. margin-bottom: 8px;
  1532. .quick-action-btn {
  1533. width: 100%;
  1534. text-align: left;
  1535. padding: 10px 12px;
  1536. background: white;
  1537. border: none;
  1538. border-radius: 6px;
  1539. font-size: 14px;
  1540. color: #374151;
  1541. cursor: pointer;
  1542. transition: all 0.2s ease;
  1543. &:hover {
  1544. background: #fff4e5;
  1545. }
  1546. }
  1547. }
  1548. .message-quick-box {
  1549. display: flex;
  1550. flex-direction: column;
  1551. flex-shrink: 0;
  1552. gap: 10px;
  1553. .message-quick-box-item {
  1554. box-shadow: rgba(17, 24, 39, 0.04) 0px 10px 30px;
  1555. border-width: 1px;
  1556. border-style: solid;
  1557. border-color: rgb(239, 229, 216);
  1558. border-image: initial;
  1559. border-radius: 18px;
  1560. background: rgba(255, 255, 255, 0.96);
  1561. overflow: hidden;
  1562. .coco-collapse-trigger {
  1563. width: 100%;
  1564. display: flex;
  1565. align-items: center;
  1566. cursor: pointer;
  1567. text-align: left;
  1568. gap: 12px;
  1569. border-width: initial;
  1570. border-style: none;
  1571. border-color: initial;
  1572. border-image: initial;
  1573. background: transparent;
  1574. padding: 12px 14px;
  1575. &:hover {
  1576. background: rgb(255, 251, 245);
  1577. }
  1578. }
  1579. .coco-collapse-icon.soft-orange {
  1580. color: rgb(154, 91, 17);
  1581. background: rgb(255, 240, 219);
  1582. }
  1583. .coco-collapse-icon {
  1584. width: 34px;
  1585. height: 34px;
  1586. display: flex;
  1587. align-items: center;
  1588. justify-content: center;
  1589. flex-shrink: 0;
  1590. border-radius: 12px;
  1591. }
  1592. .coco-collapse-copy {
  1593. min-width: 0px;
  1594. display: flex;
  1595. flex-direction: column;
  1596. flex: 1 1 0%;
  1597. gap: 2px;
  1598. .coco-collapse-title {
  1599. font-size: 14px;
  1600. font-weight: 700;
  1601. color: rgb(17, 24, 39);
  1602. }
  1603. .coco-collapse-subtitle {
  1604. font-size: 12px;
  1605. color: rgb(139, 115, 86);
  1606. line-height: 1.5;
  1607. }
  1608. }
  1609. .coco-collapse-chevron {
  1610. color: rgb(156, 163, 175);
  1611. flex-shrink: 0;
  1612. svg {
  1613. transition: transform 0.2s;
  1614. }
  1615. }
  1616. &.active {
  1617. .coco-collapse-chevron svg {
  1618. transform: rotate(180deg);
  1619. }
  1620. }
  1621. .coco-collapse-body {
  1622. padding: 0px 14px 14px;
  1623. .coco-quick-tabs {
  1624. display: grid;
  1625. grid-template-columns: repeat(2, minmax(0px, 1fr));
  1626. margin-bottom: 12px;
  1627. gap: 8px;
  1628. .coco-quick-tab {
  1629. height: 34px;
  1630. color: rgb(124, 92, 56);
  1631. font-size: 12px;
  1632. font-weight: 600;
  1633. cursor: pointer;
  1634. border-radius: 12px;
  1635. border-width: 1px;
  1636. border-style: solid;
  1637. border-color: rgb(234, 223, 206);
  1638. border-image: initial;
  1639. background: rgb(250, 247, 242);
  1640. transition: 0.2s;
  1641. &.active {
  1642. color: rgb(154, 91, 17);
  1643. box-shadow: rgba(247, 139, 34, 0.12) 0px 8px 18px;
  1644. border-color: rgba(247, 139, 34, 0.48);
  1645. background: rgb(255, 244, 229);
  1646. }
  1647. }
  1648. }
  1649. .coco-quick-list {
  1650. display: flex;
  1651. flex-direction: column;
  1652. gap: 8px;
  1653. .coco-quick-action {
  1654. width: 100%;
  1655. display: flex;
  1656. align-items: flex-start;
  1657. cursor: pointer;
  1658. text-align: left;
  1659. gap: 12px;
  1660. border-width: 1px;
  1661. border-style: solid;
  1662. border-color: rgb(239, 229, 216);
  1663. border-image: initial;
  1664. background: rgb(255, 253, 250);
  1665. border-radius: 14px;
  1666. padding: 12px;
  1667. transition: 0.2s;
  1668. .coco-quick-action-copy {
  1669. min-width: 0px;
  1670. display: flex;
  1671. flex-direction: column;
  1672. flex: 1 1 0%;
  1673. gap: 4px;
  1674. .coco-quick-action-title {
  1675. font-size: 13px;
  1676. font-weight: 700;
  1677. color: rgb(17, 24, 39);
  1678. line-height: 1.4;
  1679. }
  1680. .coco-quick-action-desc {
  1681. font-size: 12px;
  1682. line-height: 1.6;
  1683. color: rgb(139, 115, 86);
  1684. }
  1685. }
  1686. .coco-quick-action-arrow {
  1687. color: rgb(212, 163, 115);
  1688. flex-shrink: 0;
  1689. padding-top: 2px;
  1690. }
  1691. }
  1692. }
  1693. }
  1694. }
  1695. }
  1696. .coco-interactive-web-card {
  1697. width: 100%;
  1698. margin-top: 10px;
  1699. display: flex;
  1700. flex-direction: column;
  1701. text-align: left;
  1702. cursor: pointer;
  1703. border-width: 1px;
  1704. border-style: solid;
  1705. border-color: rgb(247, 197, 143);
  1706. border-image: initial;
  1707. border-radius: 14px;
  1708. background: linear-gradient(rgb(255, 250, 243) 0%, rgb(255, 255, 255) 100%);
  1709. padding: 12px 14px;
  1710. gap: 8px;
  1711. transition: 0.2s;
  1712. position: relative;
  1713. overflow: hidden;
  1714. &:hover {
  1715. box-shadow: rgba(247, 139, 34, 0.1) 0px 10px 24px;
  1716. transform: translateY(-1px);
  1717. border-color: rgb(247, 139, 34);
  1718. }
  1719. &.loading-state {
  1720. opacity: 0.7;
  1721. cursor: not-allowed;
  1722. pointer-events: none;
  1723. // .submenu-icon {
  1724. // animation: pulse 1s infinite;
  1725. // }
  1726. .spin {
  1727. animation: spin 1s linear infinite;
  1728. }
  1729. .submenu-icon {
  1730. position: absolute;
  1731. width: 100%;
  1732. height: 100%;
  1733. background: #00000026;
  1734. top: 0;
  1735. left: 0;
  1736. display: flex;
  1737. align-items: center;
  1738. justify-content: center;
  1739. svg {
  1740. height: 30px;
  1741. color: #fff;
  1742. }
  1743. }
  1744. }
  1745. .coco-interactive-web-card-head,
  1746. .coco-interactive-web-card-foot {
  1747. display: flex;
  1748. align-items: center;
  1749. justify-content: space-between;
  1750. gap: 10px;
  1751. .coco-interactive-web-card-title {
  1752. min-width: 0px;
  1753. font-size: 14px;
  1754. font-weight: 700;
  1755. color: rgb(17, 24, 39);
  1756. line-height: 1.4;
  1757. flex: 1 1 0%;
  1758. }
  1759. .coco-interactive-web-card-action {
  1760. margin-left: auto;
  1761. font-size: 12px;
  1762. font-weight: 700;
  1763. color: rgb(240, 120, 21);
  1764. }
  1765. }
  1766. .coco-interactive-web-card-desc {
  1767. font-size: 12px;
  1768. color: rgb(107, 114, 128);
  1769. line-height: 1.6;
  1770. }
  1771. }
  1772. </style>
  1773. <style>
  1774. .chat table {
  1775. text-align: center;
  1776. border-spacing: 0;
  1777. border-left: 1px solid #000;
  1778. border-bottom: 1px solid #000;
  1779. }
  1780. .chat table td,
  1781. .chat table th {
  1782. border-top: 1px solid #000;
  1783. border-right: 1px solid #000;
  1784. padding: 10px;
  1785. }
  1786. .message-files {
  1787. margin-top: 8px;
  1788. display: flex;
  1789. flex-direction: column;
  1790. gap: 4px;
  1791. }
  1792. .message-file-item {
  1793. display: flex;
  1794. align-items: center;
  1795. gap: 6px;
  1796. font-size: 12px;
  1797. color: #6b7280;
  1798. background: #f3f4f6;
  1799. padding: 4px 8px;
  1800. border-radius: 4px;
  1801. max-width: 200px;
  1802. overflow: hidden;
  1803. }
  1804. .message-file-item span {
  1805. flex: 1;
  1806. white-space: nowrap;
  1807. overflow: hidden;
  1808. text-overflow: ellipsis;
  1809. }
  1810. </style>